Merge branch 'master' into UserMailTemplates

# Conflicts:
#	dev/App/User.js
#	dev/Common/Enums.js
#	dev/Screen/User/Settings.js
#	dev/Settings/Admin/General.js
#	snappymail/v/0.0.0/app/libraries/RainLoop/Actions.php
#	snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Admin.php
#	snappymail/v/0.0.0/app/libraries/RainLoop/Actions/User.php
This commit is contained in:
djmaze 2022-03-15 15:00:14 +01:00
commit f7f56b3789
794 changed files with 96444 additions and 74188 deletions

View file

@ -15,7 +15,7 @@ RUN pecl install xxtea-1.0.11 && \
RUN docker-php-ext-configure intl && \ RUN docker-php-ext-configure intl && \
docker-php-ext-configure ldap && \ docker-php-ext-configure ldap && \
docker-php-ext-configure gd --with-freetype-dir=/usr/include/ --with-jpeg-dir=/usr/include/ && \ docker-php-ext-configure gd --with-freetype=/usr/include/ --with-jpeg=/usr/include/ && \
docker-php-ext-install opcache pdo_mysql zip intl gd ldap docker-php-ext-install opcache pdo_mysql zip intl gd ldap
RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer

View file

@ -27,7 +27,7 @@ RUN mkdir -p /usr/share/man/man1/ /usr/share/man/man3/ /usr/share/man/man7/ && \
RUN php -m && \ RUN php -m && \
docker-php-ext-configure ldap --with-libdir=lib/x86_64-linux-gnu/ && \ docker-php-ext-configure ldap --with-libdir=lib/x86_64-linux-gnu/ && \
docker-php-ext-configure intl && \ docker-php-ext-configure intl && \
docker-php-ext-configure gd --with-freetype-dir=/usr/include --with-jpeg-dir=/usr/include/ && \ docker-php-ext-configure gd --with-freetype --with-jpeg && \
docker-php-ext-install ldap opcache pdo_mysql pdo_pgsql zip intl gd && \ docker-php-ext-install ldap opcache pdo_mysql pdo_pgsql zip intl gd && \
php -m php -m

View file

@ -30,6 +30,9 @@ allow_additional_identities = On
; Number of messages displayed on page by default ; Number of messages displayed on page by default
messages_per_page = 20 messages_per_page = 20
; Mark message read after N seconds
message_read_delay = 5
; File size limit (MB) for file upload on compose screen ; File size limit (MB) for file upload on compose screen
; 0 for unlimited. ; 0 for unlimited.
attachment_size_limit = 2 attachment_size_limit = 2
@ -68,6 +71,8 @@ hide_x_mailer_header = On
admin_panel_host = "" admin_panel_host = ""
admin_panel_key = "admin" admin_panel_key = "admin"
content_security_policy = "" content_security_policy = ""
csp_report = Off
encrypt_cipher = "aes-256-cbc-hmac-sha1"
[ssl] [ssl]
; Require verification of SSL certificate used. ; Require verification of SSL certificate used.
@ -86,18 +91,12 @@ capath = ""
client_cert = "" client_cert = ""
[capa] [capa]
composer = On
contacts = On contacts = On
settings = On
quota = On quota = On
help = On
reload = On
search = On search = On
search_adv = On search_adv = On
x-templates = Off
dangerous_actions = On dangerous_actions = On
message_actions = On message_actions = On
messagelist_actions = On
attachments_actions = On attachments_actions = On
[login] [login]
@ -134,6 +133,7 @@ view_editor_type = "Html"
view_layout = 1 view_layout = 1
view_use_checkboxes = On view_use_checkboxes = On
autologout = 30 autologout = 30
view_html = On
show_images = Off show_images = Off
contacts_autosave = On contacts_autosave = On
mail_use_threads = Off mail_use_threads = Off
@ -144,6 +144,17 @@ mail_reply_same_folder = Off
; Enable logging ; Enable logging
enable = Off enable = Off
; Log messages of set RFC 5424 section 6.2.1 Severity level and higher (0 = highest, 7 = lowest).
; 0 = Emergency
; 1 = Alert
; 2 = Critical
; 3 = Error
; 4 = Warning
; 5 = Notice
; 6 = Informational
; 7 = Debug
level = 4
; Logs entire request only if error occured (php requred) ; Logs entire request only if error occured (php requred)
write_on_error_only = Off write_on_error_only = Off
@ -193,6 +204,9 @@ auth_logging = Off
auth_logging_filename = "fail2ban/auth-{date:Y-m-d}.txt" auth_logging_filename = "fail2ban/auth-{date:Y-m-d}.txt"
auth_logging_format = "[{date:Y-m-d H:i:s}] Auth failed: ip={request:ip} user={imap:login} host={imap:host} port={imap:port}" auth_logging_format = "[{date:Y-m-d H:i:s}] Auth failed: ip={request:ip} user={imap:login} host={imap:host} port={imap:port}"
; Enable auth logging to syslog for fail2ban
auth_syslog = On
[debug] [debug]
; Special option required for development purposes ; Special option required for development purposes
enable = Off enable = Off
@ -206,7 +220,7 @@ enable = On
; Additional caching key. If changed, cache is purged ; Additional caching key. If changed, cache is purged
index = "v1" index = "v1"
; Can be: files, APC, memcache, redis (beta) ; Can be: files, APCU, memcache, redis (beta)
fast_cache_driver = "files" fast_cache_driver = "files"
; Additional caching key. If changed, fast cache is purged ; Additional caching key. If changed, fast cache is purged
@ -222,12 +236,10 @@ http_expires = 3600
server_uids = On server_uids = On
[labs] [labs]
update_channel = "stable" allow_prefetch = Off
allow_prefetch = On
allow_smart_html_links = On
cache_system_data = On cache_system_data = On
date_from_headers = On date_from_headers = On
autocreate_system_folders = On autocreate_system_folders = Off
allow_message_append = Off allow_message_append = Off
login_fault_delay = 1 login_fault_delay = 1
log_ajax_response_write_limit = 300 log_ajax_response_write_limit = 300
@ -239,7 +251,6 @@ use_mobile_version_for_tablets = Off
use_app_debug_css = Off use_app_debug_css = Off
use_imap_sort = On use_imap_sort = On
use_imap_force_selection = Off use_imap_force_selection = Off
use_imap_list_subscribe = On
use_imap_thread = On use_imap_thread = On
use_imap_move = Off use_imap_move = Off
use_imap_expunge_all_on_delete = Off use_imap_expunge_all_on_delete = Off
@ -254,18 +265,16 @@ imap_message_all_headers = Off
imap_large_thread_limit = 50 imap_large_thread_limit = 50
imap_folder_list_limit = 200 imap_folder_list_limit = 200
imap_show_login_alert = On imap_show_login_alert = On
imap_use_auth_plain = On imap_use_list_status = On
imap_use_auth_cram_md5 = Off imap_timeout = 300
smtp_show_server_errors = Off smtp_show_server_errors = Off
smtp_use_auth_plain = On smtp_timeout = 60
smtp_use_auth_cram_md5 = Off
sieve_utf8_folder_name = On
sieve_auth_plain_initial = On sieve_auth_plain_initial = On
sieve_allow_fileinto_inbox = Off sieve_allow_fileinto_inbox = Off
imap_timeout = 300
smtp_timeout = 60
sieve_timeout = 10 sieve_timeout = 10
domain_list_limit = 99 sasl_allow_plain = On
sasl_allow_scram_sha = Off
sasl_allow_cram_md5 = Off
mail_func_clear_headers = On mail_func_clear_headers = On
mail_func_additional_parameters = Off mail_func_additional_parameters = Off
favicon_status = On favicon_status = On
@ -285,12 +294,12 @@ cookie_default_path = ""
cookie_default_secure = Off cookie_default_secure = Off
check_new_messages = On check_new_messages = On
replace_env_in_configuration = "" replace_env_in_configuration = ""
startup_url = ""
strict_html_parser = Off strict_html_parser = Off
boundary_prefix = "" boundary_prefix = ""
kolab_enabled = Off
dev_email = "" dev_email = ""
dev_password = "" dev_password = ""
[version] [version]
current = "2.7.1" current = "2.13.4"
saved = "Mon, 23 Aug 2021 07:55:13 +0000" saved = "Fri, 04 Mar 2022 08:55:26 +0000"

View file

@ -9,7 +9,7 @@ module.exports = {
env: { env: {
node: true, node: true,
browser: true, browser: true,
es6: true es2020: true
}, },
globals: { globals: {
// SnappyMail // SnappyMail
@ -35,7 +35,9 @@ module.exports = {
// vendors/jua // vendors/jua
'Jua': "readonly", 'Jua': "readonly",
// vendors/bootstrap/bootstrap.native.js // vendors/bootstrap/bootstrap.native.js
'BSN': "readonly" 'BSN': "readonly",
// Mailvelope
'mailvelope': "readonly"
}, },
// http://eslint.org/docs/rules/ // http://eslint.org/docs/rules/
rules: { rules: {

1
.github/FUNDING.yml vendored
View file

@ -1,3 +1,2 @@
github: the-djmaze
community_bridge: SnappyMail community_bridge: SnappyMail
custom: ["https://www.paypal.me/thedjmaze", "https://snappymail.eu"] custom: ["https://www.paypal.me/thedjmaze", "https://snappymail.eu"]

8
.gitignore vendored
View file

@ -18,3 +18,11 @@
/include.php /include.php
.idea/ .idea/
.env .env
/test
/public_html
/vendors/knockout/spec
/vendors/openpgp-5
!/vendors/openpgp-5/dist
/vendors/vanillaqr.js/
/integrations/nextcloud/rainloop
/integrations/owncloud/rainloop

3
.gitmodules vendored Normal file
View file

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

View file

@ -34,7 +34,7 @@ And don't forget to read the [RainLoop documentation](https://www.rainloop.net/d
**GNU AFFERO GENERAL PUBLIC LICENSE Version 3 (AGPL)**. **GNU AFFERO GENERAL PUBLIC LICENSE Version 3 (AGPL)**.
http://www.gnu.org/licenses/agpl-3.0.html http://www.gnu.org/licenses/agpl-3.0.html
Copyright (c) 2020 - 2021 SnappyMail Copyright (c) 2020 - 2022 SnappyMail
Copyright (c) 2013 - 2021 RainLoop Copyright (c) 2013 - 2021 RainLoop
## Modifications ## Modifications
@ -45,24 +45,25 @@ This fork of RainLoop has the following changes:
* Admin uses password_hash/password_verify * Admin uses password_hash/password_verify
* Auth failed attempts written to syslog * Auth failed attempts written to syslog
* Added Fail2ban instructions * Added Fail2ban instructions
* ES2015 * ES2018
* PHP 7.3+ required * PHP 7.3+ required
* PHP mbstring extension required * PHP mbstring extension required
* PHP replaced pclZip with PharData and ZipArchive * PHP replaced pclZip with PharData and ZipArchive
* PHP yaml extension else use the old Spyc * Dark mode
* Added option to remove background/font colors from messages for real "dark mode" * Added option to remove background/font colors from messages for real "dark mode"
* Removed BackwardCapability (class \RainLoop\Account) * Removed BackwardCapability (class \RainLoop\Account)
* Removed ChangePassword (re-implemented as plugin) * Removed ChangePassword (re-implemented as plugin)
* Removed OAuth support
* Removed POP3 support * Removed POP3 support
* Removed background video support * Removed background video support
* Removed Sentry (Application Monitoring and Error Tracking Software) * Removed Sentry (Application Monitoring and Error Tracking Software)
* Removed Spyc yaml
* Replaced gulp-uglify with gulp-terser * Replaced gulp-uglify with gulp-terser
* CRLF => LF line endings * CRLF => LF line endings
* Embed boot.js and boot.css into index.html * Embed boot.js and boot.css into index.html
* Ongoing removal of old JavaScript code (things are native these days) * Ongoing removal of old JavaScript code (things are native these days)
* Added modified [Squire](https://github.com/neilj/Squire) HTML editor as replacement for CKEditor * Added modified [Squire](https://github.com/neilj/Squire) HTML editor as replacement for CKEditor
* Split Admin specific JavaScript code from User code * Split Admin specific JavaScript code from User code
* Split Sieve specific JavaScript code from User code
* JSON reviver * JSON reviver
* Better memory garbage collection management * Better memory garbage collection management
* Added serviceworker for Notifications * Added serviceworker for Notifications
@ -76,6 +77,16 @@ This fork of RainLoop has the following changes:
* Prevent Google FLoC * Prevent Google FLoC
* Added [Fetch Metadata Request Headers](https://www.w3.org/TR/fetch-metadata/) checks * Added [Fetch Metadata Request Headers](https://www.w3.org/TR/fetch-metadata/) checks
* Reduced excessive DOM size * Reduced excessive DOM size
* Support [Kolab groupware](https://kolab.org/)
* Support IMAP RFC 2971 ID extension
* Support IMAP RFC 5258 LIST-EXTENDED
* Support IMAP RFC 5464 METADATA
* Support IMAP RFC 5819 LIST-STATUS
* Support IMAP RFC 7628 SASL OAUTHBEARER aka XOAUTH2
* Support IMAP4rev2 RFC 9051
* Support Sodium and OpenSSL for encryption
* Much better PGP support
### Supported browsers ### Supported browsers
@ -96,10 +107,9 @@ The result is faster and smaller download code (good for mobile networks).
* Added dev/prototype.js for some additional features * Added dev/prototype.js for some additional features
* boot.js without webpack overhead * boot.js without webpack overhead
* Modified Jua.js to be without jQuery * Modified Jua.js to be without jQuery
* Replaced ProgressJS with simple native dropin
* Replaced Autolinker with simple https/email detection * Replaced Autolinker with simple https/email detection
* Replaced ifvisible.js with simple drop-in replacement * Replaced ifvisible.js with simple drop-in replacement
* Replaced momentToNode with proper HTML5 <time> * Replaced momentToNode with proper HTML5 `<time>`
* Replaced resize listeners with ResizeObserver * Replaced resize listeners with ResizeObserver
* Replaced bootstrap.js with native drop-in replacement * Replaced bootstrap.js with native drop-in replacement
* Replaced dev/Common/ClientStorageDriver/* with Web Storage Objects polyfill * Replaced dev/Common/ClientStorageDriver/* with Web Storage Objects polyfill
@ -123,36 +133,35 @@ The result is faster and smaller download code (good for mobile networks).
* Removed momentjs (use Intl) * Removed momentjs (use Intl)
* Removed opentip (use CSS) * Removed opentip (use CSS)
* Removed non-community (aka Prem/Premium/License) code * Removed non-community (aka Prem/Premium/License) code
* Removed ProgressJS
RainLoop 1.15 vs SnappyMail RainLoop 1.15 vs SnappyMail
|js/* |RainLoop |Snappy | |js/* |RainLoop |Snappy |
|--------------- |--------: |--------: | |--------------- |--------: |--------: |
|admin.js |2.158.025 | 88.633 | |admin.js |2.158.025 | 79.018 |
|app.js |4.215.733 | 446.059 | |app.js |4.215.733 | 407.697 |
|boot.js | 672.433 | 2.856 | |boot.js | 672.433 | 2.025 |
|libs.js | 647.679 | 213.208 | |libs.js | 647.679 | 200.131 |
|sieve.js | 0 | 75.642 |
|polyfills.js | 325.908 | 0 | |polyfills.js | 325.908 | 0 |
|serviceworker.js | 0 | 285 | |serviceworker.js | 0 | 285 |
|TOTAL |8.019.778 | 751.041 | |TOTAL |8.019.778 | 764.798 |
|js/min/* |RainLoop |Snappy |RL gzip |SM gzip |RL brotli |SM brotli | |js/min/* |RainLoop |Snappy |RL gzip |SM gzip |RL brotli |SM brotli |
|--------------- |--------: |--------: |------: |------: |--------: |--------: | |--------------- |--------: |--------: |------: |------: |--------: |--------: |
|admin.min.js | 255.514 | 45.597 | 73.899 | 13.933 | 60.674 | 12.463 | |admin.min.js | 255.514 | 39.256 | 73.899 | 13.076 | 60.674 | 11.702 |
|app.min.js | 516.000 | 228.862 |140.430 | 67.769 |110.657 | 57.420 | |app.min.js | 516.000 | 194.148 |140.430 | 62.297 |110.657 | 53.432 |
|boot.min.js | 66.456 | 1.648 | 22.553 | 986 | 20.043 | 822 | |boot.min.js | 66.456 | 1.252 | 22.553 | 782 | 20.043 | 631 |
|libs.min.js | 574.626 | 102.959 |177.280 | 37.514 |151.855 | 33.617 | |libs.min.js | 574.626 | 96.201 |177.280 | 35.522 |151.855 | 31.746 |
|sieve.min.js | 0 | 36.632 | 0 | 9.689 | 0 | 8.770 |
|polyfills.min.js | 32.608 | 0 | 11.315 | 0 | 10.072 | 0 | |polyfills.min.js | 32.608 | 0 | 11.315 | 0 | 10.072 | 0 |
|TOTAL |1.445.204 | 379.066 |425.477 |120.202 |353.301 |104.322 | |TOTAL user |1.189.690 | 291.601 |351.061 | 98.601 |292.627 | 85.809 |
|TOTAL (no admin) |1.189.690 | 333.469 |351.061 |106.269 |292.627 | 91.859 | |TOTAL user sieve |1.189.690 | 328.233 |351.061 |108.290 |292.627 | 94.579 |
|TOTAL admin |1.189.690 | 136.709 |351.061 | 49.380 |292.627 | 44.079 |
For a user its around 68% smaller and faster than traditional RainLoop. For a user its around 70% smaller and faster than traditional RainLoop.
|OpenPGP |RainLoop |Snappy |RL gzip |SM gzip |RL brotli |SM brotli |
|--------------- |--------: |--------: |------: |------: |--------: |--------: |
|openpgp.min.js | 330.742 | 293.972 |102.388 | 93.030 | 84.241 | 77.142 |
|openpgp.worker | 1.499 | 1.125 | 824 | 567 | 695 | 467 |
### CSS changes ### CSS changes
@ -169,13 +178,7 @@ For a user its around 68% smaller and faster than traditional RainLoop.
* Removed Internet Explorer from normalize.css * Removed Internet Explorer from normalize.css
* Removed node_modules/opentip/css/opentip.css * Removed node_modules/opentip/css/opentip.css
* Removed node_modules/pikaday/css/pikaday.css * Removed node_modules/pikaday/css/pikaday.css
* Removed vendors/bootstrap/less/breadcrumbs.less * Removed unused vendors/bootstrap/less/*
* Removed vendors/bootstrap/less/navbar.less
* Removed vendors/bootstrap/less/popovers.less
* Removed vendors/bootstrap/less/progress-bars.less
* Removed vendors/bootstrap/less/scaffolding.less
* Removed vendors/bootstrap/less/sprites.less
* Removed vendors/bootstrap/less/tooltip.less
* Removed vendors/jquery-nanoscroller/nanoscroller.css * Removed vendors/jquery-nanoscroller/nanoscroller.css
* Removed vendors/jquery-letterfx/jquery-letterfx.min.css * Removed vendors/jquery-letterfx/jquery-letterfx.min.css
* Removed vendors/Progress.js/minified/progressjs.min.css * Removed vendors/Progress.js/minified/progressjs.min.css
@ -184,12 +187,27 @@ For a user its around 68% smaller and faster than traditional RainLoop.
|css/* |RainLoop |Snappy |RL gzip |SM gzip |SM brotli | |css/* |RainLoop |Snappy |RL gzip |SM gzip |SM brotli |
|------------ |-------: |------: |------: |------: |--------: | |------------ |-------: |------: |------: |------: |--------: |
|app.css | 340.334 | 93.382 | 46.959 | 17.277 | 14.966 | |app.css | 340.334 | 80.865 | 46.959 | 16.751 | 14.420 |
|app.min.css | 274.791 | 75.885 | 39.618 | 15.430 | 13.632 | |app.min.css | 274.791 | 65.086 | 39.618 | 14.855 | 13.088 |
|boot.css | | 1.326 | | 664 | 545 | |boot.css | | 1.326 | | 664 | 545 |
|boot.min.css | | 1.071 | | 590 | 474 | |boot.min.css | | 1.071 | | 590 | 474 |
|admin.css | | 40.355 | | 8.507 | 7.437 | |admin.css | | 29.977 | | 6.795 | 5.900 |
|admin.min.css | | 31.835 | | 7.465 | 6.628 | |admin.min.css | | 24.101 | | 6.167 | 5.421 |
### PGP
RainLoop uses the old OpenPGP.js v2
SnappyMail v2.12 uses OpenPGP.js v5, GnuPG and Mailvelope.
SnappyMail is able to use and generate ECDSA and EDDSA keys, where RainLoop does not.
Since SnappyMail tries to achieve the best mobile experience, it forked OpenPGP.js to strip it down.
* remove all unused Node.js
* remove all old browsers support
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 | 539.642 |102.388 | 167.112 | 84.241 | 137.447 |
|openpgp.worker | 1.499 | | 824 | | 695 | |
### Squire vs CKEditor ### Squire vs CKEditor

40
SECURITY.md Normal file
View file

@ -0,0 +1,40 @@
# Security Policy
## Supported Versions
Currently due to the fast development only the latest version receives security updates.
| Version | Supported |
| -------- | --------- |
| 2.13.x | ✔ |
| < 2.13.0 | |
## Reporting a Vulnerability
Please report security issues or vulnerabilities as an encrypted email to [security@snappymail.eu](mailto:security@snappymail.eu).
Your report should be detailed enough with clear steps to reproduce and classify the found vulnerability.
You can find the PGP public key below and on the major public keyservers like [pgp.key-server.io](https://pgp.key-server.io).
```
-----BEGIN PGP PUBLIC KEY BLOCK-----
Comment: Type: 255-bit EdDSA
Comment: Fingerprint: 445D265124E6072671E64D0733F868A7E35E8277
mDMEYiE2QRYJKwYBBAHaRw8BAQdAqMrQUm6DddWcQNo0VEjNIu3Q6CfP3nokVv2Y
rNQ1avq0LFNuYXBweU1haWwgU2VjdXJpdHkgPHNlY3VyaXR5QHNuYXBweW1haWwu
ZXU+iJQEExYKADwWIQREXSZRJOYHJnHmTQcz+Gin416CdwUCYiE2QQIbAwULCQgH
AgMiAgEGFQoJCAsCBBYCAwECHgcCF4AACgkQM/hop+NegnfSGgD9GEHpOrvWpBGY
dYfvVd/+Lv5d+dFBcPyki9zu9zHfhwkBAL343EF6ZR0XwMlOQu9wu0hT9KBz4g55
6D41i0PrEaoBuDgEYiE2QRIKKwYBBAGXVQEFAQEHQMMr9gcVcJ3aiup/tpl8ZXxy
aJiJRGkPyNwGI5vxHMpZAwEIB4h4BBgWCgAgFiEERF0mUSTmByZx5k0HM/hop+Ne
gncFAmIhNkECGwwACgkQM/hop+NegndVhgD/SVGSKbF4G2W024VpW2tm3zCT+ue+
YMXQVq4SJt7UpWABAORudfJxsBqCRKtPlZMgGTJLjcOkyFJ9C2Fx7DeN0J4I
=nSOi
-----END PGP PUBLIC KEY BLOCK-----
```
## 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.
Credits to the reporter are granted and can be included in all public communication if desired.

View file

@ -4,32 +4,33 @@
//header('Strict-Transport-Security: max-age=31536000'); //header('Strict-Transport-Security: max-age=31536000');
// Uncomment to use gzip encoded output /**
* Uncomment to use gzip compressed output
*/
//define('USE_GZIP', 1); //define('USE_GZIP', 1);
// Uncomment to enable multiple domain installation. /**
* Uncomment to use brotli compressed output
*/
//define('USE_BROTLI', 1);
/**
* Uncomment to enable multiple domain installation.
*/
//define('MULTIDOMAIN', 1); //define('MULTIDOMAIN', 1);
// Uncomment to disable APCU. /**
* Uncomment to disable APCU.
*/
//define('APP_USE_APCU_CACHE', false); //define('APP_USE_APCU_CACHE', false);
/** /**
* Custom 'data' folder path * Custom 'data' folder path
* @return string
*/ */
function __get_custom_data_full_path() //define('APP_DATA_FOLDER_PATH', dirname(__DIR__) . '/snappymail-data/');
{ //define('APP_DATA_FOLDER_PATH', '/var/external-snappymail-data-folder/');
return '';
return dirname(__DIR__) . '/snappymail-data';
return '/var/external-snappymail-data-folder';
}
/** /**
* Additional configuration file name * Additional configuration file name
* @return string
*/ */
function __get_additional_configuration_name() //define('APP_CONFIGURATION_NAME', $_SERVER['HTTP_HOST'].'.ini');
{
return '';
return defined('APP_SITE') && 0 < strlen(APP_SITE) ? APP_SITE.'.ini' : '';
}

9
build/SnappyMail.asc Normal file
View file

@ -0,0 +1,9 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
mDMEYg0atBYJKwYBBAHaRw8BAQdA2S2tvGavChACjtBastsKRThD3rsBW1LUZLmN
Zbs4uaG0I1NuYXBweU1haWwgPHJlbGVhc2VzQHNuYXBweW1haWwuZXU+iJQEExYK
ADwWIQQQFuRweRRVQvi6EzVIIIuhMpDz6wUCYg0atAIbAwULCQgHAgMiAgEGFQoJ
CAsCBBYCAwECHgcCF4AACgkQSCCLoTKQ8+u9SAD/Q/IoAwjUkKDJBPq0RGwCFnl6
FG/VHB97CvBSpGOxtIsBAMCwMhWlsaBHAEqbzxiN+cdlMYwV23+SWLUJ/XMFgukE
=5ZwF
-----END PGP PUBLIC KEY BLOCK-----

57
build/arch/PKGBUILD Normal file
View file

@ -0,0 +1,57 @@
# Maintainer: George Rawlinson <george@rawlinson.net.nz>
pkgname=snappymail
pkgver=2.7.2
pkgrel=1
pkgdesc="modern PHP webmail client"
arch=('any')
license=('AGPL3')
url="https://github.com/the-djmaze/snappymail"
depends=('php-fpm')
makedepends=('php', 'nodejs' 'yarn' 'gulp', 'rollup')
optdepends=('mariadb: storage backend for contacts'
'php-pgsql: storage backend for contacts'
'php-sqlite: storage backend for contacts')
source=("$pkgname-$pkgver.tar.gz::$url/archive/v$pkgver.tar.gz"
"$pkgname.sysusers"
"$pkgname.tmpfiles")
b2sums=('a877041eaa74ca9824323b0ba80b3515205628825ed76fccff1511b2547c27419bae4733d24f8f36e6860c671cb609c4c5f31fa19529a92c628cb714570ff8ae'
'e020b2d4bc694ca056f5c15b148c69553ab610b5e1789f52543aa65e098f8097a41709b5b0fc22a6a01088a9d3f14d623b1b6e9ae2570acd4f380f429301c003'
'2536e11622895322cc752c6b651811b2122d3ae60099fe609609d7b45ba1ed00ea729c23f344405078698d161dbf9bcaffabf8eff14b740acdce3c681c513318')
prepare() {
sed -i "s/\$sCustomDataPath = '';/\$sCustomDataPath = '\/var\/lib\/$pkgname';/" "$pkgname-$pkgver/$pkgname/v/0.0.0/include.php"
# create folder for build output
mkdir -p build
}
build() {
cd "$pkgname-$pkgver"
yarn install
# build snappymail
php release.php --aur
bsdtar -x \
-C "$srcdir/build" \
-f "build/dist/releases/webmail/$pkgver/$pkgname-$pkgver.zip"
find . -type d -exec chmod 755 {} \;
find . -type f -exec chmod 644 {} \;
}
package() {
# directories
install -dm755 "$pkgdir/usr/share/snappymail" \
"$pkgdir/var/lib/snappymail"
# application files
cp -r "$srcdir/build/snappymail" "$pkgdir/usr/share/snappymail"
install -Dm644 -t "$pkgdir/usr/share/snappymail" "$srcdir/build/index.php"
# data files
cp -r "$srcdir/build/data" "$pkgdir/var/lib/snappymail"
# sysusers
install -Dm644 "$srcdir/$pkgname.sysusers" "$pkgdir/usr/lib/sysusers.d/$pkgname.conf"
install -Dm644 "$srcdir/$pkgname.tmpfiles" "$pkgdir/usr/lib/tmpfiles.d/$pkgname.conf"
}

View file

@ -0,0 +1 @@
u snappymail - "Snappymail User" /var/lib/snappymail

View file

@ -0,0 +1 @@
Z /var/lib/snappymail - snappymail snappymail

83
build/deb.php Executable file
View file

@ -0,0 +1,83 @@
<?php
echo "\x1b[33;1m === Debian === \x1b[0m\n";
// Debian Repository
define('DEB_SOURCE_DIR', __DIR__ . '/deb');
define('DEB_DEST_DIR', DEB_SOURCE_DIR . "/snappymail_{$package->version}-1_all");
is_dir(DEB_DEST_DIR) && passthru('rm -dfr '.escapeshellarg(DEB_DEST_DIR));
$dir = DEB_DEST_DIR . '/DEBIAN';
$data = file_get_contents(DEB_SOURCE_DIR . '/DEBIAN/control');
$data = str_replace('0.0.0', $package->version, $data);
mkdir($dir, 0755, true);
file_put_contents("{$dir}/control", $data);
copy(DEB_SOURCE_DIR . '/DEBIAN/postinst', $dir . '/postinst');
chmod($dir . '/postinst', 0755);
$dir = DEB_DEST_DIR . '/var/lib/snappymail';
mkdir($dir, 0755, true);
file_put_contents($dir . '/VERSION', $package->version);
copy('data/README.md', "{$dir}/README.md");
$dir = DEB_DEST_DIR . '/usr/share/doc/snappymail';
mkdir($dir, 0755, true);
copy('CODE_OF_CONDUCT.md', "{$dir}/CODE_OF_CONDUCT.md");
copy('CONTRIBUTING.md', "{$dir}/CONTRIBUTING.md");
copy('README.md', "{$dir}/README.md");
copy('CODE_OF_CONDUCT.md', "{$dir}/CODE_OF_CONDUCT.md");
//usr/share/doc/snappymail/README.Debian
//usr/share/doc/snappymail/changelog.Debian.gz
//usr/share/doc/snappymail/copyright
// Move files into package directory
$dir = DEB_DEST_DIR . '/usr/share/snappymail';
mkdir($dir, 0755, true);
passthru('cp -r "' . dirname(__DIR__) . '/snappymail" "' . $dir . '"');
rename("{$dir}/snappymail/v/0.0.0", "{$dir}/snappymail/v/{$package->version}");
$data = file_get_contents('index.php');
file_put_contents("{$dir}/index.php", str_replace('0.0.0', $package->version, $data));
$data = file_get_contents('_include.php');
file_put_contents("{$dir}/include.php", preg_replace('@(external-snappymail-data-folder/\'\);)@', "\$1\ndefine('APP_DATA_FOLDER_PATH', '/var/lib/snappymail/');", $data));
passthru('dpkg --build ' . escapeshellarg(DEB_DEST_DIR));
$TARGET_DIR = __DIR__ . "/dist/releases/webmail/{$package->version}/";
passthru('mv '
. escapeshellarg(DEB_DEST_DIR.'.deb') . ' '
. escapeshellarg($TARGET_DIR . basename(DEB_DEST_DIR.'.deb'))
);
passthru('rm -dfr '.escapeshellarg(DEB_DEST_DIR));
// https://github.com/the-djmaze/snappymail/issues/185#issuecomment-1059420588
$cwd = getcwd();
chdir($TARGET_DIR);
passthru('dpkg-scanpackages . /dev/null > '.escapeshellarg($TARGET_DIR . 'Packages'));
passthru('dpkg-scanpackages . /dev/null | gzip -9c > '.escapeshellarg($TARGET_DIR . 'Packages.gz'));
$size = filesize($TARGET_DIR . 'Packages');
$gz_size = filesize($TARGET_DIR . 'Packages.gz');
$Release = 'Origin: SnappyMail Repository
Label: SnappyMail
Suite: stable
Codename: stable
Version: 1.0
Architectures: all
Components: main
Description: SnappyMail repository
Date: ' . gmdate('r') . '
MD5Sum:
' . hash_file('md5', $TARGET_DIR . 'Packages') . ' ' . $size . ' Packages
' . hash_file('md5', $TARGET_DIR . 'Packages.gz') . ' ' . $gz_size . ' Packages.gz
SHA1:
' . hash_file('sha1', $TARGET_DIR . 'Packages') . ' ' . $size . ' Packages
' . hash_file('sha1', $TARGET_DIR . 'Packages.gz') . ' ' . $gz_size . ' Packages.gz
SHA256:
' . hash_file('sha256', $TARGET_DIR . 'Packages') . ' ' . $size . ' Packages
' . hash_file('sha256', $TARGET_DIR . 'Packages.gz') . ' ' . $gz_size . ' Packages.gz
';
file_put_contents($TARGET_DIR . 'Release', $Release);
chdir($cwd);

14
build/deb/DEBIAN/control Normal file
View file

@ -0,0 +1,14 @@
Package: snappymail
Version: 0.0.0
Maintainer: SnappyMail <debian@snappymail.eu>
Depends: nginx | apache2 | httpd, php-fpm | libapache2-mod-php, php-json, php-mbstring
Recommends: php-intl, php-sodium, php-uuid
Suggests: php-sqlite3 | php-mysql | php-pgsql, php-curl, php-exif, php-gnupg, php-gd | php-gmagick | php-imagick, php-openssl, php-zip
Architecture: all
Homepage: https://snappymail.eu
Vcs-Browser: https://github.com/the-djmaze/snappymail
Vcs-Git: https://github.com/the-djmaze/snappymail.git
Description: SnappyMail is a PHP-based simple, modern, lightweight & fast web-based email client with no database requirements.
It supports IMAP, SMTP and Sieve protocols, multiple accounts and identities, an admin panel for configuration.
Plugins can be installed to further extend functionality.
Emails are not stored locally, but are accessed through IMAP.

2
build/deb/DEBIAN/postinst Executable file
View file

@ -0,0 +1,2 @@
#!/bin/sh
chown -R www-data:www-data /var/lib/snappymail

77
build/plugins.php Executable file
View file

@ -0,0 +1,77 @@
<?php
define('ROOT_DIR', dirname(__DIR__));
define('PLUGINS_DEST_DIR', __DIR__ . '/dist/releases/plugins');
$destPath = __DIR__ . 'build/dist/releases/plugins/';
is_dir(PLUGINS_DEST_DIR) || mkdir(PLUGINS_DEST_DIR, 0777, true);
$manifest = [];
require ROOT_DIR . '/snappymail/v/0.0.0/app/libraries/RainLoop/Plugins/AbstractPlugin.php';
$keys = [
'author',
'category',
'description',
'file',
'id',
'license',
'name',
'release',
'required',
'type',
'url',
'version'
];
foreach (glob(ROOT_DIR . '/plugins/*', GLOB_NOSORT | GLOB_ONLYDIR) as $dir) {
if (is_file("{$dir}/index.php")) {
require "{$dir}/index.php";
$name = basename($dir);
$class = new ReflectionClass(str_replace('-', '', $name) . 'Plugin');
$manifest_item = [];
foreach ($class->getConstants() as $key => $value) {
$key = \strtolower($key);
if (in_array($key, $keys)) {
$manifest_item[$key] = $value;
}
}
$version = $manifest_item['version'];
if (0 < floatval($version)) {
echo "+ {$name} {$version}\n";
$manifest_item['type'] = 'plugin';
$manifest_item['id'] = $name;
$manifest_item['file'] = "{$dir}-{$version}.tgz";
ksort($manifest_item);
$manifest[$name] = $manifest_item;
$tar_destination = PLUGINS_DEST_DIR . "/{$name}-{$version}.tar";
$tgz_destination = PLUGINS_DEST_DIR . "/{$name}-{$version}.tgz";
@unlink($tgz_destination);
@unlink("{$tar_destination}.gz");
$tar = new PharData($tar_destination);
$tar->buildFromDirectory('./plugins/', '/' . \preg_quote("./plugins/{$name}", '/') . '/');
$tar->compress(Phar::GZ);
unlink($tar_destination);
rename("{$tar_destination}.gz", $tgz_destination);
if (Phar::canWrite()) {
$phar_destination = PLUGINS_DEST_DIR . "/{$name}.phar";
@unlink($phar_destination);
$tar = new Phar($phar_destination);
$tar->buildFromDirectory("./plugins/{$name}/");
$tar->compress(Phar::GZ);
unlink($phar_destination);
rename("{$phar_destination}.gz", $phar_destination);
}
} else {
echo "- {$name} {$version}\n";
}
} else {
echo "- {$name}\n";
}
}
ksort($manifest);
$manifest = json_encode(array_values($manifest));
$manifest = str_replace('{"', "\n\t{\n\t\t\"", $manifest);
$manifest = str_replace('"}', "\"\n\t}", $manifest);
$manifest = str_replace('}]', "}\n]", $manifest);
$manifest = str_replace('","', "\",\n\t\t\"", $manifest);
$manifest = str_replace('\/', '/', $manifest);
file_put_contents(PLUGINS_DEST_DIR . "/packages.json", $manifest);
exit;

View file

@ -1,9 +1,7 @@
import ko from 'ko'; import ko from 'ko';
import { import { Settings, SettingsGet } from 'Common/Globals';
elementById, import { changeTheme } from 'Common/Utils';
Settings
} from 'Common/Globals';
import { logoutLink } from 'Common/Links'; import { logoutLink } from 'Common/Links';
import { i18nToNodes, initOnStartOrLangChange } from 'Common/Translator'; import { i18nToNodes, initOnStartOrLangChange } from 'Common/Translator';
@ -25,11 +23,9 @@ export class AbstractApp {
this.Remote = Remote; this.Remote = Remote;
} }
logoutReload(close = false) { logoutReload() {
const url = logoutLink(); const url = logoutLink();
close && window.close && window.close();
if (location.href !== url) { if (location.href !== url) {
setTimeout(() => (Settings.app('inIframe') ? parent : window).location.href = url, 100); setTimeout(() => (Settings.app('inIframe') ? parent : window).location.href = url, 100);
} else { } else {
@ -37,22 +33,27 @@ export class AbstractApp {
} }
} }
refresh() {
// rl.adminArea() || !translatorReload(false, );
rl.adminArea() || (
LanguageStore.language(SettingsGet('Language'))
& ThemeStore.populate()
& changeTheme(SettingsGet('Theme'))
);
this.start();
}
bootstart() { bootstart() {
const register = (key, ClassObject, templateID) => ko.components.register(key, { const register = (key, ClassObject, templateID) => ko.components.register(key, {
template: { element: templateID || (key + 'Component') }, template: { element: templateID || (key + 'Component') },
viewModel: { viewModel: {
createViewModel: (params, componentInfo) => { createViewModel: (params, componentInfo) => {
params = params || {}; params = params || {};
i18nToNodes(componentInfo.element);
if (componentInfo && componentInfo.element) { if (params.inline) {
componentInfo.element.style.display = 'inline-block';
i18nToNodes(componentInfo.element);
if (params.inline) {
componentInfo.element.style.display = 'inline-block';
}
} }
return new ClassObject(params); return new ClassObject(params);
} }
} }
@ -68,14 +69,7 @@ export class AbstractApp {
LanguageStore.populate(); LanguageStore.populate();
ThemeStore.populate(); ThemeStore.populate();
}
/** this.start();
* @returns {void}
*/
hideLoading() {
elementById('rl-content').hidden = false;
elementById('rl-loading').remove();
} }
} }

View file

@ -1,4 +1,4 @@
import 'External/Admin/ko'; import 'External/ko';
import { Settings, SettingsGet } from 'Common/Globals'; import { Settings, SettingsGet } from 'Common/Globals';
@ -16,9 +16,8 @@ class AdminApp extends AbstractApp {
this.weakPassword = ko.observable(false); this.weakPassword = ko.observable(false);
} }
bootstart() { start() {
super.bootstart(); if (!Settings.app('adminAllowed')) {
if (!Settings.app('allowAdminPanel')) {
rl.route.root(); rl.route.root();
setTimeout(() => location.href = '/', 1); setTimeout(() => location.href = '/', 1);
} else if (SettingsGet('Auth')) { } else if (SettingsGet('Auth')) {
@ -27,7 +26,6 @@ class AdminApp extends AbstractApp {
} else { } else {
startScreens([LoginAdminScreen]); startScreens([LoginAdminScreen]);
} }
this.hideLoading();
} }
} }

File diff suppressed because it is too large Load diff

View file

@ -1,5 +1,5 @@
import * as Links from 'Common/Links'; import * as Links from 'Common/Links';
import { doc, SettingsGet } from 'Common/Globals'; import { doc, SettingsGet, fireEvent, addEventsListener } from 'Common/Globals';
let notificator = null, let notificator = null,
player = null, player = null,
@ -12,7 +12,7 @@ let notificator = null,
player.src = url; player.src = url;
player.play(); player.play();
name = name.trim(); name = name.trim();
dispatchEvent(new CustomEvent('audio.start', {detail:name.replace(/\.([a-z0-9]{3})$/, '') || 'audio'})); fireEvent('audio.start', name.replace(/\.([a-z0-9]{3})$/, '') || 'audio');
} }
}, },
@ -73,8 +73,7 @@ export const SMAudio = new class {
this.supportedOgg = canPlay('audio/ogg; codecs="vorbis"'); this.supportedOgg = canPlay('audio/ogg; codecs="vorbis"');
if (player) { if (player) {
const stopFn = () => this.pause(); const stopFn = () => this.pause();
player.addEventListener('ended', stopFn); addEventsListener(player, ['ended','error'], stopFn);
player.addEventListener('error', stopFn);
addEventListener('audio.api.stop', stopFn); addEventListener('audio.api.stop', stopFn);
} }
} }
@ -89,7 +88,7 @@ export const SMAudio = new class {
pause() { pause() {
player && player.pause(); player && player.pause();
dispatchEvent(new CustomEvent('audio.stop')); fireEvent('audio.stop');
} }
playMp3(url, name) { playMp3(url, name) {

View file

@ -1,34 +1,31 @@
import { MessageSetAction } from 'Common/EnumsUser'; import { MessageSetAction } from 'Common/EnumsUser';
import { arrayLength, pInt } from 'Common/Utils'; import { isArray } from 'Common/Utils';
let FOLDERS_CACHE = {}, let FOLDERS_CACHE = {},
FOLDERS_NAME_CACHE = {}, FOLDERS_NAME_CACHE = {},
FOLDERS_HASH_CACHE = {},
FOLDERS_UID_NEXT_CACHE = {},
MESSAGE_FLAGS_CACHE = {}, MESSAGE_FLAGS_CACHE = {},
NEW_MESSAGE_CACHE = {}, NEW_MESSAGE_CACHE = {},
REQUESTED_MESSAGE_CACHE = {},
inboxFolderName = 'INBOX'; inboxFolderName = 'INBOX';
const REQUESTED_MESSAGE_CACHE = {};
export const export const
/** /**
* @returns {void} * @returns {void}
*/ */
clear = () => { clearCache = () => {
FOLDERS_CACHE = {}; FOLDERS_CACHE = {};
FOLDERS_NAME_CACHE = {}; FOLDERS_NAME_CACHE = {};
FOLDERS_HASH_CACHE = {};
FOLDERS_UID_NEXT_CACHE = {};
MESSAGE_FLAGS_CACHE = {}; MESSAGE_FLAGS_CACHE = {};
NEW_MESSAGE_CACHE = {};
REQUESTED_MESSAGE_CACHE = {};
}, },
/** /**
* @param {string} folderFullNameRaw * @param {string} folderFullName
* @param {string} uid * @param {string} uid
* @returns {string} * @returns {string}
*/ */
getMessageKey = (folderFullNameRaw, uid) => `${folderFullNameRaw}#${uid}`, getMessageKey = (folderFullName, uid) => `${folderFullName}#${uid}`,
/** /**
* @param {string} folder * @param {string} folder
@ -44,18 +41,18 @@ export const
hasRequestedMessage = (folder, uid) => true === REQUESTED_MESSAGE_CACHE[getMessageKey(folder, uid)], hasRequestedMessage = (folder, uid) => true === REQUESTED_MESSAGE_CACHE[getMessageKey(folder, uid)],
/** /**
* @param {string} folderFullNameRaw * @param {string} folderFullName
* @param {string} uid * @param {string} uid
*/ */
addNewMessageCache = (folderFullNameRaw, uid) => NEW_MESSAGE_CACHE[getMessageKey(folderFullNameRaw, uid)] = true, addNewMessageCache = (folderFullName, uid) => NEW_MESSAGE_CACHE[getMessageKey(folderFullName, uid)] = true,
/** /**
* @param {string} folderFullNameRaw * @param {string} folderFullName
* @param {string} uid * @param {string} uid
*/ */
hasNewMessageAndRemoveFromCache = (folderFullNameRaw, uid) => { hasNewMessageAndRemoveFromCache = (folderFullName, uid) => {
if (NEW_MESSAGE_CACHE[getMessageKey(folderFullNameRaw, uid)]) { if (NEW_MESSAGE_CACHE[getMessageKey(folderFullName, uid)]) {
NEW_MESSAGE_CACHE[getMessageKey(folderFullNameRaw, uid)] = null; NEW_MESSAGE_CACHE[getMessageKey(folderFullName, uid)] = null;
return true; return true;
} }
return false; return false;
@ -80,72 +77,81 @@ export const
* @param {string} folderHash * @param {string} folderHash
* @returns {string} * @returns {string}
*/ */
getFolderFullNameRaw = folderHash => getFolderFullName = folderHash =>
folderHash && FOLDERS_NAME_CACHE[folderHash] ? FOLDERS_NAME_CACHE[folderHash] : '', folderHash && FOLDERS_NAME_CACHE[folderHash] ? FOLDERS_NAME_CACHE[folderHash] : '',
/** /**
* @param {string} folderHash * @param {string} folderHash
* @param {string} folderFullNameRaw * @param {string} folderFullName
* @param {?FolderModel} folder * @param {?FolderModel} folder
*/ */
setFolder = (folderHash, folderFullNameRaw, folder) => { setFolder = folder => {
FOLDERS_CACHE[folderFullNameRaw] = folder; folder.hash = '';
FOLDERS_NAME_CACHE[folderHash] = folderFullNameRaw; FOLDERS_CACHE[folder.fullName] = folder;
FOLDERS_NAME_CACHE[folder.fullNameHash] = folder.fullName;
}, },
/** /**
* @param {string} folderFullNameRaw * @param {string} folderFullName
* @returns {string} * @returns {string}
*/ */
getFolderHash = folderFullNameRaw => getFolderHash = folderFullName =>
folderFullNameRaw && FOLDERS_HASH_CACHE[folderFullNameRaw] ? FOLDERS_HASH_CACHE[folderFullNameRaw] : '', FOLDERS_CACHE[folderFullName] ? FOLDERS_CACHE[folderFullName].hash : '',
/** /**
* @param {string} folderFullNameRaw * @param {string} folderFullName
* @param {string} folderHash * @param {string} folderHash
*/ */
setFolderHash = (folderFullNameRaw, folderHash) => setFolderHash = (folderFullName, folderHash) =>
folderFullNameRaw && (FOLDERS_HASH_CACHE[folderFullNameRaw] = folderHash), FOLDERS_CACHE[folderFullName] && (FOLDERS_CACHE[folderFullName].hash = folderHash),
/** /**
* @param {string} folderFullNameRaw * @param {string} folderFullName
* @returns {string} * @returns {string}
*/ */
getFolderUidNext = folderFullNameRaw => getFolderUidNext = folderFullName =>
folderFullNameRaw && FOLDERS_UID_NEXT_CACHE[folderFullNameRaw] FOLDERS_CACHE[folderFullName] ? FOLDERS_CACHE[folderFullName].uidNext : 0,
? FOLDERS_UID_NEXT_CACHE[folderFullNameRaw]
: '',
/** /**
* @param {string} folderFullNameRaw * @param {string} folderFullName
* @param {string} uidNext * @param {string} uidNext
*/ */
setFolderUidNext = (folderFullNameRaw, uidNext) => setFolderUidNext = (folderFullName, uidNext) =>
FOLDERS_UID_NEXT_CACHE[folderFullNameRaw] = uidNext, FOLDERS_CACHE[folderFullName] && (FOLDERS_CACHE[folderFullName].uidNext = uidNext),
/** /**
* @param {string} folderFullNameRaw * @param {string} folderFullName
* @returns {?FolderModel} * @returns {?FolderModel}
*/ */
getFolderFromCacheList = folderFullNameRaw => getFolderFromCacheList = folderFullName =>
folderFullNameRaw && FOLDERS_CACHE[folderFullNameRaw] ? FOLDERS_CACHE[folderFullNameRaw] : null, FOLDERS_CACHE[folderFullName] ? FOLDERS_CACHE[folderFullName] : null,
/** /**
* @param {string} folderFullNameRaw * @param {string} folderFullName
*/ */
removeFolderFromCacheList = folderFullNameRaw => delete FOLDERS_CACHE[folderFullNameRaw]; removeFolderFromCacheList = folderFullName => delete FOLDERS_CACHE[folderFullName];
export class MessageFlagsCache export class MessageFlagsCache
{ {
/**
* @param {string} folderFullName
* @param {string} uid
* @param {string} flag
* @returns {bool}
*/
static hasFlag(folderFullName, uid, flag) {
return MESSAGE_FLAGS_CACHE[folderFullName]
&& MESSAGE_FLAGS_CACHE[folderFullName][uid]
&& MESSAGE_FLAGS_CACHE[folderFullName][uid].includes(flag);
}
/** /**
* @param {string} folderFullName * @param {string} folderFullName
* @param {string} uid * @param {string} uid
* @returns {?Array} * @returns {?Array}
*/ */
static getFor(folderFullName, uid) { static getFor(folderFullName, uid) {
return MESSAGE_FLAGS_CACHE[folderFullName] && MESSAGE_FLAGS_CACHE[folderFullName][uid] return MESSAGE_FLAGS_CACHE[folderFullName] && MESSAGE_FLAGS_CACHE[folderFullName][uid];
? MESSAGE_FLAGS_CACHE[folderFullName][uid]
: null;
} }
/** /**
@ -153,11 +159,13 @@ export class MessageFlagsCache
* @param {string} uid * @param {string} uid
* @param {Array} flagsCache * @param {Array} flagsCache
*/ */
static setFor(folderFullName, uid, flagsCache) { static setFor(folderFullName, uid, flags) {
if (!MESSAGE_FLAGS_CACHE[folderFullName]) { if (isArray(flags)) {
MESSAGE_FLAGS_CACHE[folderFullName] = {}; if (!MESSAGE_FLAGS_CACHE[folderFullName]) {
MESSAGE_FLAGS_CACHE[folderFullName] = {};
}
MESSAGE_FLAGS_CACHE[folderFullName][uid] = flags;
} }
MESSAGE_FLAGS_CACHE[folderFullName][uid] = flagsCache;
} }
/** /**
@ -173,39 +181,24 @@ export class MessageFlagsCache
static initMessage(message) { static initMessage(message) {
if (message) { if (message) {
const uid = message.uid, const uid = message.uid,
flags = this.getFor(message.folder, uid); flags = this.getFor(message.folder, uid),
thread = message.threads;
if (flags && flags.length) { if (isArray(flags)) {
message.isFlagged(!!flags[1]); message.flags(flags);
if (!message.isSimpleMessage) {
message.isUnseen(!!flags[0]);
message.isAnswered(!!flags[2]);
message.isForwarded(!!flags[3]);
message.isReadReceipt(!!flags[4]);
message.isDeleted(!!flags[5]);
}
} }
if (message.threads.length) { if (thread.length) {
const unseenSubUid = message.threads.find(sSubUid => { const unseenSubUid = thread.find(iSubUid =>
if (uid !== sSubUid) { (uid !== iSubUid) && !this.hasFlag(message.folder, iSubUid, '\\seen')
const subFlags = this.getFor(message.folder, sSubUid); );
return subFlags && subFlags.length && !!subFlags[0];
}
return false;
});
const flaggedSubUid = message.threads.find(sSubUid => { const flaggedSubUid = thread.find(iSubUid =>
if (uid !== sSubUid) { (uid !== iSubUid) && this.hasFlag(message.folder, iSubUid, '\\flagged')
const subFlags = this.getFor(message.folder, sSubUid); );
return subFlags && subFlags.length && !!subFlags[1];
}
return false;
});
message.hasUnseenSubMessage(unseenSubUid && 0 < pInt(unseenSubUid)); message.hasUnseenSubMessage(!!unseenSubUid);
message.hasFlaggedSubMessage(flaggedSubUid && 0 < pInt(flaggedSubUid)); message.hasFlaggedSubMessage(!!flaggedSubUid);
} }
} }
} }
@ -215,25 +208,7 @@ export class MessageFlagsCache
*/ */
static store(message) { static store(message) {
if (message) { if (message) {
this.setFor(message.folder, message.uid, [ this.setFor(message.folder, message.uid, message.flags());
message.isUnseen(),
message.isFlagged(),
message.isAnswered(),
message.isForwarded(),
message.isReadReceipt(),
message.isDeleted()
]);
}
}
/**
* @param {string} folder
* @param {string} uid
* @param {Array} flags
*/
static storeByFolderAndUid(folder, uid, flags) {
if (arrayLength(flags)) {
this.setFor(folder, uid, flags);
} }
} }
@ -243,33 +218,30 @@ export class MessageFlagsCache
* @param {number} setAction * @param {number} setAction
*/ */
static storeBySetAction(folder, uid, setAction) { static storeBySetAction(folder, uid, setAction) {
let unread = 0; let flags = this.getFor(folder, uid) || [];
const flags = this.getFor(folder, uid); const
unread = flags.includes('\\seen') ? 0 : 1,
add = item => flags.includes(item) || flags.push(item),
remove = item => flags = flags.filter(flag => flag != item);
if (arrayLength(flags)) { switch (setAction) {
if (flags[0]) { case MessageSetAction.SetSeen:
unread = 1; add('\\seen');
} break;
case MessageSetAction.UnsetSeen:
switch (setAction) { remove('\\seen');
case MessageSetAction.SetSeen: break;
flags[0] = false; case MessageSetAction.SetFlag:
break; add('\\flagged');
case MessageSetAction.UnsetSeen: break;
flags[0] = true; case MessageSetAction.UnsetFlag:
break; remove('\\flagged');
case MessageSetAction.SetFlag: break;
flags[1] = true; // no default
break;
case MessageSetAction.UnsetFlag:
flags[1] = false;
break;
// no default
}
this.setFor(folder, uid, flags);
} }
this.setFor(folder, uid, flags);
return unread; return unread;
} }

View file

@ -1,85 +1,53 @@
/* eslint quote-props: 0 */ /* eslint quote-props: 0 */
/** export const
* @enum {string}
*/
export const Capa = {
OpenPGP: 'OPEN_PGP',
Prefetch: 'PREFETCH',
Composer: 'COMPOSER',
Contacts: 'CONTACTS',
Reload: 'RELOAD',
Search: 'SEARCH',
SearchAdv: 'SEARCH_ADV',
MessageActions: 'MESSAGE_ACTIONS',
MessageListActions: 'MESSAGELIST_ACTIONS',
AttachmentsActions: 'ATTACHMENTS_ACTIONS',
DangerousActions: 'DANGEROUS_ACTIONS',
Settings: 'SETTINGS',
Help: 'HELP',
Themes: 'THEMES',
UserBackground: 'USER_BACKGROUND',
Sieve: 'SIEVE',
AttachmentThumbnails: 'ATTACHMENT_THUMBNAILS',
Templates: 'TEMPLATES',
AutoLogout: 'AUTOLOGOUT',
AdditionalAccounts: 'ADDITIONAL_ACCOUNTS',
Identities: 'IDENTITIES'
};
/** /**
* @enum {string} * @enum {string}
*/ */
export const Scope = { Scope = {
All: 'all',
None: 'none',
Contacts: 'Contacts',
MessageList: 'MessageList', MessageList: 'MessageList',
FolderList: 'FolderList', FolderList: 'FolderList',
MessageView: 'MessageView', MessageView: 'MessageView',
Compose: 'Compose', Settings: 'Settings'
Settings: 'Settings', },
Menu: 'Menu',
ComposeOpenPgp: 'ComposeOpenPgp',
MessageOpenPgp: 'MessageOpenPgp',
ViewOpenPgpKey: 'ViewOpenPgpKey',
KeyboardShortcutsHelp: 'KeyboardShortcutsHelp',
Ask: 'Ask'
};
/** /**
* @enum {number} * @enum {number}
*/ */
export const UploadErrorCode = { UploadErrorCode = {
Normal: 0, Normal: 0,
FileIsTooBig: 1, FileIsTooBig: 1,
FilePartiallyUploaded: 2, FilePartiallyUploaded: 3,
NoFileUploaded: 3, NoFileUploaded: 4,
MissingTempFolder: 4, MissingTempFolder: 6,
OnSavingFile: 5, OnSavingFile: 7,
FileType: 98, FileType: 98,
Unknown: 99 Unknown: 99
}; },
/** /**
* @enum {number} * @enum {number}
*/ */
export const SaveSettingsStep = { SaveSettingsStep = {
Animate: -2, Animate: -2,
Idle: -1, Idle: -1,
TrueResult: 1, TrueResult: 1,
FalseResult: 0 FalseResult: 0
}; },
/** /**
* @enum {number} * @enum {number}
*/ */
export const Notification = { Notification = {
RequestError: 1, RequestError: 1,
RequestAborted: 2, RequestAborted: 2,
// Global
InvalidToken: 101, InvalidToken: 101,
AuthError: 102, AuthError: 102,
// User
ConnectionError: 104, ConnectionError: 104,
DomainNotAllowed: 109, DomainNotAllowed: 109,
AccountNotAllowed: 110, AccountNotAllowed: 110,
@ -110,20 +78,15 @@ export const Notification = {
CantDeleteNonEmptyFolder: 405, CantDeleteNonEmptyFolder: 405,
// CantSaveSettings: 501, // CantSaveSettings: 501,
CantSavePluginSettings: 502,
DomainAlreadyExists: 601, DomainAlreadyExists: 601,
CantInstallPackage: 701,
CantDeletePackage: 702,
InvalidPluginPackage: 703,
UnsupportedPluginPackage: 704,
DemoSendMessageError: 750, DemoSendMessageError: 750,
DemoAccountError: 751, DemoAccountError: 751,
AccountAlreadyExists: 801, AccountAlreadyExists: 801,
AccountDoesNotExist: 802, AccountDoesNotExist: 802,
AccountSwitchFailed: 803,
MailServerError: 901, MailServerError: 901,
ClientViewError: 902, ClientViewError: 902,
@ -134,5 +97,12 @@ export const Notification = {
// JsonTimeout: 953, // JsonTimeout: 953,
UnknownNotification: 998, UnknownNotification: 998,
UnknownError: 999 UnknownError: 999,
// Admin
CantInstallPackage: 701,
CantDeletePackage: 702,
InvalidPluginPackage: 703,
UnsupportedPluginPackage: 704,
CantSavePluginSettings: 705
}; };

View file

@ -14,6 +14,20 @@ export const FolderType = {
NotSpam: 80 NotSpam: 80
}; };
/**
* @enum {string}
*/
export const FolderMetadataKeys = {
// RFC 5464
Comment: '/private/comment',
CommentShared: '/shared/comment',
// RFC 6154
SpecialUse: '/private/specialuse',
// Kolab
KolabFolderType: '/private/vendor/kolab/folder-type',
KolabFolderTypeShared: '/shared/vendor/kolab/folder-type'
};
/** /**
* @enum {string} * @enum {string}
*/ */
@ -34,13 +48,13 @@ export const FolderSortMode = {
* @enum {string} * @enum {string}
*/ */
export const ComposeType = { export const ComposeType = {
Empty: 'empty', Empty: 0,
Reply: 'reply', Reply: 1,
ReplyAll: 'replyall', ReplyAll: 2,
Forward: 'forward', Forward: 3,
ForwardAsAttachment: 'forward-as-attachment', ForwardAsAttachment: 4,
Draft: 'draft', Draft: 5,
EditAsNew: 'editasnew' EditAsNew: 6
}; };
/** /**
@ -58,19 +72,14 @@ export const SetSystemFoldersNotification = {
/** /**
* @enum {number} * @enum {number}
*/ */
export const ClientSideKeyName = { export const
FoldersLashHash: 0, ClientSideKeyNameExpandedFolders = 3,
MessagesInboxLastHash: 1, ClientSideKeyNameFolderListSize = 4,
MailBoxListSize: 2, ClientSideKeyNameMessageListSize = 5,
ExpandedFolders: 3, ClientSideKeyNameLastReplyAction = 6,
FolderListSize: 4, ClientSideKeyNameLastSignMe = 7,
MessageListSize: 5, ClientSideKeyNameMessageHeaderFullInfo = 9,
LastReplyAction: 6, ClientSideKeyNameMessageAttachmentControls = 10;
LastSignMe: 7,
ComposeLastIdentityID: 8,
MessageHeaderFullInfo: 9,
MessageAttachmentControls: 10
};
/** /**
* @enum {number} * @enum {number}

View file

@ -9,6 +9,7 @@ const
msOffice = app+'vnd.openxmlformats-officedocument.', msOffice = app+'vnd.openxmlformats-officedocument.',
openDoc = app+'vnd.oasis.opendocument.', openDoc = app+'vnd.oasis.opendocument.',
sizes = ['B', 'KiB', 'MiB', 'GiB', 'TiB'], sizes = ['B', 'KiB', 'MiB', 'GiB', 'TiB'],
lowerCase = text => text.toLowerCase().trim(),
exts = { exts = {
eml: 'message/rfc822', eml: 'message/rfc822',
@ -123,13 +124,13 @@ export const FileInfo = {
* @returns {string} * @returns {string}
*/ */
getExtension: fileName => { getExtension: fileName => {
fileName = fileName.toLowerCase().trim(); fileName = lowerCase(fileName);
const result = fileName.split('.').pop(); const result = fileName.split('.').pop();
return result === fileName ? '' : result; return result === fileName ? '' : result;
}, },
getContentType: fileName => { getContentType: fileName => {
fileName = fileName.toLowerCase().trim(); fileName = lowerCase(fileName);
if ('winmail.dat' === fileName) { if ('winmail.dat' === fileName) {
return app + 'ms-tnef'; return app + 'ms-tnef';
} }
@ -158,8 +159,8 @@ export const FileInfo = {
* @returns {string} * @returns {string}
*/ */
getType: (ext, mimeType) => { getType: (ext, mimeType) => {
ext = ext.toLowerCase().trim(); ext = lowerCase(ext);
mimeType = mimeType.toLowerCase().trim().replace('csv/plain', 'text/csv'); mimeType = lowerCase(mimeType).replace('csv/plain', 'text/csv');
let key = ext + mimeType; let key = ext + mimeType;
if (cache[key]) { if (cache[key]) {
@ -249,10 +250,10 @@ export const FileInfo = {
* @param {string} sFileType * @param {string} sFileType
* @returns {string} * @returns {string}
*/ */
getCombinedIconClass: data => { getAttachmentsIconClass: data => {
if (arrayLength(data)) { if (arrayLength(data)) {
let icons = data let icons = data
.map(item => item ? FileInfo.getIconClass(FileInfo.getExtension(item[0]), item[1]) : '') .map(item => item ? FileInfo.getIconClass(FileInfo.getExtension(item.fileName), item.mimeType) : '')
.validUnique(); .validUnique();
return (icons && 1 === icons.length && 'icon-file' !== icons[0]) return (icons && 1 === icons.length && 'icon-file' !== icons[0])

259
dev/Common/Folders.js Normal file
View file

@ -0,0 +1,259 @@
import { isArray, arrayLength } from 'Common/Utils';
import {
MessageFlagsCache,
setFolderHash,
getFolderHash,
getFolderInboxName,
getFolderFromCacheList,
getFolderUidNext
} from 'Common/Cache';
import { SettingsUserStore } from 'Stores/User/Settings';
import { FolderUserStore } from 'Stores/User/Folder';
import { MessagelistUserStore } from 'Stores/User/Messagelist';
import { getNotification } from 'Common/Translator';
import Remote from 'Remote/User/Fetch';
export const
sortFolders = folders => {
try {
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))
);
} catch (e) {
console.error(e);
}
},
/**
* @param {?Function} fCallback
* @param {string} folder
* @param {Array=} list = []
*/
fetchFolderInformation = (fCallback, folder, list = []) => {
let fetch = !arrayLength(list);
const uids = [];
if (!fetch) {
list.forEach(messageListItem => {
if (!MessageFlagsCache.getFor(messageListItem.folder, messageListItem.uid)) {
uids.push(messageListItem.uid);
}
if (messageListItem.threads.length) {
messageListItem.threads.forEach(uid => {
if (!MessageFlagsCache.getFor(messageListItem.folder, uid)) {
uids.push(uid);
}
});
}
});
fetch = uids.length;
}
if (fetch) {
Remote.request('FolderInformation', fCallback, {
Folder: folder,
FlagsUids: uids,
UidNext: getFolderUidNext(folder) // Used to check for new messages
});
} else if (SettingsUserStore.useThreads()) {
MessagelistUserStore.reloadFlagsAndCachedMessage();
}
},
/**
* @param {Array=} aDisabled
* @param {Array=} aHeaderLines
* @param {Function=} fRenameCallback
* @param {Function=} fDisableCallback
* @param {boolean=} bNoSelectSelectable Used in FolderCreatePopupView
* @returns {Array}
*/
folderListOptionsBuilder = (
aDisabled,
aHeaderLines,
fRenameCallback,
fDisableCallback,
bNoSelectSelectable,
aList = FolderUserStore.folderList()
) => {
const
aResult = [],
sDeepPrefix = '\u00A0\u00A0\u00A0',
// FolderSystemPopupView should always be true
showUnsubscribed = fRenameCallback ? !SettingsUserStore.hideUnsubscribed() : true,
foldersWalk = folders => {
folders.forEach(oItem => {
if (showUnsubscribed || oItem.hasSubscriptions() || !oItem.exists) {
aResult.push({
id: oItem.fullName,
name:
sDeepPrefix.repeat(oItem.deep) +
fRenameCallback(oItem),
system: false,
disabled: !bNoSelectSelectable && (
!oItem.selectable() ||
aDisabled.includes(oItem.fullName) ||
fDisableCallback(oItem))
});
}
if (oItem.subFolders.length) {
foldersWalk(oItem.subFolders());
}
});
};
fDisableCallback = fDisableCallback || (() => false);
fRenameCallback = fRenameCallback || (oItem => oItem.name());
isArray(aDisabled) || (aDisabled = []);
isArray(aHeaderLines) && aHeaderLines.forEach(line =>
aResult.push({
id: line[0],
name: line[1],
system: false,
disabled: false
})
);
foldersWalk(aList);
return aResult;
},
// Every 5 minutes
refreshFoldersInterval = 300000,
/**
* @param {boolean=} boot = false
*/
folderInformationMultiply = (boot = false) => {
const folders = FolderUserStore.getNextFolderNames(refreshFoldersInterval);
if (arrayLength(folders)) {
Remote.request('FolderInformationMultiply', (iError, oData) => {
if (!iError && arrayLength(oData.Result)) {
const utc = Date.now();
oData.Result.forEach(item => {
const hash = getFolderHash(item.Folder),
folder = getFolderFromCacheList(item.Folder);
if (folder) {
folder.expires = utc;
setFolderHash(item.Folder, item.Hash);
folder.messageCountAll(item.MessageCount);
let unreadCountChange = folder.messageCountUnread() !== item.MessageUnseenCount;
folder.messageCountUnread(item.MessageUnseenCount);
if (unreadCountChange) {
MessageFlagsCache.clearFolder(folder.fullName);
}
if (!hash || item.Hash !== hash) {
if (folder.fullName === FolderUserStore.currentFolderFullName()) {
MessagelistUserStore.reload();
}
} else if (unreadCountChange
&& folder.fullName === FolderUserStore.currentFolderFullName()
&& MessagelistUserStore.length) {
rl.app.folderInformation(folder.fullName, MessagelistUserStore());
}
}
});
if (boot) {
setTimeout(() => folderInformationMultiply(true), 2000);
}
}
}, {
Folders: folders
});
}
},
moveOrDeleteResponseHelper = (iError, oData) => {
if (iError) {
setFolderHash(FolderUserStore.currentFolderFullName(), '');
alert(getNotification(iError));
} else if (FolderUserStore.currentFolder()) {
if (2 === arrayLength(oData.Result)) {
setFolderHash(oData.Result[0], oData.Result[1]);
} else {
setFolderHash(FolderUserStore.currentFolderFullName(), '');
}
MessagelistUserStore.reload(!MessagelistUserStore.length);
}
},
messagesMoveHelper = (fromFolderFullName, toFolderFullName, uidsForMove) => {
const
sSpamFolder = FolderUserStore.spamFolder(),
isSpam = sSpamFolder === toFolderFullName,
isHam = !isSpam && sSpamFolder === fromFolderFullName && getFolderInboxName() === toFolderFullName;
Remote.request('MessageMove',
moveOrDeleteResponseHelper,
{
FromFolder: fromFolderFullName,
ToFolder: toFolderFullName,
Uids: uidsForMove.join(','),
MarkAsRead: (isSpam || FolderUserStore.trashFolder() === toFolderFullName) ? 1 : 0,
Learning: isSpam ? 'SPAM' : isHam ? 'HAM' : ''
},
null,
'',
['MessageList']
);
},
messagesDeleteHelper = (sFromFolderFullName, aUidForRemove) => {
Remote.request('MessageDelete',
moveOrDeleteResponseHelper,
{
Folder: sFromFolderFullName,
Uids: aUidForRemove.join(',')
},
null,
'',
['MessageList']
);
},
/**
* @param {string} sFromFolderFullName
* @param {Array} aUidForMove
* @param {string} sToFolderFullName
* @param {boolean=} bCopy = false
*/
moveMessagesToFolder = (sFromFolderFullName, aUidForMove, sToFolderFullName, bCopy) => {
if (sFromFolderFullName !== sToFolderFullName && arrayLength(aUidForMove)) {
const oFromFolder = getFolderFromCacheList(sFromFolderFullName),
oToFolder = getFolderFromCacheList(sToFolderFullName);
if (oFromFolder && oToFolder) {
if (bCopy) {
Remote.request('MessageCopy', null, {
FromFolder: oFromFolder.fullName,
ToFolder: oToFolder.fullName,
Uids: aUidForMove.join(',')
});
} else {
messagesMoveHelper(oFromFolder.fullName, oToFolder.fullName, aUidForMove);
}
MessagelistUserStore.removeMessagesFromList(oFromFolder.fullName, aUidForMove, oToFolder.fullName, bCopy);
return true;
}
}
return false;
};

View file

@ -1,9 +1,9 @@
import ko from 'ko'; import ko from 'ko';
import { Scope } from 'Common/Enums';
let keyScopeFake = Scope.All; let keyScopeFake = 'all';
export const export const
ScopeMenu = 'Menu',
doc = document, doc = document,
@ -11,12 +11,14 @@ export const
elementById = id => doc.getElementById(id), elementById = id => doc.getElementById(id),
exitFullscreen = () => getFullscreenElement() && (doc.exitFullscreen || doc.webkitExitFullscreen)(), exitFullscreen = () => getFullscreenElement() && (doc.exitFullscreen || doc.webkitExitFullscreen).call(doc),
getFullscreenElement = () => doc.fullscreenElement || doc.webkitFullscreenElement, getFullscreenElement = () => doc.fullscreenElement || doc.webkitFullscreenElement,
Settings = rl.settings, Settings = rl.settings,
SettingsGet = Settings.get, SettingsGet = Settings.get,
SettingsCapa = Settings.capa,
dropdowns = [],
dropdownVisibility = ko.observable(false).extend({ rateLimit: 0 }), dropdownVisibility = ko.observable(false).extend({ rateLimit: 0 }),
moveAction = ko.observable(false), moveAction = ko.observable(false),
@ -28,31 +30,46 @@ export const
return el; return el;
}, },
// keys fireEvent = (name, detail) => dispatchEvent(new CustomEvent(name, {detail:detail})),
keyScopeReal = ko.observable(Scope.All),
formFieldFocused = () => doc.activeElement && doc.activeElement.matches('input,textarea'),
addShortcut = (...args) => shortcuts.add(...args),
registerShortcut = (keys, modifiers, scopes, method) =>
addShortcut(keys, modifiers, scopes, event => formFieldFocused() ? true : method(event)),
addEventsListener = (element, events, fn, options) =>
events.forEach(event => element.addEventListener(event, fn, options)),
addEventsListeners = (element, events) =>
Object.entries(events).forEach(([event, fn]) => element.addEventListener(event, fn)),
// keys / shortcuts
keyScopeReal = ko.observable('all'),
keyScope = value => { keyScope = value => {
if (value) { if (!value) {
if (Scope.Menu !== value) {
keyScopeFake = value;
if (dropdownVisibility()) {
value = Scope.Menu;
}
}
keyScopeReal(value);
shortcuts.setScope(value);
} else {
return keyScopeFake; return keyScopeFake;
} }
if (ScopeMenu !== value) {
keyScopeFake = value;
if (dropdownVisibility()) {
value = ScopeMenu;
}
}
keyScopeReal(value);
shortcuts.setScope(value);
}; };
dropdownVisibility.subscribe(value => { dropdownVisibility.subscribe(value => {
if (value) { if (value) {
keyScope(Scope.Menu); keyScope(ScopeMenu);
} else if (Scope.Menu === shortcuts.getScope()) { } else if (ScopeMenu === shortcuts.getScope()) {
keyScope(keyScopeFake); keyScope(keyScopeFake);
} }
}); });
leftPanelDisabled.toggle = () => leftPanelDisabled(!leftPanelDisabled());
leftPanelDisabled.subscribe(value => { leftPanelDisabled.subscribe(value => {
value && moveAction() && moveAction(false); value && moveAction() && moveAction(false);
$htmlCL.toggle('rl-left-panel-disabled', value); $htmlCL.toggle('rl-left-panel-disabled', value);

View file

@ -1,4 +1,9 @@
import { createElement, SettingsGet } from 'Common/Globals';
import { forEachObjectEntry, pInt } from 'Common/Utils';
import { proxy } from 'Common/Links';
const const
tpl = createElement('template'),
htmlre = /[&<>"']/g, htmlre = /[&<>"']/g,
htmlmap = { htmlmap = {
'&': '&amp;', '&': '&amp;',
@ -6,15 +11,487 @@ const
'>': '&gt;', '>': '&gt;',
'"': '&quot;', '"': '&quot;',
"'": '&#x27;' "'": '&#x27;'
}; },
/** replaceWithChildren = node => node.replaceWith(...[...node.childNodes]),
* @param {string} text
* @returns {string} // Strip utm_* tracking
*/ stripTracking = text => text.replace(/([?&])utm_[a-z]+=[^&?#]*/gsi, '$1').replace(/&&+/, '');
export function encodeHtml(text) {
return (text && text.toString ? text.toString() : ''+text).replace(htmlre, m => htmlmap[m]); export const
}
/**
* @param {string} text
* @returns {string}
*/
encodeHtml = text => (text && text.toString ? text.toString() : '' + text).replace(htmlre, m => htmlmap[m]),
/**
* Clears the Message Html for viewing
* @param {string} text
* @returns {string}
*/
cleanHtml = (html, contentLocationUrls, removeColors) => {
const
debug = false, // Config()->Get('debug', 'enable', false);
useProxy = !!SettingsGet('UseLocalProxyForExternalImages'),
detectHiddenImages = true, // !!SettingsGet('try_to_detect_hidden_images'),
result = {
hasExternals: false,
foundCIDs: [],
foundContentLocationUrls: []
},
// convert body attributes to CSS
tasks = {
link: value => {
if (/^#[a-fA-Z0-9]{3,6}$/.test(value)) {
tpl.content.querySelectorAll('a').forEach(node => node.style.color = value)
}
},
text: (value, node) => node.style.color = value,
topmargin: (value, node) => node.style.marginTop = pInt(value) + 'px',
leftmargin: (value, node) => node.style.marginLeft = pInt(value) + 'px',
bottommargin: (value, node) => node.style.marginBottom = pInt(value) + 'px',
rightmargin: (value, node) => node.style.marginRight = pInt(value) + 'px'
},
allowedAttributes = [
// defaults
'name',
'dir', 'lang', 'style', 'title',
'background', 'bgcolor', 'alt', 'height', 'width', 'src', 'href',
'border', 'bordercolor', 'charset', 'direction',
// a
'download', 'hreflang',
// body
'alink', 'bottommargin', 'leftmargin', 'link', 'rightmargin', 'text', 'topmargin', 'vlink',
// col
'align', 'valign',
// font
'color', 'face', 'size',
// hr
'noshade',
// img
'hspace', 'sizes', 'srcset', 'vspace',
// meter
'low', 'high', 'optimum', 'value',
// ol
'reversed', 'start',
// table
'cols', 'rows', 'frame', 'rules', 'summary', 'cellpadding', 'cellspacing',
// th
'abbr', 'scope',
// td
'colspan', 'rowspan', 'headers'
],
disallowedTags = [
'HEAD','STYLE','SVG','SCRIPT','TITLE','LINK','BASE','META',
'INPUT','OUTPUT','SELECT','BUTTON','TEXTAREA',
'BGSOUND','KEYGEN','SOURCE','OBJECT','EMBED','APPLET','IFRAME','FRAME','FRAMESET','VIDEO','AUDIO','AREA','MAP'
],
nonEmptyTags = [
'A','B','EM','I','SPAN','STRONG','O:P','TABLE'
];
tpl.innerHTML = html
// .replace(/<pre[^>]*>[\s\S]*?<\/pre>/gi, pre => pre.replace(/\n/g, '\n<br>'))
.replace(/<!doctype[^>]*>/gi, '')
.replace(/<\?xml[^>]*\?>/gi, '')
// Not supported by <template> element
.replace(/<(\/?)body(\s[^>]*)?>/gi, '<$1div class="mail-body"$2>')
.replace(/<\/?(html|head)[^>]*>/gi, '')
.trim();
html = '';
// \MailSo\Base\HtmlUtils::ClearComments()
// https://github.com/the-djmaze/snappymail/issues/187
const nodeIterator = document.createNodeIterator(tpl.content, NodeFilter.SHOW_COMMENT);
while (nodeIterator.nextNode()) {
nodeIterator.referenceNode.remove();
}
tpl.content.querySelectorAll('*').forEach(oElement => {
const name = oElement.tagName,
oStyle = oElement.style;
// \MailSo\Base\HtmlUtils::ClearTags()
if (disallowedTags.includes(name)
|| 'none' == oStyle.display
|| 'hidden' == oStyle.visibility
// || (oStyle.lineHeight && 1 > parseFloat(oStyle.lineHeight)
// || (oStyle.maxHeight && 1 > parseFloat(oStyle.maxHeight)
// || (oStyle.maxWidth && 1 > parseFloat(oStyle.maxWidth)
// || ('0' === oStyle.opacity
|| (nonEmptyTags.includes(name) && ('' == oElement.textContent.trim() && !oElement.querySelector('img')))
) {
oElement.remove();
return;
}
// if (['CENTER','FORM'].includes(name)) {
if ('FORM' === name || 'O:P' === name) {
replaceWithChildren(oElement);
return;
}
/*
// Idea to allow CSS
if ('STYLE' === name) {
msgId = '#rl-msg-061eb4d647771be4185943ce91f0039d';
oElement.textContent = oElement.textContent
.replace(/[^{}]+{/g, m => msgId + ' ' + m.replace(',', ', '+msgId+' '))
.replace(/(background-)color:[^};]+/g, '');
return;
}
*/
const aAttrsForRemove = [],
hasAttribute = name => oElement.hasAttribute(name),
getAttribute = name => hasAttribute(name) ? oElement.getAttribute(name).trim() : '',
setAttribute = (name, value) => oElement.setAttribute(name, value),
delAttribute = name => oElement.removeAttribute(name);
if ('mail-body' === oElement.className) {
forEachObjectEntry(tasks, (name, cb) => {
if (hasAttribute(name)) {
cb(getAttribute(name), oElement);
delAttribute(name);
}
});
}
if (oElement.hasAttributes()) {
let i = oElement.attributes.length;
while (i--) {
let sAttrName = oElement.attributes[i].name.toLowerCase();
if (!allowedAttributes.includes(sAttrName)) {
delAttribute(sAttrName);
aAttrsForRemove.push(sAttrName);
}
}
}
let value;
if ('TABLE' === name) {
if (hasAttribute('width')) {
oStyle.width = getAttribute('width');
delAttribute('width');
}
value = oStyle.width;
if (value && !value.includes('%')) {
oStyle.maxWidth = value;
oStyle.width = '100%';
}
}
else if ('A' === name) {
value = oElement.href;
if (!/^([a-z]+):/i.test(value)) {
setAttribute('data-x-broken-href', value);
delAttribute('href');
} else {
oElement.href = stripTracking(value);
setAttribute('target', '_blank');
setAttribute('rel', 'external nofollow noopener noreferrer');
}
setAttribute('tabindex', '-1');
}
// SVG xlink:href
/*
if (hasAttribute('xlink:href')) {
delAttribute('xlink:href');
}
*/
let skipStyle = false;
if (hasAttribute('src')) {
value = getAttribute('src');
delAttribute('src');
if (detectHiddenImages
&& 'IMG' === name
&& (('' != getAttribute('height') && 3 > pInt(getAttribute('height')))
|| ('' != getAttribute('width') && 3 > pInt(getAttribute('width')))
|| [
'email.microsoftemail.com/open',
'github.com/notifications/beacon/',
'mandrillapp.com/track/open',
'list-manage.com/track/open'
].filter(uri => value.toLowerCase().includes(uri)).length
)) {
skipStyle = true;
setAttribute('style', 'display:none');
setAttribute('data-x-hidden-src', value);
}
else if (contentLocationUrls[value])
{
setAttribute('data-x-src-location', value);
result.foundContentLocationUrls.push(value);
}
else if ('cid:' === value.slice(0, 4))
{
setAttribute('data-x-src-cid', value.slice(4));
result.foundCIDs.push(value.slice(4));
}
else if (/^https?:\/\//i.test(value) || '//' === value.slice(0, 2))
{
setAttribute('data-x-src', useProxy ? proxy(value) : value);
result.hasExternals = true;
}
else if ('data:image/' === value.slice(0, 11))
{
setAttribute('src', value);
}
else
{
setAttribute('data-x-broken-src', value);
}
}
if (hasAttribute('background')) {
oStyle.backgroundImage = 'url("' + getAttribute('background') + '")';
delAttribute('background');
}
if (hasAttribute('bgcolor')) {
oStyle.backgroundColor = getAttribute('bgcolor');
delAttribute('bgcolor');
}
if (hasAttribute('color')) {
oStyle.color = getAttribute('color');
delAttribute('color');
}
if (!skipStyle) {
/*
if ('fixed' === oStyle.position) {
oStyle.position = 'absolute';
}
*/
oStyle.removeProperty('behavior');
oStyle.removeProperty('cursor');
oStyle.removeProperty('min-width');
const urls = {
cid: [], // 'data-x-style-cid'
remote: [], // 'data-x-style-url'
broken: [] // 'data-x-broken-style-src'
};
['backgroundImage', 'listStyleImage', 'content'].forEach(property => {
if (oStyle[property]) {
let value = oStyle[property],
found = value.match(/url\s*\(([^)]+)\)/gi);
if (found) {
oStyle[property] = null;
found = found[0].replace(/^["'\s]+|["'\s]+$/g, '');
let lowerUrl = found.toLowerCase();
if ('cid:' === lowerUrl.slice(0, 4)) {
found = found.slice(4);
urls.cid[property] = found
result.foundCIDs.push(found);
} else if (/http[s]?:\/\//.test(lowerUrl) || '//' === found.slice(0, 2)) {
result.hasExternals = true;
urls.remote[property] = useProxy ? proxy(found) : found;
} else if ('data:image/' === lowerUrl.slice(0, 11)) {
oStyle[property] = value;
} else {
urls.broken[property] = found;
}
}
}
});
// oStyle.removeProperty('background-image');
// oStyle.removeProperty('list-style-image');
if (urls.cid.length) {
setAttribute('data-x-style-cid', JSON.stringify(urls.cid));
}
if (urls.remote.length) {
setAttribute('data-x-style-url', JSON.stringify(urls.remote));
}
if (urls.broken.length) {
setAttribute('data-x-style-broken-urls', JSON.stringify(urls.broken));
}
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
if (removeColors) {
oStyle.removeProperty('background-color');
oStyle.removeProperty('background-image');
oStyle.removeProperty('color');
}
// Drop Microsoft Office style properties
// oStyle.cssText = oStyle.cssText.replace(/mso-[^:;]+:[^;]+/gi, '');
}
if (debug && aAttrsForRemove.length) {
setAttribute('data-removed-attrs', aAttrsForRemove.join(', '));
}
});
// return tpl.content.firstChild;
result.html = tpl.innerHTML.trim();
return result;
},
/**
* @param {string} html
* @returns {string}
*/
htmlToPlain = html => {
const
hr = '⎯'.repeat(64),
forEach = (selector, fn) => tpl.content.querySelectorAll(selector).forEach(fn),
blockquotes = node => {
let bq;
while ((bq = node.querySelector('blockquote'))) {
// Convert child blockquote first
blockquotes(bq);
// Convert blockquote
// bq.innerHTML = '\n' + ('\n' + bq.innerHTML.replace(/\n{3,}/gm, '\n\n').trim() + '\n').replace(/^/gm, '&gt; ');
// replaceWithChildren(bq);
bq.replaceWith(
'\n' + ('\n' + bq.textContent.replace(/\n{3,}/g, '\n\n').trim() + '\n').replace(/^/gm, '> ')
);
}
};
html = html
.replace(/<pre[^>]*>([\s\S]*?)<\/pre>/gim, (...args) =>
1 < args.length ? args[1].toString().replace(/\n/g, '<br>') : '')
.replace(/\r?\n/g, '')
.replace(/\s+/gm, ' ');
while (/<(div|tr)[\s>]/i.test(html)) {
html = html.replace(/\n*<(div|tr)(\s[\s\S]*?)?>\n*/gi, '\n');
}
while (/<\/(div|tr)[\s>]/i.test(html)) {
html = html.replace(/\n*<\/(div|tr)(\s[\s\S]*?)?>\n*/gi, '\n');
}
tpl.innerHTML = html
.replace(/<t[dh](\s[\s\S]*?)?>/gi, '\t')
.replace(/<\/tr(\s[\s\S]*?)?>/gi, '\n');
// Convert line-breaks
forEach('br', br => br.replaceWith('\n'));
// lines
forEach('hr', node => node.replaceWith(`\n\n${hr}\n\n`));
// headings
forEach('h1,h2,h3,h4,h5,h6', h => h.replaceWith(`\n\n${'#'.repeat(h.tagName[1])} ${h.textContent}\n\n`));
// paragraphs
forEach('p', node => {
node.prepend('\n\n');
if ('' == node.textContent.trim()) {
node.remove();
} else {
node.after('\n\n');
}
});
// proper indenting and numbering of (un)ordered lists
forEach('ol,ul', node => {
let prefix = '',
parent = node,
ordered = 'OL' == node.tagName,
i = 0;
while (parent && parent.parentNode && parent.parentNode.closest) {
parent = parent.parentNode.closest('ol,ul');
parent && (prefix = ' ' + prefix);
}
node.querySelectorAll(':scope > li').forEach(li => {
li.prepend('\n' + prefix + (ordered ? `${++i}. ` : ' * '));
});
node.prepend('\n\n');
node.after('\n\n');
});
// Convert anchors
forEach('a', a => {
let txt = a.textContent, href = a.href;
return a.replaceWith((txt.trim() == href ? txt : txt + ' ' + href + ' '));
});
// Bold
forEach('b,strong', b => b.replaceWith(`**${b.textContent}**`));
// Italic
forEach('i,em', i => i.replaceWith(`*${i.textContent}*`));
// Blockquotes must be last
blockquotes(tpl.content);
return (tpl.content.textContent || '').replace(/\n{3,}/gm, '\n\n').trim();
},
/**
* @param {string} plain
* @param {boolean} findEmailAndLinksInText = false
* @returns {string}
*/
plainToHtml = plain => {
plain = stripTracking(plain)
.toString()
.replace(/\r/g, '')
.replace(/^>[> ]>+/gm, ([match]) => (match ? match.replace(/[ ]+/g, '') : match));
let bIn = false,
bDo = true,
bStart = true,
aNextText = [],
aText = plain.split('\n');
do {
bDo = false;
aNextText = [];
aText.forEach(sLine => {
bStart = '>' === sLine.slice(0, 1);
if (bStart && !bIn) {
bDo = true;
bIn = true;
aNextText.push('~~~blockquote~~~');
aNextText.push(sLine.slice(1));
} else if (!bStart && bIn) {
if (sLine) {
bIn = false;
aNextText.push('~~~/blockquote~~~');
aNextText.push(sLine);
} else {
aNextText.push(sLine);
}
} else if (bStart && bIn) {
aNextText.push(sLine.slice(1));
} else {
aNextText.push(sLine);
}
});
if (bIn) {
bIn = false;
aNextText.push('~~~/blockquote~~~');
}
aText = aNextText;
} while (bDo);
return aText.join('\n')
// .replace(/~~~\/blockquote~~~\n~~~blockquote~~~/g, '\n')
.replace(/&/g, '&amp;')
.replace(/>/g, '&gt;')
.replace(/</g, '&lt;')
.replace(/~~~blockquote~~~\s*/g, '<blockquote>')
.replace(/\s*~~~\/blockquote~~~/g, '</blockquote>')
.replace(/\n/g, '<br>');
};
export class HtmlEditor { export class HtmlEditor {
/** /**
@ -29,12 +506,6 @@ export class HtmlEditor {
this.onBlur = onBlur; this.onBlur = onBlur;
this.onModeChange = onModeChange; this.onModeChange = onModeChange;
this.resize = (() => {
try {
this.editor && this.editor.resize(element.clientWidth, element.clientHeight);
} catch (e) {} // eslint-disable-line no-empty
}).throttle(100);
if (element) { if (element) {
let editor; let editor;
@ -42,7 +513,6 @@ export class HtmlEditor {
this.onReady = fn => onReady.push(fn); this.onReady = fn => onReady.push(fn);
const readyCallback = () => { const readyCallback = () => {
this.editor = editor; this.editor = editor;
this.resize();
this.onReady = fn => fn(); this.onReady = fn => fn();
onReady.forEach(fn => fn()); onReady.forEach(fn => fn());
}; };
@ -112,31 +582,25 @@ export class HtmlEditor {
* @param {boolean=} wrapIsHtml = false * @param {boolean=} wrapIsHtml = false
* @returns {string} * @returns {string}
*/ */
getData(wrapIsHtml = false) { getData() {
let result = ''; let result = '';
if (this.editor) { if (this.editor) {
try { try {
if (this.isPlain() && this.editor.plugins.plain && this.editor.__plain) { if (this.isPlain() && this.editor.plugins.plain && this.editor.__plain) {
result = this.editor.__plain.getRawData(); result = this.editor.__plain.getRawData();
} else { } else {
result = wrapIsHtml result = this.editor.getData();
? '<div data-html-editor-font-wrapper="true" style="font-family: arial, sans-serif; font-size: 13px;">' +
this.editor.getData() +
'</div>'
: this.editor.getData();
} }
} catch (e) {} // eslint-disable-line no-empty } catch (e) {} // eslint-disable-line no-empty
} }
return result; return result;
} }
/** /**
* @param {boolean=} wrapIsHtml = false
* @returns {string} * @returns {string}
*/ */
getDataWithHtmlMark(wrapIsHtml = false) { getDataWithHtmlMark() {
return (this.isHtml() ? ':HTML:' : '') + this.getData(wrapIsHtml); return (this.isHtml() ? ':HTML:' : '') + this.getData();
} }
modeWysiwyg() { modeWysiwyg() {
@ -147,8 +611,8 @@ export class HtmlEditor {
} }
setHtmlOrPlain(text) { setHtmlOrPlain(text) {
if (':HTML:' === text.substr(0, 6)) { if (':HTML:' === text.slice(0, 6)) {
this.setHtml(text.substr(6)); this.setHtml(text.slice(6));
} else { } else {
this.setPlain(text); this.setPlain(text);
} }
@ -197,3 +661,8 @@ export class HtmlEditor {
this.onReady(() => this.isPlain() ? this.setPlain('') : this.setHtml('')); this.onReady(() => this.isPlain() ? this.setPlain('') : this.setHtml(''));
} }
} }
rl.Utils = {
htmlToPlain: htmlToPlain,
plainToHtml: plainToHtml
};

View file

@ -2,11 +2,14 @@ import { pString, pInt } from 'Common/Utils';
import { Settings } from 'Common/Globals'; import { Settings } from 'Common/Globals';
const const
ROOT = './',
HASH_PREFIX = '#/', HASH_PREFIX = '#/',
SERVER_PREFIX = './?', SERVER_PREFIX = './?',
VERSION = Settings.app('version'), VERSION = Settings.app('version'),
VERSION_PREFIX = Settings.app('webVersionPath') || 'snappymail/v/' + VERSION + '/'; VERSION_PREFIX = () => Settings.app('webVersionPath') || 'snappymail/v/' + VERSION + '/',
adminPath = () => rl.adminArea() && !Settings.app('adminHostUse'),
prefix = () => SERVER_PREFIX + (adminPath() ? Settings.app('adminPath') : '');
export const export const
SUB_QUERY_PREFIX = '&q[]=', SUB_QUERY_PREFIX = '&q[]=',
@ -15,14 +18,12 @@ export const
* @param {string=} startupUrl * @param {string=} startupUrl
* @returns {string} * @returns {string}
*/ */
root = (startupUrl = '') => HASH_PREFIX + pString(startupUrl), root = () => HASH_PREFIX,
/** /**
* @returns {string} * @returns {string}
*/ */
logoutLink = () => (rl.adminArea() && !Settings.app('adminHostUse')) logoutLink = () => adminPath() ? prefix() : './',
? SERVER_PREFIX + (Settings.app('adminPath') || 'admin')
: ROOT,
/** /**
* @param {string} type * @param {string} type
@ -45,17 +46,21 @@ export const
attachmentDownload = (download, customSpecSuffix) => attachmentDownload = (download, customSpecSuffix) =>
serverRequestRaw('Download', download, customSpecSuffix), serverRequestRaw('Download', download, customSpecSuffix),
proxy = url =>
SERVER_PREFIX + '/ProxyExternal/' + btoa(url).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''),
/*
return './?/ProxyExternal/'.Utils::EncodeKeyValuesQ(array(
'Rnd' => \md5(\microtime(true)),
'Token' => Utils::GetConnectionToken(),
'Url' => $sUrl
)).'/';
*/
/** /**
* @param {string} type * @param {string} type
* @returns {string} * @returns {string}
*/ */
serverRequest = type => SERVER_PREFIX + '/' + type + '/' + SUB_QUERY_PREFIX + '/0/', serverRequest = type => prefix() + '/' + type + '/' + SUB_QUERY_PREFIX + '/0/',
/**
* @param {string} email
* @returns {string}
*/
change = email => serverRequest('Change') + encodeURIComponent(email) + '/',
/** /**
* @param {string} lang * @param {string} lang
@ -69,26 +74,16 @@ export const
* @param {string} path * @param {string} path
* @returns {string} * @returns {string}
*/ */
staticLink = path => VERSION_PREFIX + 'static/' + path, staticLink = path => VERSION_PREFIX() + 'static/' + path,
/**
* @returns {string}
*/
openPgpJs = () => staticLink('js/min/openpgp.min.js'),
/**
* @returns {string}
*/
openPgpWorkerJs = () => staticLink('js/min/openpgp.worker.min.js'),
/** /**
* @param {string} theme * @param {string} theme
* @returns {string} * @returns {string}
*/ */
themePreviewLink = theme => { themePreviewLink = theme => {
let prefix = VERSION_PREFIX; let prefix = VERSION_PREFIX();
if ('@custom' === theme.substr(-7)) { if ('@custom' === theme.slice(-7)) {
theme = theme.substr(0, theme.length - 7).trim(); theme = theme.slice(0, theme.length - 7).trim();
prefix = Settings.app('webPath') || ''; prefix = Settings.app('webPath') || '';
} }
@ -124,23 +119,22 @@ export const
* @param {number=} threadUid = 0 * @param {number=} threadUid = 0
* @returns {string} * @returns {string}
*/ */
mailBox = (folder, page = 1, search = '', threadUid = 0) => { mailBox = (folder, page, search, threadUid) => {
page = pInt(page, 1); let result = [HASH_PREFIX + 'mailbox'];
search = pString(search);
let result = HASH_PREFIX + 'mailbox/';
if (folder) { if (folder) {
result += encodeURI(folder) + (threadUid ? '~' + threadUid : ''); result.push(folder + (threadUid ? '~' + threadUid : ''));
} }
page = pInt(page, 1);
if (1 < page) { if (1 < page) {
result = result.replace(/\/+$/, '') + '/p' + page; result.push('p' + page);
} }
search = pString(search);
if (search) { if (search) {
result = result.replace(/\/+$/, '') + '/' + encodeURI(search); result.push(encodeURI(search));
} }
return result; return result.join('/');
}; };

View file

@ -1,54 +0,0 @@
import { i18n } from 'Common/Translator';
export function timestampToString(timeStampInUTC, formatStr) {
const now = Date.now(),
time = 0 < timeStampInUTC ? Math.min(now, timeStampInUTC * 1000) : (0 === timeStampInUTC ? now : 0);
if (31536000000 < time) {
const m = new Date(time);
switch (formatStr) {
case 'FROMNOW':
return m.fromNow();
case 'SHORT': {
if (4 >= (now - time) / 3600000)
return m.fromNow();
const mt = m.getTime(), date = new Date,
dt = date.setHours(0,0,0,0);
if (mt > dt)
return i18n('MESSAGE_LIST/TODAY_AT', {TIME: m.format('LT')});
if (mt > dt - 86400000)
return i18n('MESSAGE_LIST/YESTERDAY_AT', {TIME: m.format('LT')});
if (date.getFullYear() === m.getFullYear())
return m.format('d M');
return m.format('LL');
}
case 'FULL':
return m.format('LLL');
default:
return m.format(formatStr);
}
}
return '';
}
export function timeToNode(element, time) {
try {
time = time || (Date.parse(element.dateTime) / 1000);
if (time) {
element.dateTime = (new Date(time * 1000)).format('Y-m-d\\TH:i:s');
let key = element.dataset.momentFormat;
if (key) {
element.textContent = timestampToString(time, key);
}
if ((key = element.dataset.momentFormatTitle)) {
element.title = timestampToString(time, key);
}
}
} catch (e) {
// prevent knockout crashes
console.error(e);
}
}

View file

@ -1,6 +1,5 @@
import { settingsAddViewModel } from 'Screen/AbstractSettings'; import { settingsAddViewModel } from 'Screen/AbstractSettings';
import { SettingsGet } from 'Common/Globals'; import { SettingsGet } from 'Common/Globals';
import { showScreenPopup } from 'Knoin/Knoin';
import { AbstractViewPopup } from 'Knoin/AbstractViews'; import { AbstractViewPopup } from 'Knoin/AbstractViews';
const USER_VIEW_MODELS_HOOKS = [], const USER_VIEW_MODELS_HOOKS = [],
@ -13,7 +12,7 @@ const USER_VIEW_MODELS_HOOKS = [],
* @param {?number=} timeout * @param {?number=} timeout
*/ */
rl.pluginRemoteRequest = (callback, action, parameters, timeout) => { rl.pluginRemoteRequest = (callback, action, parameters, timeout) => {
rl.app && rl.app.Remote.defaultRequest(callback, 'Plugin' + action, parameters, timeout); rl.app.Remote.request('Plugin' + action, callback, parameters, timeout);
}; };
/** /**
@ -56,5 +55,4 @@ rl.pluginSettingsGet = (pluginSection, name) => {
return plugins ? (null == plugins[name] ? null : plugins[name]) : null; return plugins ? (null == plugins[name] ? null : plugins[name]) : null;
}; };
rl.showPluginPopup = showScreenPopup;
rl.pluginPopupView = AbstractViewPopup; rl.pluginPopupView = AbstractViewPopup;

View file

@ -1,5 +1,7 @@
import ko from 'ko'; import ko from 'ko';
import { addEventsListeners, addShortcut, registerShortcut } from 'Common/Globals';
import { isArray } from 'Common/Utils'; import { isArray } from 'Common/Utils';
import { koComputable } from 'External/ko';
/* /*
oCallbacks: oCallbacks:
@ -28,8 +30,8 @@ export class Selector {
sItemFocusedSelector sItemFocusedSelector
) { ) {
this.list = koList; this.list = koList;
this.listChecked = ko.computed(() => this.list.filter(item => item.checked())).extend({ rateLimit: 0 }); this.listChecked = koComputable(() => this.list.filter(item => item.checked())).extend({ rateLimit: 0 });
this.isListChecked = ko.computed(() => 0 < this.listChecked().length); this.isListChecked = koComputable(() => 0 < this.listChecked().length);
this.focusedItem = koFocusedItem || ko.observable(null); this.focusedItem = koFocusedItem || ko.observable(null);
this.selectedItem = koSelectedItem || ko.observable(null); this.selectedItem = koSelectedItem || ko.observable(null);
@ -238,32 +240,33 @@ export class Selector {
return el ? ko.dataFor(el) : null; return el ? ko.dataFor(el) : null;
}; };
contentScrollable.addEventListener('click', event => { addEventsListeners(contentScrollable, {
let el = event.target.closestWithin(this.sItemSelector, contentScrollable); click: event => {
el && this.actionClick(ko.dataFor(el), event); let el = event.target.closestWithin(this.sItemSelector, contentScrollable);
el && this.actionClick(ko.dataFor(el), event);
const item = getItem(this.sItemCheckedSelector); const item = getItem(this.sItemCheckedSelector);
if (item) {
if (event.shiftKey) {
this.actionClick(item, event);
} else {
this.focusedItem(item);
item.checked(!item.checked());
}
}
});
contentScrollable.addEventListener('auxclick', event => {
if (1 == event.button) {
const item = getItem(this.sItemSelector);
if (item) { if (item) {
this.focusedItem(item); if (event.shiftKey) {
(this.oCallbacks.MiddleClick || (()=>0))(item); this.actionClick(item, event);
} else {
this.focusedItem(item);
item.checked(!item.checked());
}
}
},
auxclick: event => {
if (1 == event.button) {
const item = getItem(this.sItemSelector);
if (item) {
this.focusedItem(item);
(this.oCallbacks.MiddleClick || (()=>0))(item);
}
} }
} }
}); });
shortcuts.add('enter,open', '', keyScope, () => { registerShortcut('enter,open', '', keyScope, () => {
const focused = this.focusedItem(); const focused = this.focusedItem();
if (focused && !focused.selected()) { if (focused && !focused.selected()) {
this.actionClick(focused); this.actionClick(focused);
@ -271,13 +274,13 @@ export class Selector {
} }
}); });
shortcuts.add('arrowup,arrowdown', 'meta', keyScope, () => false); addShortcut('arrowup,arrowdown', 'meta', keyScope, () => false);
shortcuts.add('arrowup,arrowdown', 'shift', keyScope, event => { addShortcut('arrowup,arrowdown', 'shift', keyScope, event => {
this.newSelectPosition(event.key, true); this.newSelectPosition(event.key, true);
return false; return false;
}); });
shortcuts.add('arrowup,arrowdown,home,end,pageup,pagedown,space', '', keyScope, event => { registerShortcut('arrowup,arrowdown,home,end,pageup,pagedown,space', '', keyScope, event => {
this.newSelectPosition(event.key, false); this.newSelectPosition(event.key, false);
return false; return false;
}); });
@ -288,7 +291,7 @@ export class Selector {
* @returns {boolean} * @returns {boolean}
*/ */
autoSelect() { autoSelect() {
return !!(this.oCallbacks.AutoSelect || (()=>true))(); return !!(this.oCallbacks.AutoSelect || (()=>1))();
} }
/** /**
@ -329,7 +332,7 @@ export class Selector {
} else if (++i < listLen) { } else if (++i < listLen) {
result = list[i]; result = list[i];
} }
result || (this.oCallbacks.UpOrDown || (()=>true))('ArrowUp' === sEventKey); result || (this.oCallbacks.UpOrDown || (()=>0))('ArrowUp' === sEventKey);
} else if ('Home' === sEventKey) { } else if ('Home' === sEventKey) {
result = list[0]; result = list[0];
} else if ('End' === sEventKey) { } else if ('End' === sEventKey) {

View file

@ -2,6 +2,7 @@ import ko from 'ko';
import { Notification, UploadErrorCode } from 'Common/Enums'; import { Notification, UploadErrorCode } from 'Common/Enums';
import { langLink } from 'Common/Links'; import { langLink } from 'Common/Links';
import { doc, createElement } from 'Common/Globals'; import { doc, createElement } from 'Common/Globals';
import { getKeyByValue, forEachObjectEntry } from 'Common/Utils';
let I18N_DATA = {}; let I18N_DATA = {};
@ -9,16 +10,16 @@ const
i18nToNode = element => { i18nToNode = element => {
const key = element.dataset.i18n; const key = element.dataset.i18n;
if (key) { if (key) {
if ('[' === key.substr(0, 1)) { if ('[' === key.slice(0, 1)) {
switch (key.substr(0, 6)) { switch (key.slice(0, 6)) {
case '[html]': case '[html]':
element.innerHTML = i18n(key.substr(6)); element.innerHTML = i18n(key.slice(6));
break; break;
case '[place': case '[place':
element.placeholder = i18n(key.substr(13)); element.placeholder = i18n(key.slice(13));
break; break;
case '[title': case '[title':
element.title = i18n(key.substr(7)); element.title = i18n(key.slice(7));
break; break;
// no default // no default
} }
@ -39,14 +40,9 @@ const
i18nKey = key => key.replace(/([a-z])([A-Z])/g, '$1_$2').toUpperCase(), i18nKey = key => key.replace(/([a-z])([A-Z])/g, '$1_$2').toUpperCase(),
getKeyByValue = (o, v) => Object.keys(o).find(key => o[key] === v),
getNotificationMessage = code => { getNotificationMessage = code => {
let key = getKeyByValue(Notification, code); let key = getKeyByValue(Notification, code);
if (key) { return key ? I18N_DATA.NOTIFICATIONS[i18nKey(key).replace('_NOTIFICATION', '_ERROR')] : '';
key = i18nKey(key).replace('_NOTIFICATION', '_ERROR');
return I18N_DATA.NOTIFICATIONS[key];
}
}; };
export const export const
@ -67,7 +63,7 @@ export const
} }
} }
if (valueList) { if (valueList) {
Object.entries(valueList).forEach(([key, value]) => { forEachObjectEntry(valueList, (key, value) => {
result = result.replace('%' + key + '%', value); result = result.replace('%' + key + '%', value);
}); });
} }
@ -83,6 +79,64 @@ export const
element.querySelectorAll('[data-i18n]').forEach(item => i18nToNode(item)) element.querySelectorAll('[data-i18n]').forEach(item => i18nToNode(item))
, 1), , 1),
timestampToString = (timeStampInUTC, formatStr) => {
const now = Date.now(),
time = 0 < timeStampInUTC ? Math.min(now, timeStampInUTC * 1000) : (0 === timeStampInUTC ? now : 0);
if (31536000000 < time) {
const m = new Date(time);
switch (formatStr) {
case 'FROMNOW':
return m.fromNow();
case 'SHORT': {
if (4 >= (now - time) / 3600000)
return m.fromNow();
const mt = m.getTime(), date = new Date,
dt = date.setHours(0,0,0,0);
if (mt > dt)
return i18n('MESSAGE_LIST/TODAY_AT', {TIME: m.format('LT')});
if (mt > dt - 86400000)
return i18n('MESSAGE_LIST/YESTERDAY_AT', {TIME: m.format('LT')});
if (date.getFullYear() === m.getFullYear())
return m.format('d M');
return m.format('LL');
}
case 'FULL':
return m.format('LLL');
default:
return m.format(formatStr);
}
}
return '';
},
timeToNode = (element, time) => {
try {
if (time) {
element.dateTime = (new Date(time * 1000)).format('Y-m-d\\TH:i:s');
} else {
time = Date.parse(element.dateTime) / 1000;
}
let key = element.dataset.momentFormat;
if (key) {
element.textContent = timestampToString(time, key);
}
if ((key = element.dataset.momentFormatTitle)) {
element.title = timestampToString(time, key);
}
} catch (e) {
// prevent knockout crashes
console.error(e);
}
},
reloadTime = () => setTimeout(() =>
doc.querySelectorAll('time').forEach(element => timeToNode(element))
, 1),
/** /**
* @param {Function} startCallback * @param {Function} startCallback
* @param {Function=} langCallback = null * @param {Function=} langCallback = null
@ -115,33 +169,22 @@ export const
* @returns {string} * @returns {string}
*/ */
getUploadErrorDescByCode = code => { getUploadErrorDescByCode = code => {
let result = 'UNKNOWN'; let key = getKeyByValue(UploadErrorCode, parseInt(code, 10));
code = parseInt(code, 10) || 0; return i18n('UPLOAD/ERROR_' + (key ? i18nKey(key) : 'UNKNOWN'));
switch (code) {
case UploadErrorCode.FileIsTooBig:
case UploadErrorCode.FilePartiallyUploaded:
case UploadErrorCode.NoFileUploaded:
case UploadErrorCode.MissingTempFolder:
case UploadErrorCode.OnSavingFile:
case UploadErrorCode.FileType:
result = i18nKey(getKeyByValue(UploadErrorCode, code));
break;
}
return i18n('UPLOAD/ERROR_' + result);
}, },
/** /**
* @param {boolean} admin * @param {boolean} admin
* @param {string} language * @param {string} language
*/ */
reload = (admin, language) => translatorReload = (admin, language) =>
new Promise((resolve, reject) => { new Promise((resolve, reject) => {
const script = createElement('script'); const script = createElement('script');
script.onload = () => { script.onload = () => {
// reload the data // reload the data
if (init()) { if (init()) {
i18nToNodes(doc); i18nToNodes(doc);
admin || rl.app.reloadTime(); admin || reloadTime();
trigger(!trigger()); trigger(!trigger());
} }
script.remove(); script.remove();

View file

@ -1,5 +1,5 @@
import { SaveSettingsStep } from 'Common/Enums'; import { SaveSettingsStep } from 'Common/Enums';
import { doc, elementById } from 'Common/Globals'; import { elementById } from 'Common/Globals';
let __themeTimer = 0, let __themeTimer = 0,
__themeJson = null; __themeJson = null;
@ -10,6 +10,10 @@ export const
isFunction = v => typeof v === 'function', isFunction = v => typeof v === 'function',
pString = value => null != value ? '' + value : '', pString = value => null != value ? '' + value : '',
forEachObjectValue = (obj, fn) => Object.values(obj).forEach(fn),
forEachObjectEntry = (obj, fn) => Object.entries(obj).forEach(([key, value]) => fn(key, value)),
pInt = (value, defaultValue = 0) => { pInt = (value, defaultValue = 0) => {
value = parseInt(value, 10); value = parseInt(value, 10);
return isNaN(value) || !isFinite(value) ? defaultValue : value; return isNaN(value) || !isFinite(value) ? defaultValue : value;
@ -25,72 +29,43 @@ export const
domItem && item && undefined !== item.disabled domItem && item && undefined !== item.disabled
&& domItem.classList.toggle('disabled', domItem.disabled = item.disabled), && domItem.classList.toggle('disabled', domItem.disabled = item.disabled),
addObservablesTo = (target, observables) => // unescape(encodeURIComponent()) makes the UTF-16 DOMString to an UTF-8 string
Object.entries(observables).forEach(([key, value]) => b64EncodeJSON = data => btoa(unescape(encodeURIComponent(JSON.stringify(data)))),
target[key] = /*isArray(value) ? ko.observableArray(value) :*/ ko.observable(value) ), /* // Without deprecated 'unescape':
b64EncodeJSON = data => btoa(encodeURIComponent(JSON.stringify(data)).replace(
addComputablesTo = (target, computables) => /%([0-9A-F]{2})/g, (match, p1) => String.fromCharCode('0x' + p1)
Object.entries(computables).forEach(([key, fn]) => target[key] = ko.computed(fn)), )),
*/
addSubscribablesTo = (target, subscribables) => b64EncodeJSONSafe = data => b64EncodeJSON(data).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''),
Object.entries(subscribables).forEach(([key, fn]) => target[key].subscribe(fn)),
inFocus = () => {
try {
return doc.activeElement && doc.activeElement.matches(
'input,textarea,[contenteditable]'
);
} catch (e) {
return false;
}
},
settingsSaveHelperSimpleFunction = (koTrigger, context) =>
iError => {
koTrigger.call(context, iError ? SaveSettingsStep.FalseResult : SaveSettingsStep.TrueResult);
setTimeout(() => koTrigger.call(context, SaveSettingsStep.Idle), 1000);
},
changeTheme = (value, themeTrigger = ()=>0) => { changeTheme = (value, themeTrigger = ()=>0) => {
const themeStyle = elementById('app-theme-style'), const themeStyle = elementById('app-theme-style'),
clearTimer = () => { clearTimer = () => {
__themeTimer = setTimeout(() => themeTrigger(SaveSettingsStep.Idle), 1000); __themeTimer = setTimeout(() => themeTrigger(SaveSettingsStep.Idle), 1000);
__themeJson = null; __themeJson = null;
}; },
url = themeStyle.dataset.href.replace(/(Admin|User)\/-\/[^/]+\//, '$1/-/' + value + '/') + 'Json/';
let url = themeStyle.dataset.href; clearTimeout(__themeTimer);
if (url) { themeTrigger(SaveSettingsStep.Animate);
url = url.toString()
.replace(/\/-\/[^/]+\/-\//, '/-/' + value + '/-/')
.replace(/\/Css\/[^/]+\/User\//, '/Css/0/User/')
.replace(/\/Hash\/[^/]+\//, '/Hash/-/');
if ('Json/' !== url.substr(-5)) { if (__themeJson) {
url += 'Json/'; __themeJson.abort();
}
clearTimeout(__themeTimer);
themeTrigger(SaveSettingsStep.Animate);
if (__themeJson) {
__themeJson.abort();
}
let init = {};
if (window.AbortController) {
__themeJson = new AbortController();
init.signal = __themeJson.signal;
}
rl.fetchJSON(url, init)
.then(data => {
if (2 === arrayLength(data)) {
themeStyle.textContent = data[1];
themeStyle.dataset.href = url;
themeStyle.dataset.theme = data[0];
themeTrigger(SaveSettingsStep.TrueResult);
}
})
.then(clearTimer, clearTimer);
} }
}; let init = {};
if (window.AbortController) {
__themeJson = new AbortController();
init.signal = __themeJson.signal;
}
rl.fetchJSON(url, init)
.then(data => {
if (2 === arrayLength(data)) {
themeStyle.textContent = data[1];
themeTrigger(SaveSettingsStep.TrueResult);
}
})
.then(clearTimer, clearTimer);
},
getKeyByValue = (o, v) => Object.keys(o).find(key => o[key] === v);

View file

@ -1,260 +1,46 @@
import { ComposeType/*, FolderType*/ } from 'Common/EnumsUser'; import { MessageFlagsCache, addRequestedMessage } from 'Common/Cache';
import { Notification } from 'Common/Enums';
import { MessageSetAction, ComposeType/*, FolderType*/ } from 'Common/EnumsUser';
import { doc, createElement, elementById, dropdowns, dropdownVisibility } from 'Common/Globals';
import { plainToHtml } from 'Common/Html';
import { getNotification } from 'Common/Translator';
import { EmailModel } from 'Model/Email'; import { EmailModel } from 'Model/Email';
import { encodeHtml } from 'Common/Html'; import { MessageModel } from 'Model/Message';
import { isArray } from 'Common/Utils'; import { MessageUserStore } from 'Stores/User/Message';
import { createElement } from 'Common/Globals'; import { MessagelistUserStore } from 'Stores/User/Messagelist';
import { FolderUserStore } from 'Stores/User/Folder';
import { SettingsUserStore } from 'Stores/User/Settings'; import { SettingsUserStore } from 'Stores/User/Settings';
import * as Local from 'Storage/Client';
import { ThemeStore } from 'Stores/Theme';
import Remote from 'Remote/User/Fetch';
export const
dropdownsDetectVisibility = (() =>
dropdownVisibility(!!dropdowns.find(item => item.classList.contains('show')))
).debounce(50),
/** /**
* @param {(string|number)} value * @param {string} link
* @param {boolean=} includeZero = true
* @returns {boolean} * @returns {boolean}
*/ */
export function isPosNumeric(value) { download = (link, name = "") => {
return null != value && /^[0-9]*$/.test(value.toString()); if (ThemeStore.isMobile()) {
} open(link, '_self');
focus();
/** } else {
* @param {string} html const oLink = createElement('a');
* @returns {string} oLink.href = link;
*/ oLink.target = '_blank';
export function htmlToPlain(html) { oLink.download = name;
let pos = 0, doc.body.appendChild(oLink).click();
limit = 800, oLink.remove();
iP1 = 0,
iP2 = 0,
iP3 = 0,
text = '';
const
tpl = createElement('template'),
convertBlockquote = (blockquoteText) => {
blockquoteText = '> ' + blockquoteText.trim().replace(/\n/gm, '\n> ');
return blockquoteText.replace(/(^|\n)([> ]+)/gm, (...args) =>
args && 2 < args.length ? args[1] + args[2].replace(/[\s]/g, '').trim() + ' ' : ''
);
},
convertDivs = (...args) => {
let divText = 1 < args.length ? args[1].trim() : '';
if (divText.length) {
divText = '\n' + divText.replace(/<div[^>]*>([\s\S\r\n]*)<\/div>/gim, convertDivs).trim() + '\n';
}
return divText;
},
convertPre = (...args) =>
1 < args.length
? args[1]
.toString()
.replace(/[\n]/gm, '<br/>')
.replace(/[\r]/gm, '')
: '',
fixAttibuteValue = (...args) => (1 < args.length ? args[1] + encodeHtml(args[2]) : ''),
convertLinks = (...args) => (1 < args.length ? args[1].trim() : '');
tpl.innerHTML = html
.replace(/<p[^>]*><\/p>/gi, '')
.replace(/<pre[^>]*>([\s\S\r\n\t]*)<\/pre>/gim, convertPre)
.replace(/[\s]+/gm, ' ')
.replace(/((?:href|data)\s?=\s?)("[^"]+?"|'[^']+?')/gim, fixAttibuteValue)
.replace(/<br[^>]*>/gim, '\n')
.replace(/<\/h[\d]>/gi, '\n')
.replace(/<\/p>/gi, '\n\n')
.replace(/<ul[^>]*>/gim, '\n')
.replace(/<\/ul>/gi, '\n')
.replace(/<li[^>]*>/gim, ' * ')
.replace(/<\/li>/gi, '\n')
.replace(/<\/td>/gi, '\n')
.replace(/<\/tr>/gi, '\n')
.replace(/<hr[^>]*>/gim, '\n_______________________________\n\n')
.replace(/<div[^>]*>([\s\S\r\n]*)<\/div>/gim, convertDivs)
.replace(/<blockquote[^>]*>/gim, '\n__bq__start__\n')
.replace(/<\/blockquote>/gim, '\n__bq__end__\n')
.replace(/<a [^>]*>([\s\S\r\n]*?)<\/a>/gim, convertLinks)
.replace(/<\/div>/gi, '\n')
.replace(/&nbsp;/gi, ' ')
.replace(/&quot;/gi, '"')
.replace(/<[^>]*>/gm, '');
text = tpl.content.textContent;
if (text) {
text = text
.replace(/\n[ \t]+/gm, '\n')
.replace(/[\n]{3,}/gm, '\n\n')
.replace(/&gt;/gi, '>')
.replace(/&lt;/gi, '<')
.replace(/&amp;/gi, '&')
// wordwrap max line length 100
.match(/.{1,100}(\s|$)|\S+?(\s|$)/g).join('\n');
} }
},
while (0 < --limit) {
iP1 = text.indexOf('__bq__start__', pos);
if (0 > iP1) {
break;
}
iP2 = text.indexOf('__bq__start__', iP1 + 5);
iP3 = text.indexOf('__bq__end__', iP1 + 5);
if ((-1 === iP2 || iP3 < iP2) && iP1 < iP3) {
text = text.substr(0, iP1) + convertBlockquote(text.substring(iP1 + 13, iP3)) + text.substr(iP3 + 11);
pos = 0;
} else if (-1 < iP2 && iP2 < iP3) {
pos = iP2 - 1;
} else {
pos = 0;
}
}
return text.replace(/__bq__start__|__bq__end__/gm, '').trim();
}
/**
* @param {string} plain
* @param {boolean} findEmailAndLinksInText = false
* @returns {string}
*/
export function plainToHtml(plain) {
plain = plain.toString().replace(/\r/g, '');
plain = plain.replace(/^>[> ]>+/gm, ([match]) => (match ? match.replace(/[ ]+/g, '') : match));
let bIn = false,
bDo = true,
bStart = true,
aNextText = [],
aText = plain.split('\n');
do {
bDo = false;
aNextText = [];
aText.forEach(sLine => {
bStart = '>' === sLine.substr(0, 1);
if (bStart && !bIn) {
bDo = true;
bIn = true;
aNextText.push('~~~blockquote~~~');
aNextText.push(sLine.substr(1));
} else if (!bStart && bIn) {
if (sLine) {
bIn = false;
aNextText.push('~~~/blockquote~~~');
aNextText.push(sLine);
} else {
aNextText.push(sLine);
}
} else if (bStart && bIn) {
aNextText.push(sLine.substr(1));
} else {
aNextText.push(sLine);
}
});
if (bIn) {
bIn = false;
aNextText.push('~~~/blockquote~~~');
}
aText = aNextText;
} while (bDo);
return aText.join('\n')
// .replace(/~~~\/blockquote~~~\n~~~blockquote~~~/g, '\n')
.replace(/&/g, '&amp;')
.replace(/>/g, '&gt;')
.replace(/</g, '&lt;')
.replace(/~~~blockquote~~~[\s]*/g, '<blockquote>')
.replace(/[\s]*~~~\/blockquote~~~/g, '</blockquote>')
.replace(/\n/g, '<br/>');
}
rl.Utils = {
htmlToPlain: htmlToPlain,
plainToHtml: plainToHtml
};
/**
* @param {Array=} aDisabled
* @param {Array=} aHeaderLines
* @param {Function=} fDisableCallback
* @param {Function=} fRenameCallback
* @param {boolean=} bNoSelectSelectable Used in FolderCreatePopupView
* @returns {Array}
*/
export function folderListOptionsBuilder(
aDisabled,
aHeaderLines,
fRenameCallback,
fDisableCallback,
bNoSelectSelectable,
aList = FolderUserStore.folderList()
) {
const
aResult = [],
sDeepPrefix = '\u00A0\u00A0\u00A0',
// FolderSystemPopupView should always be true
showUnsubscribed = fRenameCallback ? !SettingsUserStore.hideUnsubscribed() : true,
foldersWalk = folders => {
folders.forEach(oItem => {
if (showUnsubscribed || oItem.hasSubscriptions() || !oItem.exists) {
aResult.push({
id: oItem.fullNameRaw,
name:
sDeepPrefix.repeat(oItem.deep) +
fRenameCallback(oItem),
system: false,
disabled: !bNoSelectSelectable && (
!oItem.selectable() ||
aDisabled.includes(oItem.fullNameRaw) ||
fDisableCallback(oItem))
});
}
if (oItem.subFolders.length) {
foldersWalk(oItem.subFolders());
}
});
};
fDisableCallback = fDisableCallback || (() => false);
fRenameCallback = fRenameCallback || (oItem => oItem.name());
isArray(aDisabled) || (aDisabled = []);
isArray(aHeaderLines) && aHeaderLines.forEach(line =>
aResult.push({
id: line[0],
name: line[1],
system: false,
disabled: false
})
);
foldersWalk(aList);
return aResult;
}
/**
* Call the Model/CollectionModel onDestroy() to clear knockout functions/objects
* @param {Object|Array} objectOrObjects
* @returns {void}
*/
export function delegateRunOnDestroy(objectOrObjects) {
objectOrObjects && (isArray(objectOrObjects) ? objectOrObjects : [objectOrObjects]).forEach(
obj => obj.onDestroy && obj.onDestroy()
);
}
/** /**
* @returns {function} * @returns {function}
*/ */
export function computedPaginatorHelper(koCurrentPage, koPageCount) { computedPaginatorHelper = (koCurrentPage, koPageCount) => {
return () => { return () => {
const currentPage = koCurrentPage(), const currentPage = koCurrentPage(),
pageCount = koPageCount(), pageCount = koPageCount(),
@ -335,71 +121,253 @@ export function computedPaginatorHelper(koCurrentPage, koPageCount) {
return result; return result;
}; };
} },
/** /**
* @param {string} mailToUrl * @param {string} mailToUrl
* @returns {boolean} * @returns {boolean}
*/ */
export function mailToHelper(mailToUrl) { mailToHelper = mailToUrl => {
if ( if (mailToUrl && 'mailto:' === mailToUrl.slice(0, 7).toLowerCase()) {
mailToUrl && mailToUrl = mailToUrl.slice(7).split('?');
'mailto:' ===
mailToUrl
.toString()
.substr(0, 7)
.toLowerCase()
) {
mailToUrl = mailToUrl.toString().substr(7);
let to = [], const
params = {}; email = mailToUrl[0],
params = new URLSearchParams(mailToUrl[1]),
const email = mailToUrl.replace(/\?.+$/, ''), toEmailModel = value => null != value ? EmailModel.parseEmailLine(value) : null;
query = mailToUrl.replace(/^[^?]*\?/, ''),
toEmailModel = value => null != value ? EmailModel.parseEmailLine(decodeURIComponent(value)) : null;
query.split('&').forEach(temp => {
temp = temp.split('=');
params[decodeURIComponent(temp[0])] = decodeURIComponent(temp[1]);
});
if (null != params.to) {
to = Object.values(
toEmailModel(email + ',' + params.to).reduce((result, value) => {
if (value) {
if (result[value.email]) {
if (!result[value.email].name) {
result[value.email] = value;
}
} else {
result[value.email] = value;
}
}
return result;
}, {})
);
} else {
to = EmailModel.parseEmailLine(email);
}
showMessageComposer([ showMessageComposer([
ComposeType.Empty, ComposeType.Empty,
null, null,
to, params.get('to')
toEmailModel(params.cc), ? Object.values(
toEmailModel(params.bcc), toEmailModel(email + ',' + params.get('to')).reduce((result, value) => {
null == params.subject ? null : decodeURIComponent(params.subject), if (value) {
null == params.body ? null : plainToHtml(decodeURIComponent(params.body)) if (result[value.email]) {
if (!result[value.email].name) {
result[value.email] = value;
}
} else {
result[value.email] = value;
}
}
return result;
}, {})
)
: EmailModel.parseEmailLine(email),
toEmailModel(params.get('cc')),
toEmailModel(params.get('bcc')),
params.get('subject'),
plainToHtml(params.get('body') || '')
]); ]);
return true; return true;
} }
return false; return false;
} },
export function showMessageComposer(params = []) showMessageComposer = (params = []) =>
{ {
rl.app.showMessageComposer(params); rl.app.showMessageComposer(params);
} },
initFullscreen = (el, fn) =>
{
let event = 'fullscreenchange';
if (!el.requestFullscreen && el.webkitRequestFullscreen) {
el.requestFullscreen = el.webkitRequestFullscreen;
event = 'webkit'+event;
}
if (el.requestFullscreen) {
el.addEventListener(event, fn);
return el;
}
},
setLayoutResizer = (source, target, sClientSideKeyName, mode) =>
{
if (source.layoutResizer && source.layoutResizer.mode != mode) {
target.removeAttribute('style');
source.removeAttribute('style');
}
// source.classList.toggle('resizable', mode);
if (mode) {
const length = Local.get(sClientSideKeyName+mode);
if (!source.layoutResizer) {
const resizer = createElement('div', {'class':'resizer'}),
size = {},
store = () => {
if ('Width' == resizer.mode) {
target.style.left = source.offsetWidth + 'px';
Local.set(resizer.key+resizer.mode, source.offsetWidth);
} else {
target.style.top = (4 + source.offsetTop + source.offsetHeight) + 'px';
Local.set(resizer.key+resizer.mode, source.offsetHeight);
}
},
cssint = s => {
let value = getComputedStyle(source, null)[s].replace('px', '');
if (value.includes('%')) {
value = source.parentElement['offset'+resizer.mode]
* value.replace('%', '') / 100;
}
return parseFloat(value);
};
source.layoutResizer = resizer;
source.append(resizer);
resizer.addEventListener('mousedown', {
handleEvent: function(e) {
if ('mousedown' == e.type) {
const lmode = resizer.mode.toLowerCase();
e.preventDefault();
size.pos = ('width' == lmode) ? e.pageX : e.pageY;
size.min = cssint('min-'+lmode);
size.max = cssint('max-'+lmode);
size.org = cssint(lmode);
addEventListener('mousemove', this);
addEventListener('mouseup', this);
} else if ('mousemove' == e.type) {
const lmode = resizer.mode.toLowerCase(),
length = size.org + (('width' == lmode ? e.pageX : e.pageY) - size.pos);
if (length >= size.min && length <= size.max ) {
source.style[lmode] = length + 'px';
source.observer || store();
}
} else if ('mouseup' == e.type) {
removeEventListener('mousemove', this);
removeEventListener('mouseup', this);
}
}
});
source.observer = window.ResizeObserver ? new ResizeObserver(store) : null;
}
source.layoutResizer.mode = mode;
source.layoutResizer.key = sClientSideKeyName;
source.observer && source.observer.observe(source, { box: 'border-box' });
if (length) {
source.style[mode] = length + 'px';
}
} else {
source.observer && source.observer.disconnect();
}
},
populateMessageBody = (oMessage, preload) => {
if (oMessage) {
preload || MessageUserStore.hideMessageBodies();
preload || MessageUserStore.loading(true);
Remote.message((iError, oData/*, bCached*/) => {
if (iError) {
if (Notification.RequestAborted !== iError && !preload) {
MessageUserStore.message(null);
MessageUserStore.error(getNotification(iError));
}
} else {
oMessage = preload ? oMessage : null;
let
isNew = false,
json = oData && oData.Result,
message = oMessage || MessageUserStore.message();
if (
json &&
MessageModel.validJson(json) &&
message &&
message.folder === json.Folder
) {
const threads = message.threads(),
messagesDom = MessageUserStore.bodiesDom();
if (!oMessage && message.uid != json.Uid && threads.includes(json.Uid)) {
message = MessageModel.reviveFromJson(json);
if (message) {
message.threads(threads);
MessageFlagsCache.initMessage(message);
// Set clone
MessageUserStore.message(MessageModel.fromMessageListItem(message));
message = MessageUserStore.message();
isNew = true;
}
}
if (message && message.uid == json.Uid) {
oMessage || MessageUserStore.error('');
/*
if (bCached) {
delete json.Flags;
}
*/
isNew || message.revivePropertiesFromJson(json);
addRequestedMessage(message.folder, message.uid);
if (messagesDom) {
let id = 'rl-msg-' + message.hash.replace(/[^a-zA-Z0-9]/g, ''),
body = elementById(id);
if (body) {
message.body = body;
message.isHtml(body.classList.contains('html'));
message.hasImages(body.rlHasImages);
} else {
body = Element.fromHTML('<div id="' + id + '" hidden="" class="b-text-part '
+ (message.pgpSigned() ? ' openpgp-signed' : '')
+ (message.pgpEncrypted() ? ' openpgp-encrypted' : '')
+ '">'
+ '</div>');
message.body = body;
if (!SettingsUserStore.viewHTML() || !message.viewHtml()) {
message.viewPlain();
}
MessageUserStore.purgeMessageBodyCache();
}
messagesDom.append(body);
if (!oMessage) {
MessageUserStore.activeDom(message.body);
MessageUserStore.hideMessageBodies();
message.body.hidden = false;
}
oMessage && message.viewPopupMessage();
}
MessageFlagsCache.initMessage(message);
if (message.isUnseen()) {
MessageUserStore.MessageSeenTimer = setTimeout(
() => MessagelistUserStore.setAction(message.folder, MessageSetAction.SetSeen, [message]),
SettingsUserStore.messageReadDelay() * 1000 // seconds
);
}
if (message && isNew) {
let selectedMessage = MessagelistUserStore.selectedMessage();
if (
selectedMessage &&
(message.folder !== selectedMessage.folder || message.uid != selectedMessage.uid)
) {
MessagelistUserStore.selectedMessage(null);
if (1 === MessagelistUserStore.length) {
MessagelistUserStore.focusedMessage(null);
}
} else if (!selectedMessage) {
selectedMessage = MessagelistUserStore.find(
subMessage =>
subMessage &&
subMessage.folder === message.folder &&
subMessage.uid == message.uid
);
if (selectedMessage) {
MessagelistUserStore.selectedMessage(selectedMessage);
MessagelistUserStore.focusedMessage(selectedMessage);
}
}
}
}
}
}
preload || MessageUserStore.loading(false);
}, oMessage.folder, oMessage.uid);
}
};

View file

@ -1,14 +0,0 @@
export class AbstractComponent {
constructor() {
this.disposable = [];
}
dispose() {
this.disposable.forEach((funcToDispose) => {
if (funcToDispose && funcToDispose.dispose) {
funcToDispose.dispose();
}
});
}
}

View file

@ -1,13 +1,10 @@
import ko from 'ko'; import ko from 'ko';
import { AbstractComponent } from 'Component/Abstract';
class AbstractCheckbox extends AbstractComponent { export class AbstractCheckbox {
/** /**
* @param {Object} params = {} * @param {Object} params = {}
*/ */
constructor(params = {}) { constructor(params = {}) {
super();
this.value = ko.isObservable(params.value) ? params.value this.value = ko.isObservable(params.value) ? params.value
: ko.observable(!!params.value); : ko.observable(!!params.value);
@ -24,5 +21,3 @@ class AbstractCheckbox extends AbstractComponent {
this.enable() && this.value(!this.value()); this.enable() && this.value(!this.value());
} }
} }
export { AbstractCheckbox };

View file

@ -1,15 +1,14 @@
import ko from 'ko'; import ko from 'ko';
import { pInt } from 'Common/Utils'; import { pInt } from 'Common/Utils';
import { SaveSettingsStep } from 'Common/Enums'; import { SaveSettingsStep } from 'Common/Enums';
import { AbstractComponent } from 'Component/Abstract'; import { koComputable } from 'External/ko';
import { dispose } from 'External/ko';
class AbstractInput extends AbstractComponent { export class AbstractInput {
/** /**
* @param {Object} params * @param {Object} params
*/ */
constructor(params) { constructor(params) {
super();
this.value = params.value || ''; this.value = params.value || '';
this.label = params.label || ''; this.label = params.label || '';
this.enable = null == params.enable ? true : params.enable; this.enable = null == params.enable ? true : params.enable;
@ -18,7 +17,7 @@ class AbstractInput extends AbstractComponent {
this.labeled = null != params.label; this.labeled = null != params.label;
let size = params.size || 0; let size = 0 < params.size ? 'span' + params.size : '';
if (this.trigger) { if (this.trigger) {
const const
classForTrigger = ko.observable(''), classForTrigger = ko.observable(''),
@ -38,17 +37,21 @@ class AbstractInput extends AbstractComponent {
setTriggerState(this.trigger()); setTriggerState(this.trigger());
this.className = ko.computed(() => this.className = koComputable(() =>
((0 < size ? 'span' + size : '') + ' settings-saved-trigger-input ' + classForTrigger()).trim() (size + ' settings-saved-trigger-input ' + classForTrigger()).trim()
); );
this.disposable.push(this.trigger.subscribe(setTriggerState, this)); this.disposables = [
this.trigger.subscribe(setTriggerState, this),
this.className
];
} else { } else {
this.className = ko.computed(() => 0 < size ? 'span' + size : ''); this.className = size;
this.disposables = [];
} }
}
this.disposable.push(this.className); dispose() {
this.disposables.forEach(dispose);
} }
} }
export { AbstractInput };

View file

@ -1,5 +1,4 @@
import { doc, createElement } from 'Common/Globals'; import { doc, createElement, addEventsListeners } from 'Common/Globals';
import { isArray } from 'Common/Utils';
import { EmailModel } from 'Model/Email'; import { EmailModel } from 'Model/Email';
const contentType = 'snappymail/emailaddress', const contentType = 'snappymail/emailaddress',
@ -17,7 +16,7 @@ export class EmailAddressesComponent {
doc.body.append(datalist); doc.body.append(datalist);
} }
var self = this, const self = this,
// In Chrome we have no access to dataTransfer.getData unless it's the 'drop' event // In Chrome we have no access to dataTransfer.getData unless it's the 'drop' event
// In Chrome Mobile dataTransfer.types.includes(contentType) fails, only text/plain is set // In Chrome Mobile dataTransfer.types.includes(contentType) fails, only text/plain is set
validDropzone = () => dragAddress && dragAddress.li.parentNode !== self.ul, validDropzone = () => dragAddress && dragAddress.li.parentNode !== self.ul,
@ -42,49 +41,55 @@ export class EmailAddressesComponent {
// Create the elements // Create the elements
self.ul = createElement('ul',{class:"emailaddresses"}); self.ul = createElement('ul',{class:"emailaddresses"});
self.ul.addEventListener('click', e => self._focus(e)); addEventsListeners(self.ul, {
self.ul.addEventListener('dblclick', e => self._editTag(e)); click: e => self._focus(e),
self.ul.addEventListener("dragenter", fnDrag); dblclick: e => self._editTag(e),
self.ul.addEventListener("dragover", fnDrag); dragenter: fnDrag,
self.ul.addEventListener("drop", e => { dragover: fnDrag,
if (validDropzone() && dragAddress.value) { drop: e => {
e.preventDefault(); if (validDropzone() && dragAddress.value) {
dragAddress.source._removeDraggedTag(dragAddress.li); e.preventDefault();
self._parseValue(dragAddress.value); dragAddress.source._removeDraggedTag(dragAddress.li);
self._parseValue(dragAddress.value);
}
} }
}); });
self.input = createElement('input',{type:"text", list:datalist.id, self.input = createElement('input',{type:"text", list:datalist.id,
autocomplete:"off", autocorrect:"off", autocapitalize:"off", spellcheck:"false"}); autocomplete:"off", autocorrect:"off", autocapitalize:"off", spellcheck:"false"});
self.input.addEventListener('focus', () => self._focusTrigger(true)); addEventsListeners(self.input, {
self.input.addEventListener('blur', () => { focus: () => {
// prevent autoComplete menu click from causing a false 'blur' self._focusTrigger(true);
self._parseInput(true); self.input.value || self._resetDatalist();
self._focusTrigger(false); },
}); blur: () => {
self.input.addEventListener('keydown', e => { // prevent autoComplete menu click from causing a false 'blur'
if ('Backspace' === e.key || 'ArrowLeft' === e.key) {
// if our input contains no value and backspace has been pressed, select the last tag
var lastTag = self.inputCont.previousElementSibling,
input = self.input;
if (lastTag && (!input.value
|| (('selectionStart' in input) && input.selectionStart === 0 && input.selectionEnd === 0))
) {
e.preventDefault();
lastTag.querySelector('a').focus();
}
self._updateDatalist();
} else if (e.key == 'Enter') {
e.preventDefault();
self._parseInput(true); self._parseInput(true);
self._focusTrigger(false);
},
keydown: e => {
if ('Backspace' === e.key || 'ArrowLeft' === e.key) {
// if our input contains no value and backspace has been pressed, select the last tag
var lastTag = self.inputCont.previousElementSibling,
input = self.input;
if (lastTag && (!input.value
|| (('selectionStart' in input) && input.selectionStart === 0 && input.selectionEnd === 0))
) {
e.preventDefault();
lastTag.querySelector('a').focus();
}
self._updateDatalist();
} else if (e.key == 'Enter') {
e.preventDefault();
self._parseInput(true);
}
},
input: () => {
self._parseInput();
self._updateDatalist();
} }
}); });
self.input.addEventListener('input', () => {
self._parseInput();
self._updateDatalist();
});
self.input.addEventListener('focus', () => self.input.value || self._resetDatalist());
// define starting placeholder // define starting placeholder
if (element.placeholder) { if (element.placeholder) {
@ -138,20 +143,56 @@ export class EmailAddressesComponent {
_parseValue(val) { _parseValue(val) {
if (val) { if (val) {
var self = this, const self = this,
values = []; v = val.trim(),
hook = (v && [',', ';', '\n'].includes(v.slice(-1))) ? EmailModel.splitEmailLine(val) : null,
const v = val.trim(), values = (hook || [val]).map(value => EmailModel.parseEmailLine(value))
hook = (v && [',', ';', '\n'].includes(v.substr(-1))) .flat(Infinity)
? EmailModel.splitEmailLine(val) .map(item => (item.toLine ? [item.toLine(false), item] : [item, null]));
: null;
values = (hook || [val]).map(value => EmailModel.parseEmailLine(value))
.flat(Infinity)
.map(item => (item.toLine ? [item.toLine(false), item] : [item, null]));
if (values.length) { if (values.length) {
self._setChosen(values); values.forEach(a => {
var v = a[0].trim(),
exists = false,
lastIndex = -1,
obj = {
key : '',
obj : null,
value : ''
};
self._chosenValues.forEach((vv, kk) => {
if (vv.value === self._lastEdit) {
lastIndex = kk;
}
vv.value === v && (exists = true);
});
if (v !== '' && a[1] && !exists) {
obj.key = 'mi_' + Math.random().toString( 16 ).slice( 2, 10 );
obj.value = v;
obj.obj = a[1];
if (-1 < lastIndex) {
self._chosenValues.splice(lastIndex, 0, obj);
} else {
self._chosenValues.push(obj);
}
self._lastEdit = '';
self._renderTags();
}
});
if (values.length === 1 && values[0] === '' && self._lastEdit !== '') {
self._lastEdit = '';
self._renderTags();
}
self._setValue(self._buildValue());
return true; return true;
} }
} }
@ -202,56 +243,6 @@ export class EmailAddressesComponent {
self._resizeInput(ev); self._resizeInput(ev);
} }
_setChosen(valArr) {
var self = this;
if (!isArray(valArr)){
return false;
}
valArr.forEach(a => {
var v = a[0].trim(),
exists = false,
lastIndex = -1,
obj = {
key : '',
obj : null,
value : ''
};
self._chosenValues.forEach((vv, kk) => {
if (vv.value === self._lastEdit) {
lastIndex = kk;
}
vv.value === v && (exists = true);
});
if (v !== '' && a && a[1] && !exists) {
obj.key = 'mi_' + Math.random().toString( 16 ).slice( 2, 10 );
obj.value = v;
obj.obj = a[1];
if (-1 < lastIndex) {
self._chosenValues.splice(lastIndex, 0, obj);
} else {
self._chosenValues.push(obj);
}
self._lastEdit = '';
self._renderTags();
}
});
if (valArr.length === 1 && valArr[0] === '' && self._lastEdit !== '') {
self._lastEdit = '';
self._renderTags();
}
self._setValue(self._buildValue());
}
_buildValue() { _buildValue() {
return this._chosenValues.map(v => v.value).join(','); return this._chosenValues.map(v => v.value).join(',');
} }
@ -276,66 +267,70 @@ export class EmailAddressesComponent {
el = createElement('a',{href:'#', class:'ficon'}); el = createElement('a',{href:'#', class:'ficon'});
el.append('✖'); el.append('✖');
el.addEventListener('click', e => self._removeTag(e, li)); addEventsListeners(el, {
el.addEventListener('focus', () => li.className = 'emailaddresses-selected'); click: e => self._removeTag(e, li),
el.addEventListener('blur', () => li.className = null); focus: () => li.className = 'emailaddresses-selected',
el.addEventListener('keydown', e => { blur: () => li.className = null,
switch (e.key) { keydown: e => {
case 'Delete': switch (e.key) {
case 'Backspace': case 'Delete':
self._removeTag(e, li); case 'Backspace':
break; self._removeTag(e, li);
break;
// 'e' - edit tag (removes tag and places value into visible input // 'e' - edit tag (removes tag and places value into visible input
case 'e': case 'e':
case 'Enter': case 'Enter':
self._editTag(e); self._editTag(e);
break; break;
case 'ArrowLeft': case 'ArrowLeft':
// select the previous tag or input if no more tags exist // select the previous tag or input if no more tags exist
var previous = el.closest('li').previousElementSibling; var previous = el.closest('li').previousElementSibling;
if (previous.matches('li')) { if (previous.matches('li')) {
previous.querySelector('a').focus(); previous.querySelector('a').focus();
} else { } else {
self.focus(); self.focus();
} }
break; break;
case 'ArrowRight': case 'ArrowRight':
// select the next tag or input if no more tags exist // select the next tag or input if no more tags exist
var next = el.closest('li').nextElementSibling; var next = el.closest('li').nextElementSibling;
if (next !== this.inputCont) { if (next !== this.inputCont) {
next.querySelector('a').focus(); next.querySelector('a').focus();
} else { } else {
this.focus(); this.focus();
} }
break; break;
case 'ArrowDown': case 'ArrowDown':
self._focus(e); self._focus(e);
break; break;
}
} }
}); });
li.append(el); li.append(el);
li.emailaddress = v; li.emailaddress = v;
li.addEventListener("dragstart", e => { addEventsListeners(li, {
dragAddress = { dragstart: e => {
source: self, dragAddress = {
li: li, source: self,
value: li.emailaddress.obj.toLine() li: li,
}; value: li.emailaddress.obj.toLine()
// e.dataTransfer.setData(contentType, li.emailaddress.obj.toLine()); };
e.dataTransfer.setData('text/plain', contentType); // e.dataTransfer.setData(contentType, li.emailaddress.obj.toLine());
// e.dataTransfer.setDragImage(li, 0, 0); e.dataTransfer.setData('text/plain', contentType);
e.dataTransfer.effectAllowed = 'move'; // e.dataTransfer.setDragImage(li, 0, 0);
li.style.opacity = 0.25; e.dataTransfer.effectAllowed = 'move';
}); li.style.opacity = 0.25;
li.addEventListener("dragend", () => { },
dragAddress = null; dragend: () => {
li.style.cssText = ''; dragAddress = null;
li.style.cssText = '';
}
}); });
self.inputCont.before(li); self.inputCont.before(li);

View file

@ -1,28 +1,3 @@
import ko from 'ko';
import { AbstractCheckbox } from 'Component/AbstractCheckbox'; import { AbstractCheckbox } from 'Component/AbstractCheckbox';
export class CheckboxMaterialDesignComponent extends AbstractCheckbox { export class CheckboxMaterialDesignComponent extends AbstractCheckbox {}
/**
* @param {Object} params
*/
constructor(params) {
super(params);
this.animationBox = ko.observable(false).extend({ falseTimeout: 200 });
this.animationCheckmark = ko.observable(false).extend({ falseTimeout: 200 });
this.disposable.push(
this.value.subscribe(value => this.triggerAnimation(value), this)
);
}
triggerAnimation(box) {
if (box) {
this.animationBox(true);
setTimeout(()=>this.animationCheckmark(true), 200);
} else {
this.animationCheckmark(true);
setTimeout(()=>this.animationBox(true), 200);
}
}
}

View file

@ -1,10 +0,0 @@
import 'External/ko';
import ko from 'ko';
import { SaveSettingsStep } from 'Common/Enums';
// functions
ko.observable.fn.idleTrigger = function() {
this.trigger = ko.observable(SaveSettingsStep.Idle);
return this;
};

View file

@ -11,11 +11,14 @@ const
ctrlKey = shortcuts.getMetaKey() + ' + ', ctrlKey = shortcuts.getMetaKey() + ' + ',
tpl = doc.createElement('template'), createElement = name => doc.createElement(name),
clr = doc.createElement('input'),
tpl = createElement('template'),
clr = createElement('input'),
trimLines = html => html.trim().replace(/^(<div>\s*<br\s*\/?>\s*<\/div>)+/, '').trim(), trimLines = html => html.trim().replace(/^(<div>\s*<br\s*\/?>\s*<\/div>)+/, '').trim(),
clearHtmlLine = html => rl.Utils.htmlToPlain(html).trim(), htmlToPlain = html => rl.Utils.htmlToPlain(html).trim(),
plainToHtml = text => rl.Utils.plainToHtml(text),
getFragmentOfChildren = parent => { getFragmentOfChildren = parent => {
let frag = doc.createDocumentFragment(); let frag = doc.createDocumentFragment();
@ -33,13 +36,12 @@ const
addLinks: true // allow_smart_html_links addLinks: true // allow_smart_html_links
*/ */
sanitizeToDOMFragment: (html, isPaste/*, squire*/) => { sanitizeToDOMFragment: (html, isPaste/*, squire*/) => {
tpl.innerHTML = html tpl.innerHTML = (html||'')
.replace(/<\/?(BODY|HTML)[^>]*>/gi,'') .replace(/<\/?(BODY|HTML)[^>]*>/gi,'')
.replace(/<!--[^>]+-->/g,'') .replace(/<!--[^>]+-->/g,'')
.replace(/<span[^>]*>\s*<\/span>/gi,'') .replace(/<span[^>]*>\s*<\/span>/gi,'')
.trim(); .trim();
tpl.querySelectorAll('a:empty,span:empty').forEach(el => el.remove()); tpl.querySelectorAll('a:empty,span:empty').forEach(el => el.remove());
tpl.querySelectorAll('[data-x-div-type]').forEach(el => el.replaceWith(getFragmentOfChildren(el)));
if (isPaste) { if (isPaste) {
tpl.querySelectorAll(removeElements).forEach(el => el.remove()); tpl.querySelectorAll(removeElements).forEach(el => el.remove());
tpl.querySelectorAll('*').forEach(el => { tpl.querySelectorAll('*').forEach(el => {
@ -57,66 +59,6 @@ const
} }
return tpl.content; return tpl.content;
} }
},
rl_signature_replacer = (editor, text, signature, isHtml, insertBefore) => {
let
prevSignature = editor.__previous_signature,
skipInsert = false,
isEmptyText = false;
isEmptyText = !text.trim();
if (!isEmptyText && isHtml) {
isEmptyText = !clearHtmlLine(text);
}
if (prevSignature && !isEmptyText) {
if (isHtml && !prevSignature.isHtml) {
prevSignature = {
body: rl.Utils.plainToHtml(prevSignature.body),
isHtml: true
};
} else if (!isHtml && prevSignature.isHtml) {
prevSignature = {
body: rl.Utils.htmlToPlain(prevSignature.body),
isHtml: true
};
}
if (isHtml) {
var clearSig = clearHtmlLine(prevSignature.body);
text = text.replace(/<signature>([\s\S]*)<\/signature>/igm, all => {
var c = clearSig === clearHtmlLine(all);
if (!c) {
skipInsert = true;
}
return c ? '' : all;
});
} else {
var textLen = text.length;
text = text
.replace(prevSignature.body, '')
.replace(prevSignature.body, '');
skipInsert = textLen === text.length;
}
}
if (!skipInsert) {
signature = (isHtml ? '<br/><br/><signature>' : "\n\n") + signature + (isHtml ? '</signature>' : '');
text = insertBefore ? signature + text : text + signature;
if (10 < signature.length) {
prevSignature = {
body: signature,
isHtml: isHtml
};
}
}
editor.__previous_signature = prevSignature;
return text;
}; };
clr.type = "color"; clr.type = "color";
@ -127,9 +69,9 @@ class SquireUI
{ {
constructor(container) { constructor(container) {
const const
doClr = fn => () => { doClr = name => () => {
clr.value = ''; clr.value = '';
clr.onchange = () => squire[fn](clr.value); clr.onchange = () => squire.setStyle({[name]:clr.value});
clr.click(); clr.click();
}, },
@ -171,29 +113,15 @@ class SquireUI
colors: { colors: {
textColor: { textColor: {
html: 'A<sub>▾</sub>', html: 'A<sub>▾</sub>',
cmd: doClr('setTextColor'), cmd: doClr('color'),
hint: 'Text color' hint: 'Text color'
}, },
backgroundColor: { backgroundColor: {
html: '🎨', /* ▧ */ html: '🎨', /* ▧ */
cmd: doClr('setBackgroundColor'), cmd: doClr('backgroundColor'),
hint: 'Background color' hint: 'Background color'
}, },
}, },
/*
bidi: {
bdoLtr: {
html: '&lrm;𝐁',
cmd: () => this.doAction('bold','B'),
hint: 'Bold'
},
bdoRtl: {
html: '&rlm;𝐁',
cmd: () => this.doAction('bold','B'),
hint: 'Bold'
}
},
*/
inline: { inline: {
bold: { bold: {
html: 'B', html: 'B',
@ -318,10 +246,10 @@ class SquireUI
} }
}, },
plain = doc.createElement('textarea'), plain = createElement('textarea'),
wysiwyg = doc.createElement('div'), wysiwyg = createElement('div'),
toolbar = doc.createElement('div'), toolbar = createElement('div'),
browseImage = doc.createElement('input'), browseImage = createElement('input'),
squire = new Squire(wysiwyg, SquireDefaultConfig); squire = new Squire(wysiwyg, SquireDefaultConfig);
browseImage.type = 'file'; browseImage.type = 'file';
@ -356,17 +284,17 @@ class SquireUI
continue; continue;
} }
*/ */
let toolgroup = doc.createElement('div'); let toolgroup = createElement('div');
toolgroup.className = 'btn-group'; toolgroup.className = 'btn-group';
toolgroup.id = 'squire-toolgroup-'+group; toolgroup.id = 'squire-toolgroup-'+group;
for (action in actions[group]) { for (action in actions[group]) {
let cfg = actions[group][action], input, ev = 'click'; let cfg = actions[group][action], input, ev = 'click';
if (cfg.input) { if (cfg.input) {
input = doc.createElement('input'); input = createElement('input');
input.type = cfg.input; input.type = cfg.input;
ev = 'change'; ev = 'change';
} else if (cfg.select) { } else if (cfg.select) {
input = doc.createElement('select'); input = createElement('select');
input.className = 'btn'; input.className = 'btn';
if (Array.isArray(cfg.select)) { if (Array.isArray(cfg.select)) {
cfg.select.forEach(value => { cfg.select.forEach(value => {
@ -376,7 +304,7 @@ class SquireUI
}); });
} else { } else {
Object.entries(cfg.select).forEach(([label, options]) => { Object.entries(cfg.select).forEach(([label, options]) => {
let group = doc.createElement('optgroup'); let group = createElement('optgroup');
group.label = label; group.label = label;
Object.entries(options).forEach(([text, value]) => { Object.entries(options).forEach(([text, value]) => {
var option = new Option(text, value); var option = new Option(text, value);
@ -388,7 +316,7 @@ class SquireUI
} }
ev = 'input'; ev = 'input';
} else { } else {
input = doc.createElement('button'); input = createElement('button');
input.type = 'button'; input.type = 'button';
input.className = 'btn'; input.className = 'btn';
input.innerHTML = cfg.html; input.innerHTML = cfg.html;
@ -425,8 +353,8 @@ class SquireUI
changes.redo.input.disabled = !state.canRedo; changes.redo.input.disabled = !state.canRedo;
}); });
squire.addEventListener('focus', () => shortcuts.off()); // squire.addEventListener('focus', () => shortcuts.off());
squire.addEventListener('blur', () => shortcuts.on()); // squire.addEventListener('blur', () => shortcuts.on());
container.append(toolbar, wysiwyg, plain); container.append(toolbar, wysiwyg, plain);
@ -477,9 +405,9 @@ class SquireUI
let cl = this.container.classList; let cl = this.container.classList;
cl.remove('squire-mode-'+this.mode); cl.remove('squire-mode-'+this.mode);
if ('plain' == mode) { if ('plain' == mode) {
this.plain.value = rl.Utils.htmlToPlain(this.squire.getHTML(), true).trim(); this.plain.value = htmlToPlain(this.squire.getHTML(), true);
} else { } else {
this.setData(rl.Utils.plainToHtml(this.plain.value, true)); this.setData(plainToHtml(this.plain.value, true));
mode = 'wysiwyg'; mode = 'wysiwyg';
} }
this.mode = mode; // 'wysiwyg' or 'plain' this.mode = mode; // 'wysiwyg' or 'plain'
@ -509,19 +437,32 @@ class SquireUI
}, cfg); }, cfg);
if (cfg.clearCache) { if (cfg.clearCache) {
this.__previous_signature = null; this._prev_txt_sig = null;
} else try { } else try {
const signature = cfg.isHtml ? htmlToPlain(cfg.signature) : cfg.signature;
if ('plain' === this.mode) { if ('plain' === this.mode) {
if (cfg.isHtml) { let
cfg.signature = rl.Utils.htmlToPlain(cfg.signature); text = this.plain.value,
prevSignature = this._prev_txt_sig;
if (prevSignature) {
text = text.replace(prevSignature, '').trim();
} }
this.plain.value = rl_signature_replacer(this, this.plain.value, cfg.signature, false, cfg.insertBefore); this.plain.value = cfg.insertBefore ? '\n\n' + signature + '\n\n' + text : text + '\n\n' + signature;
} else { } else {
if (!cfg.isHtml) { const squire = this.squire,
cfg.signature = rl.Utils.plainToHtml(cfg.signature); root = squire.getRoot(),
} range = squire.getSelection(),
this.setData(rl_signature_replacer(this, this.getData(), cfg.signature, true, cfg.insertBefore)); div = createElement('div');
div.className = 'rl-signature';
div.innerHTML = cfg.isHtml ? cfg.signature : plainToHtml(cfg.signature);
root.querySelectorAll('div.rl-signature').forEach(node => node.remove());
cfg.insertBefore ? root.prepend(div) : root.append(div);
// Move cursor above signature
range.setStart(div, 0);
range.setEnd(div, 0);
squire.setSelection( range );
} }
this._prev_txt_sig = signature;
} catch (e) { } catch (e) {
console.error(e); console.error(e);
} }
@ -540,12 +481,6 @@ class SquireUI
focus() { focus() {
('plain' == this.mode ? this.plain : this.squire).focus(); ('plain' == this.mode ? this.plain : this.squire).focus();
} }
resize(width, height) {
height = Math.max(200, (height - this.wysiwyg.offsetTop)) + 'px';
this.wysiwyg.style.height = height;
this.plain.style.height = height;
}
} }
this.SquireUI = SquireUI; this.SquireUI = SquireUI;

View file

@ -1,11 +1,14 @@
import 'External/ko'; import 'External/ko';
import ko from 'ko'; import ko from 'ko';
import { HtmlEditor } from 'Common/Html'; import { HtmlEditor } from 'Common/Html';
import { timeToNode } from 'Common/Momentor'; import { timeToNode } from 'Common/Translator';
import { elementById } from 'Common/Globals'; import { elementById, addEventsListeners, dropdowns } from 'Common/Globals';
import { isArray } from 'Common/Utils'; import { isArray } from 'Common/Utils';
import { dropdownsDetectVisibility } from 'Common/UtilsUser';
import { EmailAddressesComponent } from 'Component/EmailAddresses'; import { EmailAddressesComponent } from 'Component/EmailAddresses';
import { ThemeStore } from 'Stores/Theme'; import { ThemeStore } from 'Stores/Theme';
import { moveMessagesToFolder } from 'Common/Folders';
import { setExpandedFolder } from 'Model/FolderCollection';
const rlContentType = 'snappymail/action', const rlContentType = 'snappymail/action',
@ -27,226 +30,223 @@ const rlContentType = 'snappymail/action',
id: 0, id: 0,
stop: () => clearTimeout(dragTimer.id), stop: () => clearTimeout(dragTimer.id),
start: fn => dragTimer.id = setTimeout(fn, 500) start: fn => dragTimer.id = setTimeout(fn, 500)
}; },
ttn = (element, fValueAccessor) => timeToNode(element, ko.unwrap(fValueAccessor()));
let dragImage, let dragImage,
dragData; dragData;
ko.bindingHandlers.editor = { Object.assign(ko.bindingHandlers, {
init: (element, fValueAccessor) => {
let editor = null;
const fValue = fValueAccessor(), editor: {
fUpdateEditorValue = () => fValue && fValue.__editor && fValue.__editor.setHtmlOrPlain(fValue()), init: (element, fValueAccessor) => {
fUpdateKoValue = () => fValue && fValue.__editor && fValue(fValue.__editor.getDataWithHtmlMark()), let editor = null;
fOnReady = () => {
fValue.__editor = editor;
fUpdateEditorValue();
};
if (ko.isObservable(fValue) && HtmlEditor) { const fValue = fValueAccessor(),
editor = new HtmlEditor(element, fUpdateKoValue, fOnReady, fUpdateKoValue); fUpdateEditorValue = () => fValue && fValue.__editor && fValue.__editor.setHtmlOrPlain(fValue()),
fUpdateKoValue = () => fValue && fValue.__editor && fValue(fValue.__editor.getDataWithHtmlMark()),
fOnReady = () => {
fValue.__editor = editor;
fUpdateEditorValue();
};
fValue.__fetchEditorValue = fUpdateKoValue; if (ko.isObservable(fValue) && HtmlEditor) {
editor = new HtmlEditor(element, fUpdateKoValue, fOnReady, fUpdateKoValue);
fValue.subscribe(fUpdateEditorValue); fValue.__fetchEditorValue = fUpdateKoValue;
// ko.utils.domNodeDisposal.addDisposeCallback(element, () => { fValue.subscribe(fUpdateEditorValue);
// });
}
}
};
let ttn = (element, fValueAccessor) => timeToNode(element, ko.unwrap(fValueAccessor())); // ko.utils.domNodeDisposal.addDisposeCallback(element, () => {
ko.bindingHandlers.moment = { // });
init: ttn, }
update: ttn
};
ko.bindingHandlers.emailsTags = {
init: (element, fValueAccessor, fAllBindings) => {
const fValue = fValueAccessor();
element.addresses = new EmailAddressesComponent(element, {
focusCallback: value => fValue.focused && fValue.focused(!!value),
autoCompleteSource: fAllBindings.get('autoCompleteSource'),
onChange: value => fValue(value)
});
if (fValue.focused && fValue.focused.subscribe) {
fValue.focused.subscribe(value =>
element.addresses[value ? 'focus' : 'blur']()
);
} }
}, },
update: (element, fValueAccessor) => {
element.addresses.value = ko.unwrap(fValueAccessor());
}
};
// Start dragging selected messages moment: {
ko.bindingHandlers.dragmessages = { init: ttn,
init: (element, fValueAccessor) => { update: ttn
element.addEventListener("dragstart", e => { },
let data = fValueAccessor()(e);
dragImage || (dragImage = elementById('messagesDragImage'));
if (data && dragImage && !ThemeStore.isMobile()) {
dragImage.querySelector('.text').textContent = data.uids.length;
let img = dragImage.querySelector('i');
img.classList.toggle('icon-copy', e.ctrlKey);
img.classList.toggle('icon-mail', !e.ctrlKey);
// Else Chrome doesn't show it emailsTags: {
dragImage.style.left = e.clientX + 'px'; init: (element, fValueAccessor, fAllBindings) => {
dragImage.style.top = e.clientY + 'px'; const fValue = fValueAccessor();
dragImage.style.right = 'auto';
setDragAction(e, 'messages', e.ctrlKey ? 'copy' : 'move', data, dragImage); element.addresses = new EmailAddressesComponent(element, {
focusCallback: value => fValue.focused && fValue.focused(!!value),
autoCompleteSource: fAllBindings.get('autoCompleteSource'),
onChange: value => fValue(value)
});
// Remove the Chrome visibility if (fValue.focused && fValue.focused.subscribe) {
dragImage.style.cssText = ''; fValue.focused.subscribe(value =>
} else { element.addresses[value ? 'focus' : 'blur']()
e.preventDefault(); );
} }
},
update: (element, fValueAccessor) => {
element.addresses.value = ko.unwrap(fValueAccessor());
}
},
}, false); // Start dragging selected messages
element.addEventListener("dragend", () => dragData = null); dragmessages: {
element.setAttribute('draggable', true); init: (element, fValueAccessor) => {
} element.addEventListener("dragstart", e => {
}; let data = fValueAccessor()(e);
dragImage || (dragImage = elementById('messagesDragImage'));
if (data && dragImage && !ThemeStore.isMobile()) {
dragImage.querySelector('.text').textContent = data.uids.length;
let img = dragImage.querySelector('i');
img.classList.toggle('icon-copy', e.ctrlKey);
img.classList.toggle('icon-mail', !e.ctrlKey);
// Drop selected messages on folder // Else Chrome doesn't show it
ko.bindingHandlers.dropmessages = { dragImage.style.left = e.clientX + 'px';
init: (element, fValueAccessor) => { dragImage.style.top = e.clientY + 'px';
const folder = fValueAccessor(), dragImage.style.right = 'auto';
// folder = ko.dataFor(element),
fnStop = e => {
e.preventDefault();
element.classList.remove('droppableHover');
dragTimer.stop();
},
fnHover = e => {
if ('messages' === getDragAction(e)) {
fnStop(e);
element.classList.add('droppableHover');
if (folder && folder.collapsed()) {
dragTimer.start(() => {
folder.collapsed(false);
rl.app.setExpandedFolder(folder.fullNameHash, true);
}, 500);
}
}
};
element.addEventListener("dragenter", fnHover);
element.addEventListener("dragover", fnHover);
element.addEventListener("dragleave", fnStop);
element.addEventListener("drop", e => {
fnStop(e);
if ('messages' === getDragAction(e) && ['move','copy'].includes(e.dataTransfer.effectAllowed)) {
let data = dragData.data;
if (folder && data && data.folder && isArray(data.uids)) {
rl.app.moveMessagesToFolder(data.folder, data.uids, folder.fullNameRaw, data.copy && e.ctrlKey);
}
}
});
}
};
ko.bindingHandlers.sortableItem = { setDragAction(e, 'messages', e.ctrlKey ? 'copy' : 'move', data, dragImage);
init: (element, fValueAccessor) => {
let options = ko.unwrap(fValueAccessor()) || {}, // Remove the Chrome visibility
parent = element.parentNode, dragImage.style.cssText = '';
fnHover = e => { } else {
if ('sortable' === getDragAction(e)) {
e.preventDefault(); e.preventDefault();
let node = (e.target.closest ? e.target : e.target.parentNode).closest('[draggable]'); }
if (node && node !== dragData.data && parent.contains(node)) {
let rect = node.getBoundingClientRect(); }, false);
if (rect.top + (rect.height / 2) <= e.clientY) { element.addEventListener("dragend", () => dragData = null);
if (node.nextElementSibling !== dragData.data) { element.setAttribute('draggable', true);
node.after(dragData.data); }
} },
} else if (node.previousElementSibling !== dragData.data) {
node.before(dragData.data); // Drop selected messages on folder
dropmessages: {
init: (element, fValueAccessor) => {
const folder = fValueAccessor(),
// folder = ko.dataFor(element),
fnStop = e => {
e.preventDefault();
element.classList.remove('droppableHover');
dragTimer.stop();
},
fnHover = e => {
if ('messages' === getDragAction(e)) {
fnStop(e);
element.classList.add('droppableHover');
if (folder && folder.collapsed()) {
dragTimer.start(() => {
folder.collapsed(false);
setExpandedFolder(folder.fullName, true);
}, 500);
}
}
};
addEventsListeners(element, {
dragenter: fnHover,
dragover: fnHover,
dragleave: fnStop,
drop: e => {
fnStop(e);
if ('messages' === getDragAction(e) && ['move','copy'].includes(e.dataTransfer.effectAllowed)) {
let data = dragData.data;
if (folder && data && data.folder && isArray(data.uids)) {
moveMessagesToFolder(data.folder, data.uids, folder.fullName, data.copy && e.ctrlKey);
} }
} }
} }
};
element.addEventListener("dragstart", e => {
dragData = {
action: 'sortable',
element: element
};
setDragAction(e, 'sortable', 'move', element, element);
element.style.opacity = 0.25;
});
element.addEventListener("dragend", e => {
element.style.opacity = null;
if ('sortable' === getDragAction(e)) {
dragData.data.style.cssText = '';
let row = parent.rows[options.list.indexOf(ko.dataFor(element))];
if (row != dragData.data) {
row.before(dragData.data);
}
dragData = null;
}
});
if (!parent.sortable) {
parent.sortable = true;
parent.addEventListener("dragenter", fnHover);
parent.addEventListener("dragover", fnHover);
parent.addEventListener("drop", e => {
if ('sortable' === getDragAction(e)) {
e.preventDefault();
let data = ko.dataFor(dragData.data),
from = options.list.indexOf(data),
to = [...parent.children].indexOf(dragData.data);
if (from != to) {
let arr = options.list();
arr.splice(to, 0, ...arr.splice(from, 1));
options.list(arr);
}
dragData = null;
options.afterMove && options.afterMove();
}
}); });
} }
} },
};
ko.bindingHandlers.initDom = { sortableItem: {
init: (element, fValueAccessor) => fValueAccessor()(element) init: (element, fValueAccessor) => {
}; let options = ko.unwrap(fValueAccessor()) || {},
parent = element.parentNode,
ko.bindingHandlers.onEsc = { fnHover = e => {
init: (element, fValueAccessor, fAllBindings, viewModel) => { if ('sortable' === getDragAction(e)) {
let fn = event => { e.preventDefault();
if ('Escape' == event.key) { let node = (e.target.closest ? e.target : e.target.parentNode).closest('[draggable]');
element.dispatchEvent(new Event('change')); if (node && node !== dragData.data && parent.contains(node)) {
fValueAccessor().call(viewModel); let rect = node.getBoundingClientRect();
if (rect.top + (rect.height / 2) <= e.clientY) {
if (node.nextElementSibling !== dragData.data) {
node.after(dragData.data);
}
} else if (node.previousElementSibling !== dragData.data) {
node.before(dragData.data);
}
}
}
};
addEventsListeners(element, {
dragstart: e => {
dragData = {
action: 'sortable',
element: element
};
setDragAction(e, 'sortable', 'move', element, element);
element.style.opacity = 0.25;
},
dragend: e => {
element.style.opacity = null;
if ('sortable' === getDragAction(e)) {
dragData.data.style.cssText = '';
let row = parent.rows[options.list.indexOf(ko.dataFor(element))];
if (row != dragData.data) {
row.before(dragData.data);
}
dragData = null;
}
}
});
if (!parent.sortable) {
parent.sortable = true;
addEventsListeners(parent, {
dragenter: fnHover,
dragover: fnHover,
drop: e => {
if ('sortable' === getDragAction(e)) {
e.preventDefault();
let data = ko.dataFor(dragData.data),
from = options.list.indexOf(data),
to = [...parent.children].indexOf(dragData.data);
if (from != to) {
let arr = options.list();
arr.splice(to, 0, ...arr.splice(from, 1));
options.list(arr);
}
dragData = null;
options.afterMove && options.afterMove();
}
}
});
} }
}; }
element.addEventListener('keyup', fn); },
ko.utils.domNodeDisposal.addDisposeCallback(element, () => element.removeEventListener('keyup', fn));
}
};
ko.bindingHandlers.registerBootstrapDropdown = { initDom: {
init: element => { init: (element, fValueAccessor) => fValueAccessor()(element)
rl.Dropdowns.register(element); },
element.ddBtn = new BSN.Dropdown(element.querySelector('.dropdown-toggle'));
}
};
ko.bindingHandlers.openDropdownTrigger = { registerBootstrapDropdown: {
update: (element, fValueAccessor) => { init: element => {
if (ko.unwrap(fValueAccessor())) { dropdowns.push(element);
const el = element.ddBtn; element.ddBtn = new BSN.Dropdown(element.querySelector('.dropdown-toggle'));
el.open || el.toggle(); }
// el.focus(); },
rl.Dropdowns.detectVisibility(); openDropdownTrigger: {
fValueAccessor()(false); update: (element, fValueAccessor) => {
if (ko.unwrap(fValueAccessor())) {
const el = element.ddBtn;
el.open || el.toggle();
// el.focus();
dropdownsDetectVisibility();
fValueAccessor()(false);
}
} }
} }
}; });

280
dev/External/ko.js vendored
View file

@ -1,157 +1,171 @@
import ko from 'ko';
import { i18nToNodes } from 'Common/Translator'; import { i18nToNodes } from 'Common/Translator';
import { doc, createElement } from 'Common/Globals'; import { doc, createElement } from 'Common/Globals';
import { SaveSettingsStep } from 'Common/Enums'; import { SaveSettingsStep } from 'Common/Enums';
import { arrayLength, isFunction } from 'Common/Utils'; import { arrayLength, isFunction, forEachObjectEntry } from 'Common/Utils';
const export const
koValue = value => !ko.isObservable(value) && isFunction(value) ? value() : ko.unwrap(value); errorTip = (element, value) => value
? setTimeout(() => element.setAttribute('data-rainloopErrorTip', value), 100)
: element.removeAttribute('data-rainloopErrorTip'),
ko.bindingHandlers.tooltipErrorTip = { /**
init: element => { * The value of the pureComputed observable shouldnt vary based on the
doc.addEventListener('click', () => element.removeAttribute('data-rainloopErrorTip')); * number of evaluations or other hidden information. Its value should be
}, * based solely on the values of other observables in the application
update: (element, fValueAccessor) => { */
const value = koValue(fValueAccessor()); koComputable = fn => ko.computed(fn, {'pure':true}),
if (value) {
setTimeout(() => element.setAttribute('data-rainloopErrorTip', value), 100); addObservablesTo = (target, observables) =>
} else { forEachObjectEntry(observables, (key, value) =>
element.removeAttribute('data-rainloopErrorTip'); target[key] || (target[key] = /*isArray(value) ? ko.observableArray(value) :*/ ko.observable(value)) ),
addComputablesTo = (target, computables) =>
forEachObjectEntry(computables, (key, fn) => target[key] = koComputable(fn)),
addSubscribablesTo = (target, subscribables) =>
forEachObjectEntry(subscribables, (key, fn) => target[key].subscribe(fn)),
dispose = disposable => disposable && isFunction(disposable.dispose) && disposable.dispose(),
// With this we don't need delegateRunOnDestroy
koArrayWithDestroy = data => {
data = ko.observableArray(data);
data.subscribe(changes =>
changes.forEach(item =>
'deleted' === item.status && null == item.moved && item.value.onDestroy && item.value.onDestroy()
)
, data, 'arrayChange');
return data;
};
Object.assign(ko.bindingHandlers, {
tooltipErrorTip: {
init: (element, fValueAccessor) => {
doc.addEventListener('click', () => {
let value = fValueAccessor();
ko.isObservable(value) && !ko.isComputed(value) && value('');
errorTip(element);
});
},
update: (element, fValueAccessor) => {
let value = ko.unwrap(fValueAccessor());
value = isFunction(value) ? value() : value;
errorTip(element, value);
} }
} },
};
ko.bindingHandlers.onEnter = { onEnter: {
init: (element, fValueAccessor, fAllBindings, viewModel) => { init: (element, fValueAccessor, fAllBindings, viewModel) => {
let fn = event => { let fn = event => {
if ('Enter' == event.key) { if ('Enter' == event.key) {
element.dispatchEvent(new Event('change')); element.dispatchEvent(new Event('change'));
fValueAccessor().call(viewModel); fValueAccessor().call(viewModel);
}
};
element.addEventListener('keydown', fn);
ko.utils.domNodeDisposal.addDisposeCallback(element, () => element.removeEventListener('keydown', fn));
}
},
onSpace: {
init: (element, fValueAccessor, fAllBindings, viewModel) => {
let fn = event => {
if (' ' == event.key) {
fValueAccessor().call(viewModel, event);
}
};
element.addEventListener('keyup', fn);
ko.utils.domNodeDisposal.addDisposeCallback(element, () => element.removeEventListener('keyup', fn));
}
},
i18nInit: {
init: element => i18nToNodes(element)
},
i18nUpdate: {
update: (element, fValueAccessor) => {
ko.unwrap(fValueAccessor());
i18nToNodes(element);
}
},
title: {
update: (element, fValueAccessor) => element.title = ko.unwrap(fValueAccessor())
},
command: {
init: (element, fValueAccessor, fAllBindings, viewModel, bindingContext) => {
const command = fValueAccessor();
if (!command || !command.canExecute) {
throw new Error('Value should be a command');
} }
};
element.addEventListener('keydown', fn);
ko.utils.domNodeDisposal.addDisposeCallback(element, () => element.removeEventListener('keydown', fn));
}
};
ko.bindingHandlers.onSpace = { ko.bindingHandlers['FORM'==element.nodeName ? 'submit' : 'click'].init(
init: (element, fValueAccessor, fAllBindings, viewModel) => { element,
let fn = event => { fValueAccessor,
if (' ' == event.key) { fAllBindings,
fValueAccessor().call(viewModel, event); viewModel,
bindingContext
);
},
update: (element, fValueAccessor) => {
const cl = element.classList,
command = fValueAccessor();
let disabled = !command.canExecute();
cl.toggle('disabled', disabled);
if (element.matches('INPUT,TEXTAREA,BUTTON')) {
element.disabled = disabled;
} }
};
element.addEventListener('keyup', fn);
ko.utils.domNodeDisposal.addDisposeCallback(element, () => element.removeEventListener('keyup', fn));
}
};
ko.bindingHandlers.modal = {
init: (element, fValueAccessor) => {
const close = element.querySelector('.close'),
click = () => fValueAccessor()(false);
close && close.addEventListener('click.koModal', click);
ko.utils.domNodeDisposal.addDisposeCallback(element, () =>
close.removeEventListener('click.koModal', click)
);
}
};
ko.bindingHandlers.i18nInit = {
init: element => i18nToNodes(element)
};
ko.bindingHandlers.i18nUpdate = {
update: (element, fValueAccessor) => {
ko.unwrap(fValueAccessor());
i18nToNodes(element);
}
};
ko.bindingHandlers.title = {
update: (element, fValueAccessor) => element.title = ko.unwrap(fValueAccessor())
};
ko.bindingHandlers.command = {
init: (element, fValueAccessor, fAllBindings, viewModel, bindingContext) => {
const command = fValueAccessor();
if (!command || !command.enabled || !command.canExecute) {
throw new Error('Value should be a command');
} }
ko.bindingHandlers['FORM'==element.nodeName ? 'submit' : 'click'].init(
element,
fValueAccessor,
fAllBindings,
viewModel,
bindingContext
);
}, },
update: (element, fValueAccessor) => {
const cl = element.classList,
command = fValueAccessor();
let disabled = !command.enabled(); saveTrigger: {
init: (element) => {
disabled = disabled || !command.canExecute(); let icon = element;
cl.toggle('disabled', disabled); if (element.matches('input,select,textarea')) {
element.classList.add('settings-saved-trigger-input');
if (element.matches('INPUT,TEXTAREA,BUTTON')) { element.after(element.saveTriggerIcon = icon = createElement('span'));
element.disabled = disabled; }
} icon.classList.add('settings-save-trigger');
} },
}; update: (element, fValueAccessor) => {
const value = parseInt(ko.unwrap(fValueAccessor()),10);
ko.bindingHandlers.saveTrigger = { let cl = (element.saveTriggerIcon || element).classList;
init: (element) => { if (element.saveTriggerIcon) {
let icon = element; cl.toggle('saving', value === SaveSettingsStep.Animate);
if (element.matches('input,select,textarea')) { cl.toggle('success', value === SaveSettingsStep.TrueResult);
element.classList.add('settings-saved-trigger-input'); cl.toggle('error', value === SaveSettingsStep.FalseResult);
element.after(element.saveTriggerIcon = icon = createElement('span')); }
} cl = element.classList;
icon.classList.add('settings-save-trigger');
},
update: (element, fValueAccessor) => {
const value = parseInt(ko.unwrap(fValueAccessor()),10);
let cl = (element.saveTriggerIcon || element).classList;
if (element.saveTriggerIcon) {
cl.toggle('saving', value === SaveSettingsStep.Animate);
cl.toggle('success', value === SaveSettingsStep.TrueResult); cl.toggle('success', value === SaveSettingsStep.TrueResult);
cl.toggle('error', value === SaveSettingsStep.FalseResult); cl.toggle('error', value === SaveSettingsStep.FalseResult);
} }
cl = element.classList;
cl.toggle('success', value === SaveSettingsStep.TrueResult);
cl.toggle('error', value === SaveSettingsStep.FalseResult);
} }
}; });
// extenders // extenders
ko.extenders.limitedList = (target, limitedList) => { ko.extenders.limitedList = (target, limitedList) => {
const result = ko const result = ko
.computed({ .computed({
read: target, read: target,
write: (newValue) => { write: newValue => {
const currentValue = ko.unwrap(target), let currentValue = target(),
list = ko.unwrap(limitedList); list = ko.unwrap(limitedList);
list = arrayLength(list) ? list : [''];
if (arrayLength(list)) { if (!list.includes(newValue)) {
if (list.includes(newValue)) { newValue = list.includes(currentValue, list) ? currentValue : list[0];
target(newValue); target(newValue + ' ');
} else if (list.includes(currentValue, list)) {
target(currentValue + ' ');
target(currentValue);
} else {
target(list[0] + ' ');
target(list[0]);
}
} else {
target('');
}
} }
}) target(newValue);
.extend({ notify: 'always' }); }
})
.extend({ notify: 'always' });
result(target()); result(target());
@ -184,6 +198,6 @@ ko.extenders.falseTimeout = (target, option) => {
// functions // functions
ko.observable.fn.deleteAccessHelper = function() { ko.observable.fn.askDeleteHelper = function() {
return this.extend({ falseTimeout: 3000, toggleSubscribeProperty: [this, 'deleteAccess'] }); return this.extend({ falseTimeout: 3000, toggleSubscribeProperty: [this, 'askDelete'] });
}; };

View file

@ -1,31 +1,24 @@
<html> <html>
<head> <head>
<meta charset="utf-8"/> <meta charset="utf-8"/>
<meta name="viewport" content="user-scalable=no"/> <title></title>
<title>{{subject}}</title>
<style> <style>
html, body { html, body {
background-color: #fff;
color: #000;
font-size: 13px;
margin: 0; margin: 0;
padding: 0; padding: 0;
} }
header { header {
background: #eee; background: rgba(125,128,128,0.3);
border-bottom: 1px solid #ccc; border-bottom: 1px solid #888;
padding: 0 15px;
} }
header h1 { header h1 {
font-size: 16px; font-size: 120%;
margin: 0;
} }
header * { header * {
font-size: 12px; margin: 5px 0;
padding: 5px 0;
} }
header time { header time {
@ -33,30 +26,21 @@ header time {
} }
blockquote { blockquote {
border-left: 2px solid black; border-left: 2px solid rgba(125,128,128,0.5);
margin: 0; margin: 0;
padding: 0 10px; padding: 0 0 0 10px;
} }
pre, .body-wrp.plain { pre {
white-space: pre-wrap; white-space: pre-wrap;
word-wrap: break-word; word-wrap: break-word;
word-break: normal; word-break: normal;
} }
.body-wrp { body > * {
padding: 15px; padding: 0.5em 1em;
} }
</style> </style>
</head> </head>
<body> <body></body>
<header>
<h1>{{subject}}</h1>
<time>{{date}}</time>
<div>{{fromCreds}}</div>
<div>{{toLabel}}: {{toCreds}}</div>
<div {{ccHide}}>{{ccLabel}}: {{ccCreds}}</div>
</header>
<div class="body-wrp {{bodyClass}}">{{html}}</div>
</body>
</html> </html>

View file

@ -1,10 +1,5 @@
import { isArray, isFunction, addObservablesTo, addComputablesTo } from 'Common/Utils'; import { isArray, forEachObjectValue, forEachObjectEntry } from 'Common/Utils';
import { dispose, addObservablesTo, addComputablesTo } from 'External/ko';
function dispose(disposable) {
if (disposable && isFunction(disposable.dispose)) {
disposable.dispose();
}
}
function typeCast(curValue, newValue) { function typeCast(curValue, newValue) {
if (null != curValue) { if (null != curValue) {
@ -31,7 +26,7 @@ export class AbstractModel {
throw new Error("Can't instantiate AbstractModel!"); throw new Error("Can't instantiate AbstractModel!");
} }
*/ */
this.subscribables = []; this.disposables = [];
} }
addObservables(observables) { addObservables(observables) {
@ -43,25 +38,26 @@ export class AbstractModel {
} }
addSubscribables(subscribables) { addSubscribables(subscribables) {
Object.entries(subscribables).forEach(([key, fn]) => this.subscribables.push( this[key].subscribe(fn) ) ); // addSubscribablesTo(this, subscribables);
forEachObjectEntry(subscribables, (key, fn) => this.disposables.push( this[key].subscribe(fn) ) );
} }
/** Called by delegateRunOnDestroy */ /** Called by delegateRunOnDestroy */
onDestroy() { onDestroy() {
/** dispose ko subscribables */ /** dispose ko subscribables */
this.subscribables.forEach(dispose); this.disposables.forEach(dispose);
/** clear object entries */ /** clear object entries */
// Object.entries(this).forEach(([key, value]) => { // forEachObjectEntry(this, (key, value) => {
Object.values(this).forEach(value => { forEachObjectValue(this, value => {
/** clear CollectionModel */ /** clear CollectionModel */
let arr = ko.isObservableArray(value) ? value() : value; let arr = ko.isObservableArray(value) ? value() : value;
arr && arr.onDestroy && value.onDestroy(); arr && arr.onDestroy && arr.onDestroy();
/** destroy ko.observable/ko.computed? */ /** destroy ko.observable/ko.computed? */
dispose(value); // dispose(value);
/** clear object value */ /** clear object value */
// this[key] = null; // TODO: issue with Contacts view // this[key] = null; // TODO: issue with Contacts view
}); });
// this.subscribables = []; // this.disposables = [];
} }
/** /**
@ -89,9 +85,9 @@ export class AbstractModel {
if (!model.validJson(json)) { if (!model.validJson(json)) {
return false; return false;
} }
Object.entries(json).forEach(([key, value]) => { forEachObjectEntry(json, (key, value) => {
if ('@' !== key[0]) try { if ('@' !== key[0]) try {
key = key[0].toLowerCase() + key.substr(1); key = key[0].toLowerCase() + key.slice(1);
switch (typeof this[key]) switch (typeof this[key])
{ {
case 'function': case 'function':

View file

@ -2,23 +2,9 @@ import { isArray, arrayLength } from 'Common/Utils';
export class AbstractScreen { export class AbstractScreen {
constructor(screenName, viewModels = []) { constructor(screenName, viewModels = []) {
this.oCross = null; this.__cross = null;
this.sScreenName = screenName; this.screenName = screenName;
this.aViewModels = isArray(viewModels) ? viewModels : []; this.viewModels = isArray(viewModels) ? viewModels : [];
}
/**
* @returns {Array}
*/
get viewModels() {
return this.aViewModels;
}
/**
* @returns {string}
*/
screenName() {
return this.sScreenName;
} }
/** /**
@ -28,28 +14,26 @@ export class AbstractScreen {
return null; return null;
} }
/** /*
* @returns {?Object} onBuild(viewModelDom) {}
*/ onShow() {}
get __cross() { onHide() {}
return this.oCross; __started
} __builded
*/
/** /**
* @returns {void} * @returns {void}
*/ */
onStart() { onStart() {
if (!this.__started) { const routes = this.routes();
this.__started = true; if (arrayLength(routes)) {
const routes = this.routes(); let route = new Crossroads(),
if (arrayLength(routes)) { fMatcher = (this.onRoute || (()=>0)).bind(this);
let route = new Crossroads(),
fMatcher = (this.onRoute || (()=>0)).bind(this);
routes.forEach(item => item && route && (route.addRoute(item[0], fMatcher).rules = item[1])); routes.forEach(item => item && route && (route.addRoute(item[0], fMatcher).rules = item[1]));
this.oCross = route; this.__cross = route;
}
} }
} }
} }

View file

@ -1,42 +1,38 @@
import ko from 'ko'; import ko from 'ko';
import { inFocus, addObservablesTo, addComputablesTo, addSubscribablesTo } from 'Common/Utils'; import { addObservablesTo, addComputablesTo, addSubscribablesTo } from 'External/ko';
import { Scope } from 'Common/Enums'; import { keyScope, SettingsGet, leftPanelDisabled } from 'Common/Globals';
import { keyScope } from 'Common/Globals'; import { ViewTypePopup, showScreenPopup } from 'Knoin/Knoin';
import { ViewType } from 'Knoin/Knoin';
import { SaveSettingsStep } from 'Common/Enums';
class AbstractView { class AbstractView {
constructor(name, templateID, type) constructor(templateID, type)
{ {
this.viewModelName = 'View/' + name; // Object.defineProperty(this, 'viewModelTemplateID', { value: templateID });
this.viewModelTemplateID = templateID; this.viewModelTemplateID = templateID || this.constructor.name.replace('UserView', '');
this.viewModelPosition = type; this.viewType = type;
this.bDisabeCloseOnEsc = false;
this.sDefaultScope = Scope.None;
this.sCurrentScope = Scope.None;
this.viewModelVisible = false;
this.modalVisibility = ko.observable(false).extend({ rateLimit: 0 });
this.viewModelName = '';
this.viewModelDom = null; this.viewModelDom = null;
this.keyScope = {
scope: 'none',
previous: 'none',
set: function() {
this.previous = keyScope();
keyScope(this.scope);
},
unset: function() {
keyScope(this.previous);
}
};
} }
/** /*
* @returns {void} onBuild() {}
*/ beforeShow() {} // Happens before: hidden = false
storeAndSetScope() { onShow() {} // Happens after: hidden = false
this.sCurrentScope = keyScope(); onHide() {}
keyScope(this.sDefaultScope); */
}
/**
* @returns {void}
*/
restoreScope() {
keyScope(this.sCurrentScope);
}
querySelector(selectors) { querySelector(selectors) {
return this.viewModelDom.querySelector(selectors); return this.viewModelDom.querySelector(selectors);
@ -60,54 +56,115 @@ export class AbstractViewPopup extends AbstractView
{ {
constructor(name) constructor(name)
{ {
super('Popup/' + name, 'Popups' + name, ViewType.Popup); super('Popups' + name, ViewTypePopup);
if (name in Scope) { this.keyScope.scope = name;
this.sDefaultScope = Scope[name]; this.modalVisible = ko.observable(false).extend({ rateLimit: 0 });
} shortcuts.add('escape,close', '', name, () => {
} if (this.modalVisible() && false !== this.onClose()) {
/* this.close();
cancelCommand() {} return false;
closeCommand() {}
*/
/**
* @returns {void}
*/
registerPopupKeyDown() {
addEventListener('keydown', event => {
if (event && this.modalVisibility()) {
if (!this.bDisabeCloseOnEsc && 'Escape' == event.key) {
this.cancelCommand();
return false;
} else if ('Backspace' == event.key && !inFocus()) {
return false;
}
} }
return true; return true;
}); });
} }
// Happens when user hits Escape or Close key
// return false to prevent closing
onClose() {}
/*
beforeShow() {} // Happens before showModal()
onShow() {} // Happens after showModal()
afterShow() {} // Happens after showModal() animation transitionend
onHide() {} // Happens before animation transitionend
afterHide() {} // Happens after animation transitionend
close() {}
*/
} }
export class AbstractViewCenter extends AbstractView AbstractViewPopup.showModal = function(params = []) {
{ showScreenPopup(this, params);
constructor(name, templateID) }
{
super(name, templateID, ViewType.Content); AbstractViewPopup.hidden = function() {
} return !this.__vm || !this.__vm.modalVisible();
} }
export class AbstractViewLeft extends AbstractView export class AbstractViewLeft extends AbstractView
{ {
constructor(name, templateID) constructor(templateID)
{ {
super(name, templateID, ViewType.Left); super(templateID, 'Left');
this.leftPanelDisabled = leftPanelDisabled;
} }
} }
export class AbstractViewRight extends AbstractView export class AbstractViewRight extends AbstractView
{ {
constructor(name, templateID) constructor(templateID)
{ {
super(name, templateID, ViewType.Right); super(templateID, 'Right');
}
}
export class AbstractViewSettings
{
/*
onBuild(viewModelDom) {}
beforeShow() {}
onShow() {}
onHide() {}
viewModelDom
*/
addSetting(name, valueCb)
{
let prop = name[0].toLowerCase() + name.slice(1),
trigger = prop + 'Trigger';
addObservablesTo(this, {
[prop]: SettingsGet(name),
[trigger]: SaveSettingsStep.Idle,
});
addSubscribablesTo(this, {
[prop]: (value => {
this[trigger](SaveSettingsStep.Animate);
valueCb && valueCb(value);
rl.app.Remote.saveSetting(name, value,
iError => {
this[trigger](iError ? SaveSettingsStep.FalseResult : SaveSettingsStep.TrueResult);
setTimeout(() => this[trigger](SaveSettingsStep.Idle), 1000);
}
);
}).debounce(999),
});
}
addSettings(names)
{
names.forEach(name => {
let prop = name[0].toLowerCase() + name.slice(1);
this[prop] || (this[prop] = ko.observable(SettingsGet(name)));
this[prop].subscribe(value => rl.app.Remote.saveSetting(name, value));
});
}
}
export class AbstractViewLogin extends AbstractView {
constructor(templateID) {
super(templateID, 'Content');
this.hideSubmitButton = SettingsGet('hideSubmitButton');
this.formError = ko.observable(false).extend({ falseTimeout: 500 });
}
onBuild(dom) {
dom.classList.add('LoginView');
}
onShow() {
rl.route.off();
}
submitForm() {
// return false;
} }
} }

View file

@ -1,18 +1,21 @@
import ko from 'ko'; import ko from 'ko';
import { koComputable } from 'External/ko';
import { doc, $htmlCL, elementById, fireEvent } from 'Common/Globals';
import { forEachObjectValue, forEachObjectEntry } from 'Common/Utils';
import { doc, $htmlCL } from 'Common/Globals'; let
import { arrayLength, isFunction } from 'Common/Utils'; SCREENS = {},
currentScreen = null,
let currentScreen = null,
visiblePopups = new Set,
defaultScreenName = ''; defaultScreenName = '';
const SCREENS = {}, const
autofocus = dom => { autofocus = dom => {
const af = dom.querySelector('[autofocus]'); const af = dom.querySelector('[autofocus]');
af && af.focus(); af && af.focus();
}, },
visiblePopups = new Set,
/** /**
* @param {string} screenName * @param {string} screenName
* @returns {?Object} * @returns {?Object}
@ -27,81 +30,96 @@ const SCREENS = {},
buildViewModel = (ViewModelClass, vmScreen) => { buildViewModel = (ViewModelClass, vmScreen) => {
if (ViewModelClass && !ViewModelClass.__builded) { if (ViewModelClass && !ViewModelClass.__builded) {
let vmDom = null; let vmDom = null;
const vm = new ViewModelClass(vmScreen), const
position = vm.viewModelPosition || '', vm = new ViewModelClass(vmScreen),
id = vm.viewModelTemplateID,
position = vm.viewType || '',
dialog = ViewTypePopup === position,
vmPlace = position ? doc.getElementById('rl-' + position.toLowerCase()) : null; vmPlace = position ? doc.getElementById('rl-' + position.toLowerCase()) : null;
ViewModelClass.__builded = true; ViewModelClass.__builded = true;
ViewModelClass.__vm = vm; ViewModelClass.__vm = vm;
if (vmPlace) { if (vmPlace) {
vmDom = Element.fromHTML('<div class="rl-view-model RL-' + vm.viewModelTemplateID + '" hidden=""></div>'); vmDom = Element.fromHTML(dialog
? '<dialog id="V-'+ id + '"></dialog>'
: '<div id="V-'+ id + '" hidden=""></div>');
vmPlace.append(vmDom); vmPlace.append(vmDom);
vm.viewModelDom = vmDom; vm.viewModelDom = vmDom;
ViewModelClass.__dom = vmDom; ViewModelClass.__dom = vmDom;
if (ViewType.Popup === position) { if (ViewTypePopup === position) {
vm.cancelCommand = vm.closeCommand = createCommand(() => { vm.close = () => hideScreenPopup(ViewModelClass);
hideScreenPopup(ViewModelClass);
}); // Firefox / Safari HTMLDialogElement not defined
if (!vmDom.showModal) {
vmDom.classList.add('polyfill');
vmDom.showModal = () => {
vmDom.backdrop ||
vmDom.before(vmDom.backdrop = Element.fromHTML('<div class="dialog-backdrop"></div>'));
vmDom.setAttribute('open','');
vmDom.open = true;
vmDom.returnValue = null;
vmDom.backdrop.hidden = false;
};
vmDom.close = v => {
vmDom.backdrop.hidden = true;
vmDom.returnValue = v;
vmDom.removeAttribute('open', null);
vmDom.open = false;
};
}
// show/hide popup/modal // show/hide popup/modal
const endShowHide = e => { const endShowHide = e => {
if (e.target === vmDom) { if (e.target === vmDom) {
if (vmDom.classList.contains('show')) { if (vmDom.classList.contains('animate')) {
autofocus(vmDom); autofocus(vmDom);
vm.onShowWithDelay && vm.onShowWithDelay(); vm.afterShow && vm.afterShow();
} else { } else {
vmDom.hidden = true; vmDom.close();
vm.onHideWithDelay && vm.onHideWithDelay(); vm.afterHide && vm.afterHide();
} }
} }
}; };
vm.modalVisibility.subscribe(value => { vm.modalVisible.subscribe(value => {
if (value) { if (value) {
visiblePopups.add(vm); visiblePopups.add(vm);
vmDom.style.zIndex = 3000 + popupVisibilityNames().length + 10; vmDom.style.zIndex = 3000 + (visiblePopups.size * 2);
vmDom.hidden = false; vmDom.showModal();
vm.storeAndSetScope(); if (vmDom.backdrop) {
popupVisibilityNames.push(vm.viewModelName); vmDom.backdrop.style.zIndex = 3000 + visiblePopups.size;
}
vm.keyScope.set();
requestAnimationFrame(() => { // wait just before the next paint requestAnimationFrame(() => { // wait just before the next paint
vmDom.offsetHeight; // force a reflow vmDom.offsetHeight; // force a reflow
vmDom.classList.add('show'); // trigger the transitions vmDom.classList.add('animate'); // trigger the transitions
}); });
} else { } else {
visiblePopups.delete(vm); visiblePopups.delete(vm);
vm.onHide && vm.onHide(); vm.onHide && vm.onHide();
vmDom.classList.remove('show'); vm.keyScope.unset();
vm.restoreScope(); vmDom.classList.remove('animate'); // trigger the transitions
popupVisibilityNames(popupVisibilityNames.filter(v=>v!==vm.viewModelName));
} }
vmDom.setAttribute('aria-hidden', !value); arePopupsVisible(0 < visiblePopups.size);
}); });
if ('ontransitionend' in vmDom) { vmDom.addEventListener('transitionend', endShowHide);
vmDom.addEventListener('transitionend', endShowHide);
} else {
// For Edge < 79 and mobile browsers
vm.modalVisibility.subscribe(() => ()=>setTimeout(endShowHide({target:vmDom}), 500));
}
} }
ko.applyBindingAccessorsToNode( ko.applyBindingAccessorsToNode(
vmDom, vmDom,
{ {
i18nInit: true, i18nInit: true,
template: () => ({ name: vm.viewModelTemplateID }) template: () => ({ name: id })
}, },
vm vm
); );
vm.onBuild && vm.onBuild(vmDom); vm.onBuild && vm.onBuild(vmDom);
if (vm && ViewType.Popup === position) {
vm.registerPopupKeyDown();
}
dispatchEvent(new CustomEvent('rl-view-model', {detail:vm})); fireEvent('rl-view-model', vm);
} else { } else {
console.log('Cannot find view model position: ' + position); console.log('Cannot find view model position: ' + position);
} }
@ -110,6 +128,37 @@ const SCREENS = {},
return ViewModelClass && ViewModelClass.__vm; return ViewModelClass && ViewModelClass.__vm;
}, },
forEachViewModel = (screen, fn) => {
screen.viewModels.forEach(ViewModelClass => {
if (
ViewModelClass.__vm &&
ViewModelClass.__dom &&
ViewTypePopup !== ViewModelClass.__vm.viewType
) {
fn(ViewModelClass.__vm, ViewModelClass.__dom);
}
});
},
hideScreen = (screenToHide, destroy) => {
screenToHide.onHide && screenToHide.onHide();
forEachViewModel(screenToHide, (vm, dom) => {
dom.hidden = true;
vm.onHide && vm.onHide();
destroy && vm.viewModelDom.remove();
});
},
/**
* @param {Function} ViewModelClassToHide
* @returns {void}
*/
hideScreenPopup = ViewModelClassToHide => {
if (ViewModelClassToHide && ViewModelClassToHide.__vm && ViewModelClassToHide.__dom) {
ViewModelClassToHide.__vm.modalVisible(false);
}
},
/** /**
* @param {string} screenName * @param {string} screenName
* @param {string} subPart * @param {string} subPart
@ -125,7 +174,7 @@ const SCREENS = {},
// Close all popups // Close all popups
for (let vm of visiblePopups) { for (let vm of visiblePopups) {
vm.closeCommand(); false === vm.onClose() || vm.close();
} }
if (screenName) { if (screenName) {
@ -154,109 +203,34 @@ const SCREENS = {},
setTimeout(() => { setTimeout(() => {
// hide screen // hide screen
if (currentScreen && !isSameScreen) { if (currentScreen && !isSameScreen) {
currentScreen.onHide && currentScreen.onHide(); hideScreen(currentScreen);
currentScreen.onHideWithDelay && setTimeout(()=>currentScreen.onHideWithDelay(), 500);
if (arrayLength(currentScreen.viewModels)) {
currentScreen.viewModels.forEach(ViewModelClass => {
if (
ViewModelClass.__vm &&
ViewModelClass.__dom &&
ViewType.Popup !== ViewModelClass.__vm.viewModelPosition
) {
ViewModelClass.__dom.hidden = true;
ViewModelClass.__vm.viewModelVisible = false;
ViewModelClass.__vm.onHide && ViewModelClass.__vm.onHide();
ViewModelClass.__vm.onHideWithDelay && setTimeout(()=>ViewModelClass.__vm.onHideWithDelay(), 500);
}
});
}
} }
// -- // --
currentScreen = vmScreen; currentScreen = vmScreen;
// show screen // show screen
if (currentScreen && !isSameScreen) { if (!isSameScreen) {
currentScreen.onShow && currentScreen.onShow(); vmScreen.onShow && vmScreen.onShow();
if (arrayLength(currentScreen.viewModels)) { forEachViewModel(vmScreen, (vm, dom) => {
currentScreen.viewModels.forEach(ViewModelClass => { vm.beforeShow && vm.beforeShow();
if ( dom.hidden = false;
ViewModelClass.__vm && vm.onShow && vm.onShow();
ViewModelClass.__dom && autofocus(dom);
ViewType.Popup !== ViewModelClass.__vm.viewModelPosition });
) {
ViewModelClass.__vm.onBeforeShow && ViewModelClass.__vm.onBeforeShow();
ViewModelClass.__dom.hidden = false;
ViewModelClass.__vm.viewModelVisible = true;
ViewModelClass.__vm.onShow && ViewModelClass.__vm.onShow();
autofocus(ViewModelClass.__dom);
ViewModelClass.__vm.onShowWithDelay && setTimeout(()=>ViewModelClass.__vm.onShowWithDelay, 200);
}
});
}
} }
// -- // --
vmScreen && vmScreen.__cross && vmScreen.__cross.parse(subPart); vmScreen.__cross && vmScreen.__cross.parse(subPart);
}, 1); }, 1);
} }
} }
}; };
export const export const
popupVisibilityNames = ko.observableArray(), ViewTypePopup = 'Popups',
ViewType = {
Popup: 'Popups',
Left: 'Left',
Right: 'Right',
Content: 'Content'
},
/**
* @param {Function} fExecute
* @param {(Function|boolean|null)=} fCanExecute = true
* @returns {Function}
*/
createCommand = (fExecute, fCanExecute = true) => {
let fResult = fExecute
? (...args) => {
if (fResult && fResult.canExecute && fResult.canExecute()) {
fExecute.apply(null, args);
}
return false;
} : ()=>0;
fResult.enabled = ko.observable(true);
fResult.isCommand = true;
if (isFunction(fCanExecute)) {
fResult.canExecute = ko.computed(() => fResult && fResult.enabled() && fCanExecute.call(null));
} else {
fResult.canExecute = ko.computed(() => fResult && fResult.enabled() && !!fCanExecute);
}
return fResult;
},
/**
* @param {Function} ViewModelClassToHide
* @returns {void}
*/
hideScreenPopup = ViewModelClassToHide => {
if (ViewModelClassToHide && ViewModelClassToHide.__vm && ViewModelClassToHide.__dom) {
ViewModelClassToHide.__vm.modalVisibility(false);
}
},
getScreenPopupViewModel = ViewModelClassToShow =>
(buildViewModel(ViewModelClassToShow) && ViewModelClassToShow.__dom) && ViewModelClassToShow.__vm,
/** /**
* @param {Function} ViewModelClassToShow * @param {Function} ViewModelClassToShow
@ -264,48 +238,47 @@ export const
* @returns {void} * @returns {void}
*/ */
showScreenPopup = (ViewModelClassToShow, params = []) => { showScreenPopup = (ViewModelClassToShow, params = []) => {
const vm = getScreenPopupViewModel(ViewModelClassToShow); const vm = buildViewModel(ViewModelClassToShow) && ViewModelClassToShow.__dom && ViewModelClassToShow.__vm;
if (vm) { if (vm) {
params = params || []; params = params || [];
vm.onBeforeShow && vm.onBeforeShow(...params); vm.beforeShow && vm.beforeShow(...params);
vm.modalVisibility(true); vm.modalVisible(true);
vm.onShow && vm.onShow(...params); vm.onShow && vm.onShow(...params);
} }
}, },
arePopupsVisible = () => 0 < visiblePopups.size, arePopupsVisible = ko.observable(false),
/**
* @param {Function} ViewModelClassToShow
* @returns {boolean}
*/
isPopupVisible = ViewModelClassToShow =>
ViewModelClassToShow && ViewModelClassToShow.__vm && ViewModelClassToShow.__vm.modalVisibility(),
/** /**
* @param {Array} screensClasses * @param {Array} screensClasses
* @returns {void} * @returns {void}
*/ */
startScreens = screensClasses => { startScreens = screensClasses => {
hasher.clear();
forEachObjectValue(SCREENS, screen => hideScreen(screen, 1));
SCREENS = {};
currentScreen = null,
defaultScreenName = '';
screensClasses.forEach(CScreen => { screensClasses.forEach(CScreen => {
if (CScreen) { if (CScreen) {
const vmScreen = new CScreen(), const vmScreen = new CScreen(),
screenName = vmScreen && vmScreen.screenName(); screenName = vmScreen.screenName;
defaultScreenName || (defaultScreenName = screenName);
if (screenName) { SCREENS[screenName] = vmScreen;
defaultScreenName || (defaultScreenName = screenName);
SCREENS[screenName] = vmScreen;
}
} }
}); });
Object.values(SCREENS).forEach(vmScreen => forEachObjectValue(SCREENS, vmScreen => {
vmScreen && vmScreen.onStart && vmScreen.onStart() if (!vmScreen.__started) {
); vmScreen.onStart();
vmScreen.__started = true;
}
});
const cross = new Crossroads(); const cross = new Crossroads();
cross.addRoute(/^([a-zA-Z0-9-]*)\/?(.*)$/, screenOnRoute); cross.addRoute(/^([a-zA-Z0-9-]*)\/?(.*)$/, screenOnRoute);
@ -314,22 +287,22 @@ export const
hasher.init(); hasher.init();
setTimeout(() => $htmlCL.remove('rl-started-trigger'), 100); setTimeout(() => $htmlCL.remove('rl-started-trigger'), 100);
setTimeout(() => $htmlCL.add('rl-started-delay'), 200);
const c = elementById('rl-content'), l = elementById('rl-loading');
c && (c.hidden = false);
l && l.remove();
}, },
/**
* Used by ko.bindingHandlers.command (template data-bind="command: ")
* to enable/disable click/submit action.
*/
decorateKoCommands = (thisArg, commands) => decorateKoCommands = (thisArg, commands) =>
Object.entries(commands).forEach(([key, canExecute]) => { forEachObjectEntry(commands, (key, canExecute) => {
let command = thisArg[key], let command = thisArg[key],
fn = (...args) => fn.enabled() && fn.canExecute() && command.apply(thisArg, args); fn = (...args) => fn.canExecute() && command.apply(thisArg, args);
// fn.__realCanExecute = canExecute; fn.canExecute = koComputable(() => canExecute.call(thisArg, thisArg));
// fn.isCommand = true;
fn.enabled = ko.observable(true);
fn.canExecute = (typeof canExecute === 'function')
? ko.computed(() => fn.enabled() && canExecute.call(thisArg, thisArg))
: ko.computed(() => fn.enabled());
thisArg[key] = fn; thisArg[key] = fn;
}); });

132
dev/Mime/Parser.js Normal file
View file

@ -0,0 +1,132 @@
const
// RFC2045
QPDecodeIn = /=([0-9A-F]{2})/g,
QPDecodeOut = (...args) => String.fromCharCode(parseInt(args[1], 16));
export function ParseMime(text)
{
class MimePart
{
header(name) {
return this.headers && this.headers[name];
}
headerValue(name) {
return (this.header(name) || {value:null}).value;
}
get raw() {
return text.slice(this.start, this.end);
}
get bodyRaw() {
return text.slice(this.bodyStart, this.bodyEnd);
}
get body() {
let body = this.bodyRaw,
encoding = this.headerValue('content-transfer-encoding');
if ('quoted-printable' == encoding) {
body = body.replace(/=\r?\n/g, '').replace(QPDecodeIn, QPDecodeOut);
} else if ('base64' == encoding) {
body = atob(body.replace(/\r?\n/g, ''));
}
return body;
}
get dataUrl() {
let body = this.bodyRaw,
encoding = this.headerValue('content-transfer-encoding');
if ('base64' == encoding) {
body = body.replace(/\r?\n/g, '');
} else {
if ('quoted-printable' == encoding) {
body = body.replace(/=\r?\n/g, '').replace(QPDecodeIn, QPDecodeOut);
}
body = btoa(body);
}
return 'data:' + this.headerValue('content-type') + ';base64,' + body;
}
forEach(fn) {
fn(this);
if (this.parts) {
this.parts.forEach(part => part.forEach(fn));
}
}
getByContentType(type) {
if (type == this.headerValue('content-type')) {
return this;
}
if (this.parts) {
let i = 0, p = this.parts, part;
for (i; i < p.length; ++i) {
if ((part = p[i].getByContentType(type))) {
return part;
}
}
}
}
}
const ParsePart = (mimePart, start_pos = 0, id = '') =>
{
let part = new MimePart;
if (id) {
part.id = id;
part.start = start_pos;
part.end = start_pos + mimePart.length;
}
// get headers
let head = mimePart.match(/^[\s\S]+?\r?\n\r?\n/);
if (head) {
head = head[0];
let headers = {};
head.replace(/\r?\n\s+/g, ' ').split(/\r?\n/).forEach(header => {
let match = header.match(/^([^:]+):\s*([^;]+)/),
params = {};
[...header.matchAll(/;\s*([^;=]+)=\s*"?([^;"]+)"?/g)].forEach(param =>
params[param[1].trim().toLowerCase()] = param[2].trim()
);
headers[match[1].trim().toLowerCase()] = {
value: match[2].trim(),
params: params
};
});
part.headers = headers;
// get body
part.bodyStart = start_pos + head.length;
part.bodyEnd = start_pos + mimePart.length;
// get child parts
let boundary = headers['content-type'].params.boundary;
if (boundary) {
part.boundary = boundary;
part.parts = [];
let regex = new RegExp('(?:^|\r?\n)--' + boundary + '(?:--)?(?:\r?\n|$)', 'g'),
body = mimePart.slice(head.length),
bodies = body.split(regex),
pos = part.bodyStart;
[...body.matchAll(regex)].forEach(([boundary], index) => {
if (!index) {
// Mostly something like: "This is a multi-part message in MIME format."
part.bodyText = bodies[0];
}
// Not the end?
if ('--' != boundary.trim().slice(-2)) {
pos += bodies[index].length + boundary.length;
part.parts.push(ParsePart(bodies[1+index], pos, ((id ? id + '.' : '') + (1+index))));
}
});
}
}
return part;
};
return ParsePart(text);
}

76
dev/Mime/Utils.js Normal file
View file

@ -0,0 +1,76 @@
import { ParseMime } from 'Mime/Parser';
import { AttachmentModel } from 'Model/Attachment';
import { FileInfo } from 'Common/File';
import { BEGIN_PGP_MESSAGE } from 'Stores/User/Pgp';
/**
* @param string data
* @param MessageModel message
*/
export function MimeToMessage(data, message)
{
let signed;
const struct = ParseMime(data);
if (struct.headers) {
let html = struct.getByContentType('text/html');
html = html ? html.body : '';
struct.forEach(part => {
let cd = part.header('content-disposition'),
cid = part.header('content-id'),
type = part.header('content-type');
if (cid || cd) {
// if (cd && 'attachment' === cd.value) {
let attachment = new AttachmentModel;
attachment.mimeType = type.value;
attachment.fileName = type.name || (cd && cd.params.filename) || '';
attachment.fileNameExt = attachment.fileName.replace(/^.+(\.[a-z]+)$/, '$1');
attachment.fileType = FileInfo.getType('', type.value);
attachment.url = part.dataUrl;
attachment.friendlySize = FileInfo.friendlySize(part.body.length);
/*
attachment.isThumbnail = false;
attachment.contentLocation = '';
attachment.download = '';
attachment.folder = '';
attachment.uid = '';
attachment.mimeIndex = part.id;
attachment.framed = false;
*/
attachment.cid = cid ? cid.value : '';
if (cid && html) {
let cid = 'cid:' + attachment.contentId(),
found = html.includes(cid);
attachment.isInline(found);
attachment.isLinked(found);
found && (html = html
.replace('src="' + cid + '"', 'src="' + attachment.url + '"')
.replace("src='" + cid + "'", "src='" + attachment.url + "'")
);
} else {
message.attachments.push(attachment);
}
} else if ('multipart/signed' === type.value && 'application/pgp-signature' === type.params.protocol) {
signed = {
MicAlg: type.micalg,
BodyPart: part.parts[0],
SigPart: part.parts[1]
};
}
});
const text = struct.getByContentType('text/plain');
message.plain(text ? text.body : '');
message.html(html);
} else {
message.plain(data);
}
if (!signed && message.plain().includes(BEGIN_PGP_MESSAGE)) {
signed = true;
}
message.pgpSigned(signed);
// TODO: Verify instantly?
}

View file

@ -1,4 +1,4 @@
import { isArray } from 'Common/Utils'; import { isArray, forEachObjectEntry } from 'Common/Utils';
export class AbstractCollectionModel extends Array export class AbstractCollectionModel extends Array
{ {
@ -24,8 +24,7 @@ export class AbstractCollectionModel extends Array
const result = new this(); const result = new this();
if (json) { if (json) {
if ('Collection/'+this.name.replace('Model', '') === json['@Object']) { if ('Collection/'+this.name.replace('Model', '') === json['@Object']) {
Object.entries(json).forEach(([key, value]) => '@' !== key[0] && (result[key] = value)); forEachObjectEntry(json, (key, value) => '@' !== key[0] && (result[key] = value));
// json[@Count]
json = json['@Collection']; json = json['@Collection'];
} }
if (isArray(json)) { if (isArray(json)) {

View file

@ -1,5 +1,3 @@
import { change } from 'Common/Links';
import { AbstractModel } from 'Knoin/AbstractModel'; import { AbstractModel } from 'Knoin/AbstractModel';
export class AccountModel extends AbstractModel { export class AccountModel extends AbstractModel {
@ -8,23 +6,16 @@ export class AccountModel extends AbstractModel {
* @param {boolean=} canBeDelete = true * @param {boolean=} canBeDelete = true
* @param {number=} count = 0 * @param {number=} count = 0
*/ */
constructor(email, canBeDelete = true, count = 0) { constructor(email/*, count = 0*/, isAdditional = true) {
super(); super();
this.email = email; this.email = email;
this.addObservables({ this.addObservables({
count: count, // count: count || 0,
deleteAccess: false, askDelete: false,
canBeDeleted: !!canBeDelete isAdditional: isAdditional
}); });
this.canBeEdit = this.canBeDeleted;
} }
/**
* @returns {string}
*/
changeAccountLink() {
return change(this.email);
}
} }

View file

@ -21,17 +21,20 @@ export class AttachmentModel extends AbstractModel {
this.fileNameExt = ''; this.fileNameExt = '';
this.fileType = FileType.Unknown; this.fileType = FileType.Unknown;
this.friendlySize = ''; this.friendlySize = '';
this.isInline = false;
this.isLinked = false;
this.isThumbnail = false; this.isThumbnail = false;
this.cid = ''; this.cid = '';
this.cidWithoutTags = '';
this.contentLocation = ''; this.contentLocation = '';
this.download = ''; this.download = '';
this.folder = ''; this.folder = '';
this.uid = ''; this.uid = '';
this.url = '';
this.mimeIndex = ''; this.mimeIndex = '';
this.framed = false; this.framed = false;
this.addObservables({
isInline: false,
isLinked: false
});
} }
/** /**
@ -43,7 +46,6 @@ export class AttachmentModel extends AbstractModel {
const attachment = super.reviveFromJson(json); const attachment = super.reviveFromJson(json);
if (attachment) { if (attachment) {
attachment.friendlySize = FileInfo.friendlySize(json.EstimatedSize); attachment.friendlySize = FileInfo.friendlySize(json.EstimatedSize);
attachment.cidWithoutTags = attachment.cid.replace(/^<+/, '').replace(/>+$/, '');
attachment.fileNameExt = FileInfo.getExtension(attachment.fileName); attachment.fileNameExt = FileInfo.getExtension(attachment.fileName);
attachment.fileType = FileInfo.getType(attachment.fileNameExt, attachment.mimeType); attachment.fileType = FileInfo.getType(attachment.fileNameExt, attachment.mimeType);
@ -51,6 +53,10 @@ export class AttachmentModel extends AbstractModel {
return attachment; return attachment;
} }
contentId() {
return this.cid.replace(/^<+|>+$/g, '');
}
/** /**
* @returns {boolean} * @returns {boolean}
*/ */
@ -122,29 +128,21 @@ export class AttachmentModel extends AbstractModel {
* @returns {string} * @returns {string}
*/ */
linkDownload() { linkDownload() {
return attachmentDownload(this.download); return this.url || attachmentDownload(this.download);
} }
/** /**
* @returns {string} * @returns {string}
*/ */
linkPreview() { linkPreview() {
return serverRequestRaw('View', this.download); return this.url || serverRequestRaw('View', this.download);
}
/**
* @returns {string}
*/
linkThumbnail() {
return this.hasThumbnail() ? serverRequestRaw('ViewThumbnail', this.download) : '';
} }
/** /**
* @returns {string} * @returns {string}
*/ */
linkThumbnailPreviewStyle() { linkThumbnailPreviewStyle() {
const link = this.linkThumbnail(); return this.hasThumbnail() ? 'background:url(' + serverRequestRaw('ViewThumbnail', this.download) + ')' : '';
return link ? 'background:url(' + link + ')' : '';
} }
/** /**
@ -175,7 +173,7 @@ export class AttachmentModel extends AbstractModel {
const localEvent = event.originalEvent || event; const localEvent = event.originalEvent || event;
if (attachment && localEvent && localEvent.dataTransfer && localEvent.dataTransfer.setData) { if (attachment && localEvent && localEvent.dataTransfer && localEvent.dataTransfer.setData) {
let link = this.linkDownload(); let link = this.linkDownload();
if ('http' !== link.substr(0, 4)) { if ('http' !== link.slice(0, 4)) {
link = location.protocol + '//' + location.host + location.pathname + link; link = location.protocol + '//' + location.host + location.pathname + link;
} }
localEvent.dataTransfer.setData('DownloadURL', this.mimeType + ':' + this.fileName + ':' + link); localEvent.dataTransfer.setData('DownloadURL', this.mimeType + ':' + this.fileName + ':' + link);

View file

@ -11,13 +11,20 @@ export class AttachmentCollectionModel extends AbstractCollectionModel
*/ */
static reviveFromJson(items) { static reviveFromJson(items) {
return super.reviveFromJson(items, attachment => AttachmentModel.reviveFromJson(attachment)); return super.reviveFromJson(items, attachment => AttachmentModel.reviveFromJson(attachment));
/*
const attachments = super.reviveFromJson(items, attachment => AttachmentModel.reviveFromJson(attachment));
if (attachments) {
attachments.InlineCount = attachments.reduce((accumulator, a) => accumulator + (a.isInline ? 1 : 0), 0);
}
return attachments;
*/
} }
/** /**
* @returns {boolean} * @returns {boolean}
*/ */
hasVisible() { hasVisible() {
return !!this.find(item => !item.isLinked); return !!this.filter(item => !item.isLinked()).length;
} }
/** /**
@ -25,7 +32,7 @@ export class AttachmentCollectionModel extends AbstractCollectionModel
* @returns {*} * @returns {*}
*/ */
findByCid(cid) { findByCid(cid) {
cid = cid.replace(/^<+|>+$/, ''); cid = cid.replace(/^<+|>+$/g, '');
return this.find(item => cid === item.cidWithoutTags); return this.find(item => cid === item.contentId());
} }
} }

View file

@ -65,8 +65,8 @@ export class ComposeAttachmentModel extends AbstractModel {
item.download, item.download,
item.fileName, item.fileName,
item.estimatedSize, item.estimatedSize,
item.isInline, item.isInline(),
item.isLinked, item.isLinked(),
item.cid, item.cid,
item.contentLocation item.contentLocation
); );

View file

@ -260,7 +260,7 @@ class Tokenizer
} }
} }
class EmailModel extends AbstractModel { export class EmailModel extends AbstractModel {
/** /**
* @param {string=} email = '' * @param {string=} email = ''
* @param {string=} name = '' * @param {string=} name = ''
@ -430,5 +430,3 @@ class EmailModel extends AbstractModel {
return false; return false;
} }
} }
export { EmailModel, EmailModel as default };

View file

@ -1,34 +1,112 @@
import { AbstractCollectionModel } from 'Model/AbstractCollection'; import { AbstractCollectionModel } from 'Model/AbstractCollection';
import { UNUSED_OPTION_VALUE } from 'Common/Consts'; import { UNUSED_OPTION_VALUE } from 'Common/Consts';
import { isArray, pInt } from 'Common/Utils'; import { isArray, getKeyByValue, forEachObjectEntry, b64EncodeJSONSafe } from 'Common/Utils';
import { ClientSideKeyName, FolderType } from 'Common/EnumsUser'; import { ClientSideKeyNameExpandedFolders, FolderType, FolderMetadataKeys } from 'Common/EnumsUser';
import * as Cache from 'Common/Cache'; import { getFolderFromCacheList, setFolder, setFolderInboxName, setFolderHash } from 'Common/Cache';
import { Settings, SettingsGet } from 'Common/Globals'; import { Settings, SettingsGet, fireEvent } from 'Common/Globals';
import * as Local from 'Storage/Client'; import * as Local from 'Storage/Client';
import { AppUserStore } from 'Stores/User/App'; import { AppUserStore } from 'Stores/User/App';
import { FolderUserStore } from 'Stores/User/Folder'; import { FolderUserStore } from 'Stores/User/Folder';
import { MessageUserStore } from 'Stores/User/Message'; import { MessagelistUserStore } from 'Stores/User/Messagelist';
import { SettingsUserStore } from 'Stores/User/Settings'; import { SettingsUserStore } from 'Stores/User/Settings';
import ko from 'ko'; import ko from 'ko';
import { isPosNumeric } from 'Common/UtilsUser'; import { sortFolders } from 'Common/Folders';
import { i18n, trigger as translatorTrigger } from 'Common/Translator'; import { i18n, trigger as translatorTrigger } from 'Common/Translator';
import { AbstractModel } from 'Knoin/AbstractModel'; import { AbstractModel } from 'Knoin/AbstractModel';
const import { koComputable } from 'External/ko';
normalizeFolder = sFolderFullNameRaw => ('' === sFolderFullNameRaw
|| UNUSED_OPTION_VALUE === sFolderFullNameRaw
|| null !== Cache.getFolderFromCacheList(sFolderFullNameRaw))
? sFolderFullNameRaw
: '';
// index is FolderType value //import { mailBox } from 'Common/Links';
let SystemFolders = [];
import Remote from 'Remote/User/Fetch';
const
isPosNumeric = value => null != value && /^[0-9]*$/.test(value.toString()),
normalizeFolder = sFolderFullName => ('' === sFolderFullName
|| UNUSED_OPTION_VALUE === sFolderFullName
|| null !== getFolderFromCacheList(sFolderFullName))
? sFolderFullName
: '',
SystemFolders = {
Inbox: 0,
Sent: 0,
Drafts: 0,
Spam: 0,
Trash: 0,
Archive: 0
},
kolabTypes = {
configuration: 'CONFIGURATION',
event: 'CALENDAR',
contact: 'CONTACTS',
task: 'TASKS',
note: 'NOTES',
file: 'FILES',
journal: 'JOURNAL'
},
getKolabFolderName = type => kolabTypes[type] ? 'Kolab ' + i18n('SETTINGS_FOLDERS/TYPE_' + kolabTypes[type]) : '',
getSystemFolderName = (type, def) => {
switch (type) {
case FolderType.Inbox:
case FolderType.Sent:
case FolderType.Drafts:
case FolderType.Trash:
case FolderType.Archive:
return i18n('FOLDER_LIST/' + getKeyByValue(FolderType, type).toUpperCase() + '_NAME');
case FolderType.Spam:
return i18n('GLOBAL/SPAM');
// no default
}
return def;
};
export const
/**
* @param {string} sFullName
* @param {boolean} bExpanded
*/
setExpandedFolder = (sFullName, bExpanded) => {
let aExpandedList = Local.get(ClientSideKeyNameExpandedFolders);
if (!isArray(aExpandedList)) {
aExpandedList = [];
}
if (bExpanded) {
aExpandedList.includes(sFullName) || aExpandedList.push(sFullName);
} else {
aExpandedList = aExpandedList.filter(value => value !== sFullName);
}
Local.set(ClientSideKeyNameExpandedFolders, aExpandedList);
},
/**
* @param {?Function} fCallback
*/
loadFolders = fCallback => {
// clearTimeout(this.foldersTimeout);
Remote.abort('Folders')
.post('Folders', FolderUserStore.foldersLoading)
.then(data => {
data = FolderCollectionModel.reviveFromJson(data.Result);
data && data.storeIt();
fCallback && fCallback(true);
// Repeat every 15 minutes?
// this.foldersTimeout = setTimeout(loadFolders, 900000);
})
.catch(() => fCallback && setTimeout(fCallback, 1, false));
};
export class FolderCollectionModel extends AbstractCollectionModel export class FolderCollectionModel extends AbstractCollectionModel
{ {
@ -36,11 +114,11 @@ export class FolderCollectionModel extends AbstractCollectionModel
constructor() { constructor() {
super(); super();
this.CountRec this.CountRec
this.FoldersHash
this.IsThreadsSupported this.IsThreadsSupported
this.Namespace; this.Namespace;
this.Optimized this.Optimized
this.SystemFolders this.SystemFolders
this.Capabilities
} }
*/ */
@ -49,53 +127,40 @@ export class FolderCollectionModel extends AbstractCollectionModel
* @returns {FolderCollectionModel} * @returns {FolderCollectionModel}
*/ */
static reviveFromJson(object) { static reviveFromJson(object) {
const expandedFolders = Local.get(ClientSideKeyName.ExpandedFolders); const expandedFolders = Local.get(ClientSideKeyNameExpandedFolders);
if (object && object.SystemFolders) { if (object && object.SystemFolders) {
let sf = object.SystemFolders; forEachObjectEntry(SystemFolders, key =>
SystemFolders = [ SystemFolders[key] = SettingsGet(key+'Folder') || object.SystemFolders[FolderType[key]]
/* USER */ 0, );
/* INBOX */ sf[1],
SettingsGet('SentFolder') || sf[2],
SettingsGet('DraftFolder') || sf[3],
SettingsGet('SpamFolder') || sf[4],
SettingsGet('TrashFolder') || sf[5],
SettingsGet('ArchiveFolder') || sf[12]
// IMPORTANT: sf[10],
// FLAGGED: sf[11],
// ALL: sf[13]
];
} }
return super.reviveFromJson(object, oFolder => { const result = super.reviveFromJson(object, oFolder => {
let oCacheFolder = Cache.getFolderFromCacheList(oFolder.FullNameRaw); let oCacheFolder = getFolderFromCacheList(oFolder.FullName),
type = FolderType[getKeyByValue(SystemFolders, oFolder.FullName)];
if (oCacheFolder) { if (!oCacheFolder) {
oFolder.SubFolders = FolderCollectionModel.reviveFromJson(oFolder.SubFolders);
oFolder.SubFolders && oCacheFolder.subFolders(oFolder.SubFolders);
} else {
oCacheFolder = FolderModel.reviveFromJson(oFolder); oCacheFolder = FolderModel.reviveFromJson(oFolder);
if (!oCacheFolder) if (!oCacheFolder)
return null; return null;
if (1 == SystemFolders.indexOf(oFolder.FullNameRaw)) { if (1 == type) {
oCacheFolder.type(FolderType.Inbox); oCacheFolder.type(FolderType.Inbox);
Cache.setFolderInboxName(oFolder.FullNameRaw); setFolderInboxName(oFolder.FullName);
} }
Cache.setFolder(oCacheFolder.fullNameHash, oFolder.FullNameRaw, oCacheFolder); setFolder(oCacheFolder);
} }
let type = SystemFolders.indexOf(oFolder.FullNameRaw);
if (1 < type) { if (1 < type) {
oCacheFolder.type(type); oCacheFolder.type(type);
} }
oCacheFolder.collapsed(!expandedFolders oCacheFolder.collapsed(!expandedFolders
|| !isArray(expandedFolders) || !isArray(expandedFolders)
|| !expandedFolders.includes(oCacheFolder.fullNameHash)); || !expandedFolders.includes(oCacheFolder.fullName));
if (oFolder.Extended) { if (oFolder.Extended) {
if (oFolder.Extended.Hash) { if (oFolder.Extended.Hash) {
Cache.setFolderHash(oCacheFolder.fullNameRaw, oFolder.Extended.Hash); setFolderHash(oCacheFolder.fullName, oFolder.Extended.Hash);
} }
if (null != oFolder.Extended.MessageCount) { if (null != oFolder.Extended.MessageCount) {
@ -108,89 +173,77 @@ export class FolderCollectionModel extends AbstractCollectionModel
} }
return oCacheFolder; return oCacheFolder;
}); });
let i = result.length;
if (i) {
sortFolders(result);
try {
while (i--) {
let folder = result[i], parent = getFolderFromCacheList(folder.parentName);
if (parent) {
parent.subFolders.unshift(folder);
result.splice(i,1);
}
}
} catch (e) {
console.error(e);
}
}
return result;
} }
storeIt() { storeIt() {
const cnt = pInt(this.CountRec); FolderUserStore.displaySpecSetting(Settings.app('folderSpecLimit') < this.CountRec);
FolderUserStore.displaySpecSetting(0 >= cnt if (!(
|| Math.max(10, Math.min(100, pInt(Settings.app('folderSpecLimit')))) < cnt);
if (SystemFolders &&
!('' +
SettingsGet('SentFolder') + SettingsGet('SentFolder') +
SettingsGet('DraftFolder') + SettingsGet('DraftsFolder') +
SettingsGet('SpamFolder') + SettingsGet('SpamFolder') +
SettingsGet('TrashFolder') + SettingsGet('TrashFolder') +
SettingsGet('ArchiveFolder')) SettingsGet('ArchiveFolder')
)
) { ) {
FolderUserStore.saveSystemFolders({ FolderUserStore.saveSystemFolders(SystemFolders);
SentFolder: SystemFolders[FolderType.SENT] || null,
DraftFolder: SystemFolders[FolderType.DRAFTS] || null,
SpamFolder: SystemFolders[FolderType.SPAM] || null,
TrashFolder: SystemFolders[FolderType.TRASH] || null,
ArchiveFolder: SystemFolders[FolderType.ARCHIVE] || null
});
} }
FolderUserStore.folderList(this); FolderUserStore.folderList(this);
if (undefined !== this.Namespace) { FolderUserStore.namespace = this.Namespace;
FolderUserStore.namespace = this.Namespace;
}
AppUserStore.threadsAllowed(!!(Settings.app('useImapThread') && this.IsThreadsSupported)); AppUserStore.threadsAllowed(!!(Settings.app('useImapThread') && this.IsThreadsSupported));
FolderUserStore.folderListOptimized(!!this.Optimized); FolderUserStore.folderListOptimized(!!this.Optimized);
FolderUserStore.sortSupported(!!this.IsSortSupported); FolderUserStore.quotaUsage(this.quotaUsage);
FolderUserStore.quotaLimit(this.quotaLimit);
FolderUserStore.capabilities(this.Capabilities);
FolderUserStore.sentFolder(normalizeFolder(SettingsGet('SentFolder'))); FolderUserStore.sentFolder(normalizeFolder(SystemFolders.Sent));
FolderUserStore.draftFolder(normalizeFolder(SettingsGet('DraftFolder'))); FolderUserStore.draftsFolder(normalizeFolder(SystemFolders.Drafts));
FolderUserStore.spamFolder(normalizeFolder(SettingsGet('SpamFolder'))); FolderUserStore.spamFolder(normalizeFolder(SystemFolders.Spam));
FolderUserStore.trashFolder(normalizeFolder(SettingsGet('TrashFolder'))); FolderUserStore.trashFolder(normalizeFolder(SystemFolders.Trash));
FolderUserStore.archiveFolder(normalizeFolder(SettingsGet('ArchiveFolder'))); FolderUserStore.archiveFolder(normalizeFolder(SystemFolders.Archive));
// FolderUserStore.folderList.valueHasMutated(); // FolderUserStore.folderList.valueHasMutated();
Local.set(ClientSideKeyName.FoldersLashHash, this.FoldersHash);
} }
} }
function getSystemFolderName(type, def)
{
switch (type) {
case FolderType.Inbox:
return i18n('FOLDER_LIST/INBOX_NAME');
case FolderType.Sent:
return i18n('FOLDER_LIST/SENT_NAME');
case FolderType.Drafts:
return i18n('FOLDER_LIST/DRAFTS_NAME');
case FolderType.Spam:
return i18n('GLOBAL/SPAM');
case FolderType.Trash:
return i18n('FOLDER_LIST/TRASH_NAME');
case FolderType.Archive:
return i18n('FOLDER_LIST/ARCHIVE_NAME');
// no default
}
return def;
}
export class FolderModel extends AbstractModel { export class FolderModel extends AbstractModel {
constructor() { constructor() {
super(); super();
this.fullName = ''; this.fullName = '';
this.fullNameRaw = '';
this.fullNameHash = '';
this.delimiter = ''; this.delimiter = '';
this.namespace = '';
this.deep = 0; this.deep = 0;
this.expires = 0; this.expires = 0;
this.metadata = {};
this.exists = true; this.exists = true;
// this.hash = '';
// this.uidNext = 0;
this.addObservables({ this.addObservables({
name: '', name: '',
type: FolderType.User, type: FolderType.User,
@ -200,21 +253,36 @@ export class FolderModel extends AbstractModel {
selected: false, selected: false,
edited: false, edited: false,
subscribed: true, subscribed: true,
checkable: false, checkable: false, // Check for new messages
deleteAccess: false, askDelete: false,
nameForEdit: '', nameForEdit: '',
errorMsg: '',
privateMessageCountAll: 0, privateMessageCountAll: 0,
privateMessageCountUnread: 0, privateMessageCountUnread: 0,
collapsedPrivate: true kolabType: null,
collapsed: true
});
this.addSubscribables({
kolabType: sValue => this.metadata[FolderMetadataKeys.KolabFolderType] = sValue
}); });
this.subFolders = ko.observableArray(new FolderCollectionModel); this.subFolders = ko.observableArray(new FolderCollectionModel);
this.actionBlink = ko.observable(false).extend({ falseTimeout: 1000 }); this.actionBlink = ko.observable(false).extend({ falseTimeout: 1000 });
} }
/**
* For url safe '/#/mailbox/...' path
*/
get fullNameHash() {
return this.fullName.replace(/[^a-z0-9._-]+/giu, b64EncodeJSONSafe);
// return /^[a-z0-9._-]+$/iu.test(this.fullName) ? this.fullName : b64EncodeJSONSafe(this.fullName);
}
/** /**
* @static * @static
* @param {FetchJsonFolder} json * @param {FetchJsonFolder} json
@ -223,9 +291,19 @@ export class FolderModel extends AbstractModel {
static reviveFromJson(json) { static reviveFromJson(json) {
const folder = super.reviveFromJson(json); const folder = super.reviveFromJson(json);
if (folder) { if (folder) {
folder.deep = json.FullNameRaw.split(folder.delimiter).length - 1; const path = folder.fullName.split(folder.delimiter),
type = (folder.metadata[FolderMetadataKeys.KolabFolderType]
|| folder.metadata[FolderMetadataKeys.KolabFolderTypeShared]
|| ''
).split('.')[0];
folder.messageCountAll = ko.computed({ folder.deep = path.length - 1;
path.pop();
folder.parentName = path.join(folder.delimiter);
type && 'mail' != type && folder.kolabType(type);
folder.messageCountAll = koComputable({
read: folder.privateMessageCountAll, read: folder.privateMessageCountAll,
write: (iValue) => { write: (iValue) => {
if (isPosNumeric(iValue)) { if (isPosNumeric(iValue)) {
@ -237,7 +315,7 @@ export class FolderModel extends AbstractModel {
}) })
.extend({ notify: 'always' }); .extend({ notify: 'always' });
folder.messageCountUnread = ko.computed({ folder.messageCountUnread = koComputable({
read: folder.privateMessageCountUnread, read: folder.privateMessageCountUnread,
write: (value) => { write: (value) => {
if (isPosNumeric(value)) { if (isPosNumeric(value)) {
@ -254,29 +332,45 @@ export class FolderModel extends AbstractModel {
isInbox: () => FolderType.Inbox === folder.type(), isInbox: () => FolderType.Inbox === folder.type(),
isFlagged: () => FolderUserStore.currentFolder() === folder isFlagged: () => FolderUserStore.currentFolder() === folder
&& MessageUserStore.listSearch().trim().includes('is:flagged'), && MessagelistUserStore.listSearch().includes('flagged'),
hasSubscribedSubfolders: hasVisibleSubfolders: () => !!folder.subFolders().find(folder => folder.visible()),
() => !!folder.subFolders().find(
hasSubscriptions: () => folder.subscribed() | !!folder.subFolders().find(
oFolder => { oFolder => {
const subscribed = oFolder.hasSubscriptions(); const subscribed = oFolder.hasSubscriptions();
return !oFolder.isSystemFolder() && subscribed; return !oFolder.isSystemFolder() && subscribed;
} }
), ),
hasSubscriptions: () => folder.subscribed() | folder.hasSubscribedSubfolders(),
canBeEdited: () => FolderType.User === folder.type() && folder.exists/* && folder.selectable()*/, canBeEdited: () => FolderType.User === folder.type() && folder.exists/* && folder.selectable()*/,
visible: () => folder.hasSubscriptions() | !SettingsUserStore.hideUnsubscribed(), isSystemFolder: () => FolderType.User !== folder.type() | !!folder.kolabType(),
isSystemFolder: () => FolderType.User !== folder.type(), canBeSelected: () => folder.selectable() && !folder.isSystemFolder(),
hidden: () => { canBeDeleted: () => folder.canBeSelected() && folder.exists,
let hasSubFolders = folder.hasSubscribedSubfolders();
return (folder.isSystemFolder() | !folder.selectable()) && !hasSubFolders; canBeSubscribed: () => folder.selectable()
&& !(folder.isSystemFolder() | !SettingsUserStore.hideUnsubscribed()),
/**
* Folder is visible when:
* - hasVisibleSubfolders()
* Or when all below conditions are true:
* - selectable()
* - subscribed() OR hideUnsubscribed = false
* - FolderType.User
* - not kolabType()
*/
visible: () => {
const selectable = folder.canBeSelected(),
visible = (folder.subscribed() | !SettingsUserStore.hideUnsubscribed()) && selectable;
return folder.hasVisibleSubfolders() | visible;
}, },
hidden: () => !folder.selectable() && (folder.isSystemFolder() | !folder.hasVisibleSubfolders()),
printableUnreadCount: () => { printableUnreadCount: () => {
const count = folder.messageCountAll(), const count = folder.messageCountAll(),
unread = folder.messageCountUnread(), unread = folder.messageCountUnread(),
@ -299,13 +393,6 @@ export class FolderModel extends AbstractModel {
return null; return null;
}, },
canBeDeleted: () => !(folder.isSystemFolder() | !folder.selectable()),
canBeSubscribed: () => Settings.app('useImapSubscribe')
&& !(folder.isSystemFolder() | !SettingsUserStore.hideUnsubscribed() | !folder.selectable()),
canBeSelected: () => !(folder.isSystemFolder() | !folder.selectable()),
localName: () => { localName: () => {
let name = folder.name(); let name = folder.name();
if (folder.isSystemFolder()) { if (folder.isSystemFolder()) {
@ -318,28 +405,22 @@ export class FolderModel extends AbstractModel {
manageFolderSystemName: () => { manageFolderSystemName: () => {
if (folder.isSystemFolder()) { if (folder.isSystemFolder()) {
translatorTrigger(); translatorTrigger();
let suffix = getSystemFolderName(folder.type(), ''); let suffix = getSystemFolderName(folder.type(), getKolabFolderName(folder.kolabType()));
if (folder.name() !== suffix && 'inbox' !== suffix.toLowerCase()) { if (folder.name() !== suffix && 'inbox' !== suffix.toLowerCase()) {
return '(' + suffix + ')'; return '(' + suffix + ')';
} }
} }
return ''; return '';
}, },
collapsed: {
read: () => folder.isInbox() && FolderUserStore.singleRootFolder()
? false
: !folder.hidden() && folder.collapsedPrivate(),
write: value => folder.collapsedPrivate(value)
},
hasUnreadMessages: () => 0 < folder.messageCountUnread() && folder.printableUnreadCount(), hasUnreadMessages: () => 0 < folder.messageCountUnread() && folder.printableUnreadCount(),
hasSubscribedUnreadMessagesSubfolders: () => hasSubscribedUnreadMessagesSubfolders: () =>
!!folder.subFolders().find( !!folder.subFolders().find(
folder => folder.hasUnreadMessages() || folder.hasSubscribedUnreadMessagesSubfolders() folder => folder.hasUnreadMessages() | folder.hasSubscribedUnreadMessagesSubfolders()
) )
// ,href: () => folder.canBeSelected() && mailBox(folder.fullNameHash)
}); });
folder.addSubscribables({ folder.addSubscribables({
@ -349,7 +430,7 @@ export class FolderModel extends AbstractModel {
messageCountUnread: unread => { messageCountUnread: unread => {
if (FolderType.Inbox === folder.type()) { if (FolderType.Inbox === folder.type()) {
dispatchEvent(new CustomEvent('mailbox.inbox-unread-count', {detail:unread})); fireEvent('mailbox.inbox-unread-count', unread);
} }
} }
}); });
@ -361,16 +442,9 @@ export class FolderModel extends AbstractModel {
* @returns {string} * @returns {string}
*/ */
collapsedCss() { collapsedCss() {
return 'e-collapsed-sign ' + (this.hasSubscribedSubfolders() return 'e-collapsed-sign ' + (this.hasVisibleSubfolders()
? (this.collapsed() ? 'icon-right-mini' : 'icon-down-mini') ? (this.collapsed() ? 'icon-right-mini' : 'icon-down-mini')
: 'icon-none' : 'icon-none'
); );
} }
/**
* @returns {string}
*/
printableFullName() {
return this.fullName.replace(this.delimiter, ' / ');
}
} }

View file

@ -1,4 +1,4 @@
import ko from 'ko'; import { koComputable } from 'External/ko';
import { AbstractModel } from 'Knoin/AbstractModel'; import { AbstractModel } from 'Knoin/AbstractModel';
@ -21,10 +21,10 @@ export class IdentityModel extends AbstractModel {
signature: '', signature: '',
signatureInsertBefore: false, signatureInsertBefore: false,
deleteAccess: false askDelete: false
}); });
this.canBeDeleted = ko.computed(() => !!this.id()); this.canBeDeleted = koComputable(() => !!this.id());
} }
/** /**
@ -34,6 +34,6 @@ export class IdentityModel extends AbstractModel {
const name = this.name(), const name = this.name(),
email = this.email(); email = this.email();
return name ? name + ' (' + email + ')' : email; return name ? name + ' <' + email + '>' : email;
} }
} }

View file

@ -3,12 +3,13 @@ import ko from 'ko';
import { MessagePriority } from 'Common/EnumsUser'; import { MessagePriority } from 'Common/EnumsUser';
import { i18n } from 'Common/Translator'; import { i18n } from 'Common/Translator';
import { encodeHtml } from 'Common/Html'; import { doc } from 'Common/Globals';
import { isArray, arrayLength } from 'Common/Utils'; import { encodeHtml, plainToHtml, cleanHtml } from 'Common/Html';
import { isArray, arrayLength, forEachObjectEntry } from 'Common/Utils';
import { serverRequestRaw } from 'Common/Links'; import { serverRequestRaw } from 'Common/Links';
import { FolderUserStore } from 'Stores/User/Folder'; import { FolderUserStore } from 'Stores/User/Folder';
import { SettingsUserStore } from 'Stores/User/Settings';
import { FileInfo } from 'Common/File'; import { FileInfo } from 'Common/File';
import { AttachmentCollectionModel } from 'Model/AttachmentCollection'; import { AttachmentCollectionModel } from 'Model/AttachmentCollection';
@ -18,13 +19,17 @@ import { AbstractModel } from 'Knoin/AbstractModel';
import PreviewHTML from 'Html/PreviewMessage.html'; import PreviewHTML from 'Html/PreviewMessage.html';
const const
SignedVerifyStatus = { // eslint-disable-next-line max-len
UnknownPublicKeys: -4, url = /(^|[\s\n]|\/?>)(https:\/\/[-A-Z0-9+\u0026\u2019#/%?=()~_|!:,.;]*[-A-Z0-9+\u0026#/%=~()_|])/gi,
UnknownPrivateKey: -3, // eslint-disable-next-line max-len
Unverified: -2, email = /(^|[\s\n]|\/?>)((?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x21\x23-\x5b\x5d-\x7f]|\\[\x21\x23-\x5b\x5d-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\.){3}(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9])|[a-z0-9-]*[a-z0-9]:(?:[\x21-\x5a\x53-\x7f]|\\[\x21\x23-\x5b\x5d-\x7f])+)\]))/gi,
Error: -1,
None: 0, hcont = Element.fromHTML('<div area="hidden" style="position:absolute;left:-5000px"></div>'),
Success: 1 getRealHeight = el => {
hcont.innerHTML = el.outerHTML;
const result = hcont.clientHeight;
hcont.innerHTML = '';
return result;
}, },
replyHelper = (emails, unic, localEmails) => { replyHelper = (emails, unic, localEmails) => {
@ -36,6 +41,8 @@ const
}); });
}; };
doc.body.append(hcont);
export class MessageModel extends AbstractModel { export class MessageModel extends AbstractModel {
constructor() { constructor() {
super(); super();
@ -44,6 +51,8 @@ export class MessageModel extends AbstractModel {
this.addObservables({ this.addObservables({
subject: '', subject: '',
plain: '',
html: '',
size: 0, size: 0,
spamScore: 0, spamScore: 0,
spamResult: '', spamResult: '',
@ -56,25 +65,20 @@ export class MessageModel extends AbstractModel {
senderClearEmailsString: '', senderClearEmailsString: '',
deleted: false, deleted: false,
isDeleted: false,
isUnseen: false,
isFlagged: false,
isAnswered: false,
isForwarded: false,
isReadReceipt: false,
focused: false, focused: false,
selected: false, selected: false,
checked: false, checked: false,
hasAttachments: false,
isHtml: false, isHtml: false,
hasImages: false, hasImages: false,
hasExternals: false,
isPgpSigned: false, pgpSigned: null,
isPgpEncrypted: false, pgpVerified: null,
pgpSignedVerifyStatus: SignedVerifyStatus.None,
pgpSignedVerifyUser: '', pgpEncrypted: null,
pgpDecrypted: false,
readReceipt: '', readReceipt: '',
@ -83,14 +87,24 @@ export class MessageModel extends AbstractModel {
}); });
this.attachments = ko.observableArray(new AttachmentCollectionModel); this.attachments = ko.observableArray(new AttachmentCollectionModel);
this.attachmentsSpecData = ko.observableArray();
this.threads = ko.observableArray(); this.threads = ko.observableArray();
this.unsubsribeLinks = ko.observableArray(); this.unsubsribeLinks = ko.observableArray();
this.flags = ko.observableArray();
this.addComputables({ this.addComputables({
attachmentIconClass: () => FileInfo.getCombinedIconClass(this.hasAttachments() ? this.attachmentsSpecData() : []), attachmentIconClass: () => FileInfo.getAttachmentsIconClass(this.attachments()),
threadsLen: () => this.threads().length, threadsLen: () => this.threads().length,
isImportant: () => MessagePriority.High === this.priority(), isImportant: () => MessagePriority.High === this.priority(),
hasAttachments: () => this.attachments().hasVisible(),
isDeleted: () => this.flags().includes('\\deleted'),
isUnseen: () => !this.flags().includes('\\seen') /* || this.flags().includes('\\unseen')*/,
isFlagged: () => this.flags().includes('\\flagged'),
isAnswered: () => this.flags().includes('\\answered'),
isForwarded: () => this.flags().includes('$forwarded'),
isReadReceipt: () => this.flags().includes('$mdnsent')
// isJunk: () => this.flags().includes('$junk') && !this.flags().includes('$nonjunk'),
// isPhishing: () => this.flags().includes('$phishing')
}); });
} }
@ -99,7 +113,6 @@ export class MessageModel extends AbstractModel {
this.uid = 0; this.uid = 0;
this.hash = ''; this.hash = '';
this.requestHash = ''; this.requestHash = '';
this.externalProxy = false;
this.emails = []; this.emails = [];
this.from = new EmailCollectionModel; this.from = new EmailCollectionModel;
this.to = new EmailCollectionModel; this.to = new EmailCollectionModel;
@ -117,6 +130,8 @@ export class MessageModel extends AbstractModel {
clear() { clear() {
this._reset(); this._reset();
this.subject(''); this.subject('');
this.html('');
this.plain('');
this.size(0); this.size(0);
this.spamScore(0); this.spamScore(0);
this.spamResult(''); this.spamResult('');
@ -129,26 +144,20 @@ export class MessageModel extends AbstractModel {
this.senderClearEmailsString(''); this.senderClearEmailsString('');
this.deleted(false); this.deleted(false);
this.isDeleted(false);
this.isUnseen(false);
this.isFlagged(false);
this.isAnswered(false);
this.isForwarded(false);
this.isReadReceipt(false);
this.selected(false); this.selected(false);
this.checked(false); this.checked(false);
this.hasAttachments(false);
this.attachmentsSpecData([]);
this.isHtml(false); this.isHtml(false);
this.hasImages(false); this.hasImages(false);
this.hasExternals(false);
this.attachments(new AttachmentCollectionModel); this.attachments(new AttachmentCollectionModel);
this.isPgpSigned(false); this.pgpSigned(null);
this.isPgpEncrypted(false); this.pgpVerified(null);
this.pgpSignedVerifyStatus(SignedVerifyStatus.None);
this.pgpSignedVerifyUser(''); this.pgpEncrypted(null);
this.pgpDecrypted(false);
this.priority(MessagePriority.Normal); this.priority(MessagePriority.Normal);
this.readReceipt(''); this.readReceipt('');
@ -160,6 +169,11 @@ export class MessageModel extends AbstractModel {
this.hasFlaggedSubMessage(false); this.hasFlaggedSubMessage(false);
} }
spamStatus() {
let spam = this.spamResult();
return spam ? i18n(this.isSpam() ? 'GLOBAL/SPAM' : 'GLOBAL/NOT_SPAM') + ': ' + spam : '';
}
/** /**
* @param {Array} properties * @param {Array} properties
* @returns {Array} * @returns {Array}
@ -178,7 +192,7 @@ export class MessageModel extends AbstractModel {
} }
computeSenderEmail() { computeSenderEmail() {
const list = [FolderUserStore.sentFolder(), FolderUserStore.draftFolder()].includes(this.folder) ? 'to' : 'from'; const list = [FolderUserStore.sentFolder(), FolderUserStore.draftsFolder()].includes(this.folder) ? 'to' : 'from';
this.senderEmailsString(this[list].toString(true)); this.senderEmailsString(this[list].toString(true));
this.senderClearEmailsString(this[list].toStringClear()); this.senderClearEmailsString(this[list].toStringClear());
} }
@ -192,8 +206,8 @@ export class MessageModel extends AbstractModel {
json.Priority = MessagePriority.Normal; json.Priority = MessagePriority.Normal;
} }
if (super.revivePropertiesFromJson(json)) { if (super.revivePropertiesFromJson(json)) {
// this.foundedCIDs = isArray(json.FoundedCIDs) ? json.FoundedCIDs : []; // this.foundCIDs = isArray(json.FoundCIDs) ? json.FoundCIDs : [];
// this.attachments(AttachmentCollectionModel.reviveFromJson(json.Attachments, this.foundedCIDs)); // this.attachments(AttachmentCollectionModel.reviveFromJson(json.Attachments, this.foundCIDs));
this.computeSenderEmail(); this.computeSenderEmail();
} }
@ -275,7 +289,7 @@ export class MessageModel extends AbstractModel {
*/ */
lineAsCss() { lineAsCss() {
let classes = []; let classes = [];
Object.entries({ forEachObjectEntry({
deleted: this.deleted(), deleted: this.deleted(),
'deleted-mark': this.isDeleted(), 'deleted-mark': this.isDeleted(),
selected: this.selected(), selected: this.selected(),
@ -286,12 +300,12 @@ export class MessageModel extends AbstractModel {
forwarded: this.isForwarded(), forwarded: this.isForwarded(),
focused: this.focused(), focused: this.focused(),
important: this.isImportant(), important: this.isImportant(),
withAttachments: this.hasAttachments(), withAttachments: !!this.attachments().length,
emptySubject: !this.subject(), emptySubject: !this.subject(),
// hasChildrenMessage: 1 < this.threadsLen(), // hasChildrenMessage: 1 < this.threadsLen(),
hasUnseenSubMessage: this.hasUnseenSubMessage(), hasUnseenSubMessage: this.hasUnseenSubMessage(),
hasFlaggedSubMessage: this.hasFlaggedSubMessage() hasFlaggedSubMessage: this.hasFlaggedSubMessage()
}).forEach(([key, value]) => value && classes.push(key)); }, (key, value) => value && classes.push(key));
return classes.join(' '); return classes.join(' ');
} }
@ -364,35 +378,138 @@ export class MessageModel extends AbstractModel {
return [toResult, ccResult]; return [toResult, ccResult];
} }
/** viewHtml() {
* @param {boolean=} print = false const body = this.body;
*/ if (body && this.html()) {
const contentLocationUrls = {},
oAttachments = this.attachments();
// Get contentLocationUrls
oAttachments.forEach(oAttachment => {
if (oAttachment.cid && oAttachment.contentLocation) {
contentLocationUrls[oAttachment.contentId()] = oAttachment.contentLocation;
}
});
let result = cleanHtml(this.html(), contentLocationUrls, SettingsUserStore.removeColors());
this.hasExternals(result.hasExternals);
// this.hasInternals = result.foundCIDs.length || result.foundContentLocationUrls.length;
this.hasImages(body.rlHasImages = !!result.hasExternals);
// Hide valid inline attachments in message view 'attachments' section
oAttachments.forEach(oAttachment => {
let cid = oAttachment.contentId(),
found = result.foundCIDs.includes(cid);
oAttachment.isInline(found);
oAttachment.isLinked(found || result.foundContentLocationUrls.includes(oAttachment.contentLocation));
});
body.innerHTML = result.html;
body.classList.toggle('html', 1);
body.classList.toggle('plain', 0);
// showInternalImages
const findAttachmentByCid = cid => this.attachments().findByCid(cid);
body.querySelectorAll('[data-x-src-cid],[data-x-src-location],[data-x-style-cid]').forEach(el => {
const data = el.dataset;
if (data.xSrcCid) {
const attachment = findAttachmentByCid(data.xSrcCid);
if (attachment && attachment.download) {
el.src = attachment.linkPreview();
}
} else if (data.xSrcLocation) {
const attachment = this.attachments.find(item => data.xSrcLocation === item.contentLocation)
|| findAttachmentByCid(data.xSrcLocation);
if (attachment && attachment.download) {
el.loading = 'lazy';
el.src = attachment.linkPreview();
}
} else if (data.xStyleCid) {
forEachObjectEntry(JSON.parse(data.xStyleCid), (name, cid) => {
const attachment = findAttachmentByCid(cid);
if (attachment && attachment.linkPreview && name) {
el.style[name] = "url('" + attachment.linkPreview() + "')";
}
});
}
});
if (SettingsUserStore.showImages()) {
this.showExternalImages();
}
this.isHtml(true);
this.initView();
return true;
}
}
viewPlain() {
const body = this.body;
if (body && this.plain()) {
body.classList.toggle('html', 0);
body.classList.toggle('plain', 1);
body.innerHTML = plainToHtml(
this.plain()
.replace(/-----BEGIN PGP (SIGNED MESSAGE-----(\r?\n[a-z][^\r\n]+)+|SIGNATURE-----[\s\S]*)/, '')
.trim()
)
.replace(url, '$1<a href="$2" target="_blank">$2</a>')
.replace(email, '$1<a href="mailto:$2">$2</a>');
this.isHtml(false);
this.hasImages(false);
this.initView();
return true;
}
}
initView() {
// init BlockquoteSwitcher
this.body.querySelectorAll('blockquote:not(.rl-bq-switcher)').forEach(node => {
node.removeAttribute('style')
if (node.textContent.trim() && !node.parentNode.closest('blockquote')) {
let h = node.clientHeight || getRealHeight(node);
if (0 === h || 100 < h) {
const el = Element.fromHTML('<span class="rlBlockquoteSwitcher">•••</span>');
node.classList.add('rl-bq-switcher','hidden-bq');
node.before(el);
el.addEventListener('click', () => node.classList.toggle('hidden-bq'));
}
}
});
}
viewPopupMessage(print) { viewPopupMessage(print) {
const timeStampInUTC = this.dateTimeStampInUTC() || 0, const timeStampInUTC = this.dateTimeStampInUTC() || 0,
ccLine = this.ccToLine(false), ccLine = this.ccToLine(false),
m = 0 < timeStampInUTC ? new Date(timeStampInUTC * 1000) : null, m = 0 < timeStampInUTC ? new Date(timeStampInUTC * 1000) : null,
win = open(''), win = open(''),
doc = win.document; sdoc = win.document;
doc.write(PreviewHTML let subject = encodeHtml(this.subject()),
.replace(/{{subject}}/g, encodeHtml(this.subject())) mode = this.isHtml() ? 'div' : 'pre',
.replace('{{date}}', encodeHtml(m ? m.format('LLL') : '')) cc = ccLine ? `<div>${encodeHtml(i18n('GLOBAL/CC'))}: ${encodeHtml(ccLine)}</div>` : '',
.replace('{{fromCreds}}', encodeHtml(this.fromToLine(false))) style = getComputedStyle(doc.querySelector('.messageView')),
.replace('{{toCreds}}', encodeHtml(this.toToLine(false))) prop = property => style.getPropertyValue(property);
.replace('{{toLabel}}', encodeHtml(i18n('GLOBAL/TO'))) sdoc.write(PreviewHTML
.replace('{{ccHide}}', ccLine ? '' : 'hidden=""') .replace('<title>', '<title>'+subject)
.replace('{{ccCreds}}', encodeHtml(ccLine)) // eslint-disable-next-line max-len
.replace('{{ccLabel}}', encodeHtml(i18n('GLOBAL/CC'))) .replace('<body>', `<body style="background-color:${prop('background-color')};color:${prop('color')}"><header><h1>${subject}</h1><time>${encodeHtml(m ? m.format('LLL') : '')}</time><div>${encodeHtml(this.fromToLine(false))}</div><div>${encodeHtml(i18n('GLOBAL/TO'))}: ${encodeHtml(this.toToLine(false))}</div>${cc}</header><${mode}>${this.bodyAsHTML()}</${mode}>`)
.replace('{{bodyClass}}', this.isHtml() ? 'html' : 'plain')
.replace('{{html}}', this.bodyAsHTML())
); );
sdoc.close();
doc.close();
if (print) { if (print) {
setTimeout(() => win.print(), 100); setTimeout(() => win.print(), 100);
} }
} }
/**
* @param {boolean=} print = false
*/
popupMessage() {
this.viewPopupMessage(false);
}
printMessage() { printMessage() {
this.viewPopupMessage(true); this.viewPopupMessage(true);
} }
@ -408,71 +525,61 @@ export class MessageModel extends AbstractModel {
* @param {MessageModel} message * @param {MessageModel} message
* @returns {MessageModel} * @returns {MessageModel}
*/ */
populateByMessageListItem(message) { static fromMessageListItem(message) {
if (message) { let self = new MessageModel();
this.folder = message.folder;
this.uid = message.uid;
this.hash = message.hash;
this.requestHash = message.requestHash;
this.subject(message.subject());
this.size(message.size());
this.spamScore(message.spamScore());
this.spamResult(message.spamResult());
this.isSpam(message.isSpam());
this.hasVirus(message.hasVirus());
this.dateTimeStampInUTC(message.dateTimeStampInUTC());
this.priority(message.priority());
this.externalProxy = message.externalProxy;
this.emails = message.emails;
this.from = message.from;
this.to = message.to;
this.cc = message.cc;
this.bcc = message.bcc;
this.replyTo = message.replyTo;
this.deliveredTo = message.deliveredTo;
this.unsubsribeLinks(message.unsubsribeLinks);
this.isUnseen(message.isUnseen());
this.isFlagged(message.isFlagged());
this.isAnswered(message.isAnswered());
this.isForwarded(message.isForwarded());
this.isReadReceipt(message.isReadReceipt());
this.isDeleted(message.isDeleted());
this.priority(message.priority());
this.selected(message.selected());
this.checked(message.checked());
this.hasAttachments(message.hasAttachments());
this.attachmentsSpecData(message.attachmentsSpecData());
}
this.body = null;
this.draftInfo = [];
this.messageId = '';
this.inReplyTo = '';
this.references = '';
if (message) { if (message) {
this.threads(message.threads()); self.folder = message.folder;
self.uid = message.uid;
self.hash = message.hash;
self.requestHash = message.requestHash;
self.subject(message.subject());
self.plain(message.plain());
self.html(message.html());
self.size(message.size());
self.spamScore(message.spamScore());
self.spamResult(message.spamResult());
self.isSpam(message.isSpam());
self.hasVirus(message.hasVirus());
self.dateTimeStampInUTC(message.dateTimeStampInUTC());
self.priority(message.priority());
self.hasExternals(message.hasExternals());
self.emails = message.emails;
self.from = message.from;
self.to = message.to;
self.cc = message.cc;
self.bcc = message.bcc;
self.replyTo = message.replyTo;
self.deliveredTo = message.deliveredTo;
self.unsubsribeLinks(message.unsubsribeLinks);
self.flags(message.flags());
self.priority(message.priority());
self.selected(message.selected());
self.checked(message.checked());
self.attachments(message.attachments());
self.threads(message.threads());
} }
this.computeSenderEmail(); self.computeSenderEmail();
return this; return self;
} }
showExternalImages() { showExternalImages() {
if (this.body && this.body.rlHasImages) { const body = this.body;
if (body && this.hasImages()) {
this.hasImages(false); this.hasImages(false);
this.body.rlHasImages = false; body.rlHasImages = false;
let body = this.body, attr = this.externalProxy ? 'data-x-additional-src' : 'data-x-src'; let attr = 'data-x-src';
body.querySelectorAll('[' + attr + ']').forEach(node => { body.querySelectorAll('[' + attr + ']').forEach(node => {
if (node.matches('img')) { if (node.matches('img')) {
node.loading = 'lazy'; node.loading = 'lazy';
@ -480,74 +587,28 @@ export class MessageModel extends AbstractModel {
node.src = node.getAttribute(attr); node.src = node.getAttribute(attr);
}); });
attr = this.externalProxy ? 'data-x-additional-style-url' : 'data-x-style-url'; body.querySelectorAll('[data-x-style-url]').forEach(node => {
body.querySelectorAll('[' + attr + ']').forEach(node => { forEachObjectEntry(JSON.parse(node.dataset.xStyleUrl), (name, url) => node.style[name] = "url('" + url + "')");
node.setAttribute('style', ((node.getAttribute('style')||'')
+ ';' + node.getAttribute(attr))
.replace(/^[;\s]+/,''));
}); });
} }
} }
showInternalImages() {
const body = this.body;
if (body && !body.rlInitInternalImages) {
const findAttachmentByCid = cid => this.attachments().findByCid(cid);
body.rlInitInternalImages = true;
body.querySelectorAll('[data-x-src-cid],[data-x-src-location],[data-x-style-cid]').forEach(el => {
const data = el.dataset;
if (data.xSrcCid) {
const attachment = findAttachmentByCid(data.xSrcCid);
if (attachment && attachment.download) {
el.src = attachment.linkPreview();
}
} else if (data.xSrcLocation) {
const attachment = this.attachments.find(item => data.xSrcLocation === item.contentLocation)
|| findAttachmentByCid(data.xSrcLocation);
if (attachment && attachment.download) {
el.loading = 'lazy';
el.src = attachment.linkPreview();
}
} else if (data.xStyleCid) {
const name = data.xStyleCidName,
attachment = findAttachmentByCid(data.xStyleCid);
if (attachment && attachment.linkPreview && name) {
el.setAttribute('style', name + ": url('" + attachment.linkPreview() + "');"
+ (el.getAttribute('style') || ''));
}
}
});
}
}
fetchDataFromDom() {
if (this.body) {
this.isHtml(!!this.body.rlIsHtml);
this.hasImages(!!this.body.rlHasImages);
}
}
/** /**
* @returns {string} * @returns {string}
*/ */
bodyAsHTML() { bodyAsHTML() {
if (this.body) { // if (this.body && !this.body.querySelector('iframe[src*=decrypt]')) {
let clone = this.body.cloneNode(true), if (this.body && !this.body.querySelector('iframe')) {
attr = 'data-html-editor-font-wrapper'; let clone = this.body.cloneNode(true);
clone.querySelectorAll('blockquote.rl-bq-switcher').forEach( clone.querySelectorAll('blockquote.rl-bq-switcher').forEach(
node => node.classList.remove('rl-bq-switcher','hidden-bq') node => node.classList.remove('rl-bq-switcher','hidden-bq')
); );
clone.querySelectorAll('.rlBlockquoteSwitcher').forEach( clone.querySelectorAll('.rlBlockquoteSwitcher').forEach(
node => node.remove() node => node.remove()
); );
clone.querySelectorAll('['+attr+']').forEach(
node => node.removeAttribute(attr)
);
return clone.innerHTML; return clone.innerHTML;
} }
return ''; return this.html() || plainToHtml(this.plain());
} }
/** /**
@ -564,4 +625,5 @@ export class MessageModel extends AbstractModel {
this.isReadReceipt() this.isReadReceipt()
].join(','); ].join(',');
} }
} }

View file

@ -1,74 +0,0 @@
import ko from 'ko';
import { arrayLength } from 'Common/Utils';
import { AbstractModel } from 'Knoin/AbstractModel';
import { PgpUserStore } from 'Stores/User/Pgp';
export class OpenPgpKeyModel extends AbstractModel {
/**
* @param {string} index
* @param {string} guID
* @param {string} ID
* @param {array} IDs
* @param {array} userIDs
* @param {array} emails
* @param {boolean} isPrivate
* @param {string} armor
* @param {string} userID
*/
constructor(index, guID, ID, IDs, userIDs, emails, isPrivate, armor, userID) {
super();
this.index = index;
this.id = ID;
this.ids = arrayLength(IDs) ? IDs : [ID];
this.guid = guID;
this.user = '';
this.users = userIDs;
this.email = '';
this.emails = emails;
this.armor = armor;
this.isPrivate = !!isPrivate;
this.selectUser(userID);
this.deleteAccess = ko.observable(false);
}
getNativeKey() {
let key = null;
try {
key = PgpUserStore.openpgp.key.readArmored(this.armor);
if (key && !key.err && key.keys && key.keys[0]) {
return key;
}
} catch (e) {
console.log(e);
}
return null;
}
getNativeKeys() {
const key = this.getNativeKey();
return key && key.keys ? key.keys : null;
}
select(pattern, property) {
if (this[property]) {
const index = this[property].indexOf(pattern);
if (-1 !== index) {
this.user = this.users[index];
this.email = this.emails[index];
}
}
}
selectUser(user) {
this.select(user, 'users');
}
selectEmail(email) {
this.select(email, 'emails');
}
}

View file

@ -1,30 +1,28 @@
import { Notification } from 'Common/Enums'; import { Notification } from 'Common/Enums';
import { isArray, pInt, pString } from 'Common/Utils'; import { isArray, pInt, pString } from 'Common/Utils';
import { serverRequest } from 'Common/Links'; import { serverRequest } from 'Common/Links';
import { getNotification } from 'Common/Translator';
let iJsonErrorCount = 0, let iJsonErrorCount = 0;
iTokenErrorCount = 0;
const getURL = (add = '') => serverRequest('Json') + add, const getURL = (add = '') => serverRequest('Json') + add,
checkResponseError = data => { checkResponseError = data => {
const err = data ? data.ErrorCode : null; const err = data ? data.ErrorCode : null;
if (Notification.InvalidToken === err && 10 < ++iTokenErrorCount) { if (Notification.InvalidToken === err) {
alert(getNotification(err));
rl.logoutReload(); rl.logoutReload();
} else { } else if ([
if ([ Notification.AuthError,
Notification.AuthError, Notification.ConnectionError,
Notification.ConnectionError, Notification.DomainNotAllowed,
Notification.DomainNotAllowed, Notification.AccountNotAllowed,
Notification.AccountNotAllowed, Notification.MailServerError,
Notification.MailServerError, Notification.UnknownNotification,
Notification.UnknownNotification, Notification.UnknownError
Notification.UnknownError ].includes(err)
].includes(err) ) {
) { if (7 < ++iJsonErrorCount) {
++iJsonErrorCount;
}
if (data.Logout || 7 < iJsonErrorCount) {
rl.logoutReload(); rl.logoutReload();
} }
} }
@ -78,6 +76,45 @@ export class AbstractFetchRemote
return this; return this;
} }
/**
* Allows quicker visual responses to the user.
* Can be used to stream lines of json encoded data, but does not work on all servers.
* Apache needs 'flushpackets' like in <Proxy "fcgi://...." flushpackets=on></Proxy>
*/
streamPerLine(fCallback, sGetAdd) {
rl.fetch(getURL(sGetAdd))
.then(response => response.body)
.then(body => {
// Firefox TextDecoderStream is not defined
// const reader = body.pipeThrough(new TextDecoderStream()).getReader();
const reader = body.getReader(),
re = /\r\n|\n|\r/gm,
utf8decoder = new TextDecoder();
let buffer = '';
function processText({ done, value }) {
buffer += value ? utf8decoder.decode(value, {stream: true}) : '';
for (;;) {
let result = re.exec(buffer);
if (!result) {
if (done) {
break;
}
reader.read().then(processText);
return;
}
fCallback(buffer.slice(0, result.index));
buffer = buffer.slice(result.index + 1);
re.lastIndex = 0;
}
if (buffer.length) {
// last line didn't end in a newline char
fCallback(buffer);
}
}
reader.read().then(processText);
})
}
/** /**
* @param {?Function} fCallback * @param {?Function} fCallback
* @param {string} sAction * @param {string} sAction
@ -86,7 +123,7 @@ export class AbstractFetchRemote
* @param {string=} sGetAdd = '' * @param {string=} sGetAdd = ''
* @param {Array=} aAbortActions = [] * @param {Array=} aAbortActions = []
*/ */
defaultRequest(fCallback, sAction, params, iTimeout, sGetAdd, abortActions) { request(sAction, fCallback, params, iTimeout, sGetAdd, abortActions) {
params = params || {}; params = params || {};
const start = Date.now(); const start = Date.now();
@ -119,7 +156,7 @@ export class AbstractFetchRemote
} }
*/ */
if (data.Result) { if (data.Result) {
iJsonErrorCount = iTokenErrorCount = 0; iJsonErrorCount = 0;
} else { } else {
checkResponseError(data); checkResponseError(data);
iError = data.ErrorCode || Notification.UnknownError iError = data.ErrorCode || Notification.UnknownError
@ -141,32 +178,11 @@ export class AbstractFetchRemote
}); });
} }
/**
* @param {?Function} fCallback
*/
noop(fCallback) {
this.defaultRequest(fCallback, 'Noop');
}
/** /**
* @param {?Function} fCallback * @param {?Function} fCallback
*/ */
getPublicKey(fCallback) { getPublicKey(fCallback) {
this.defaultRequest(fCallback, 'GetPublicKey'); this.request('GetPublicKey', fCallback);
}
/**
* @param {?Function} fCallback
* @param {string} sVersion
*/
jsVersion(fCallback, sVersion) {
this.defaultRequest(fCallback, 'Version', {
Version: sVersion
});
}
fastResolve(mData) {
return Promise.resolve(mData);
} }
setTrigger(trigger, value) { setTrigger(trigger, value) {
@ -178,7 +194,7 @@ export class AbstractFetchRemote
} }
} }
postRequest(action, fTrigger, params, timeOut) { post(action, fTrigger, params, timeOut) {
this.setTrigger(fTrigger, true); this.setTrigger(fTrigger, true);
return fetchJSON(action, '', params, pInt(timeOut, 30000), return fetchJSON(action, '', params, pInt(timeOut, 30000),
data => { data => {

View file

@ -1,206 +1,14 @@
import { AbstractFetchRemote } from 'Remote/AbstractFetch'; import { AbstractFetchRemote } from 'Remote/AbstractFetch';
class RemoteAdminFetch extends AbstractFetchRemote { class RemoteAdminFetch extends AbstractFetchRemote {
/**
* @param {?Function} fCallback
* @param {string} sLogin
* @param {string} sPassword
*/
adminLogin(fCallback, sLogin, sPassword, sCode) {
this.defaultRequest(fCallback, 'AdminLogin', {
Login: sLogin,
Password: sPassword,
TOTP: sCode
});
}
/** /**
* @param {string} key
* @param {?scalar} value
* @param {?Function} fCallback * @param {?Function} fCallback
*/ */
adminLogout(fCallback) { saveSetting(key, value, fCallback) {
this.defaultRequest(fCallback, 'AdminLogout'); this.request('AdminSettingsUpdate', fCallback, {[key]: value});
}
/**
* @param {?Function} fCallback
* @param {?} oData
*/
saveAdminConfig(fCallback, oData) {
this.defaultRequest(fCallback, 'AdminSettingsUpdate', oData);
}
/**
* @param {?Function} fCallback
* @param {boolean=} bIncludeAliases = true
*/
domainList(fCallback, bIncludeAliases = true) {
this.defaultRequest(fCallback, 'AdminDomainList', {
IncludeAliases: bIncludeAliases ? 1 : 0
});
}
/**
* @param {?Function} fCallback
*/
packagesList(fCallback) {
this.defaultRequest(fCallback, 'AdminPackagesList');
}
/**
* @param {?Function} fCallback
* @param {Object} oPackage
*/
packageInstall(fCallback, oPackage) {
this.defaultRequest(
fCallback,
'AdminPackageInstall',
{
Id: oPackage.id,
Type: oPackage.type,
File: oPackage.file
},
60000
);
}
/**
* @param {?Function} fCallback
* @param {Object} oPackage
*/
packageDelete(fCallback, oPackage) {
this.defaultRequest(fCallback, 'AdminPackageDelete', {
Id: oPackage.id
});
}
/**
* @param {?Function} fCallback
* @param {string} sName
*/
domain(fCallback, sName) {
this.defaultRequest(fCallback, 'AdminDomainLoad', {
Name: sName
});
}
/**
* @param {?Function} fCallback
* @param {string} sId
*/
plugin(fCallback, sId) {
this.defaultRequest(fCallback, 'AdminPluginLoad', {
Id: sId
});
}
/**
* @param {?Function} fCallback
* @param {string} sName
*/
domainDelete(fCallback, sName) {
this.defaultRequest(fCallback, 'AdminDomainDelete', {
Name: sName
});
}
/**
* @param {?Function} fCallback
* @param {string} sName
* @param {boolean} bDisabled
*/
domainDisable(fCallback, sName, bDisabled) {
this.defaultRequest(fCallback, 'AdminDomainDisable', {
Name: sName,
Disabled: bDisabled ? 1 : 0
});
}
/**
* @param {?Function} fCallback
* @param {Object} oConfig
*/
pluginSettingsUpdate(fCallback, oConfig) {
this.defaultRequest(fCallback, 'AdminPluginSettingsUpdate', oConfig);
}
/**
* @param {?Function} fCallback
* @param {string} sId
* @param {boolean} bDisabled
*/
pluginDisable(fCallback, sId, bDisabled) {
this.defaultRequest(fCallback, 'AdminPluginDisable', {
Id: sId,
Disabled: bDisabled ? 1 : 0
});
}
createDomainAlias(fCallback, sName, sAlias) {
this.defaultRequest(fCallback, 'AdminDomainAliasSave', {
Name: sName,
Alias: sAlias
});
}
createOrUpdateDomain(fCallback, oDomain) {
this.defaultRequest(fCallback, 'AdminDomainSave', {
Create: oDomain.edit() ? 0 : 1,
Name: oDomain.name(),
IncHost: oDomain.imapServer(),
IncPort: oDomain.imapPort(),
IncSecure: oDomain.imapSecure(),
IncShortLogin: oDomain.imapShortLogin() ? 1 : 0,
UseSieve: oDomain.useSieve() ? 1 : 0,
SieveHost: oDomain.sieveServer(),
SievePort: oDomain.sievePort(),
SieveSecure: oDomain.sieveSecure(),
OutHost: oDomain.smtpServer(),
OutPort: oDomain.smtpPort(),
OutSecure: oDomain.smtpSecure(),
OutShortLogin: oDomain.smtpShortLogin() ? 1 : 0,
OutAuth: oDomain.smtpAuth() ? 1 : 0,
OutSetSender: oDomain.smtpSetSender() ? 1 : 0,
OutUsePhpMail: oDomain.smtpPhpMail() ? 1 : 0,
WhiteList: oDomain.whiteList()
});
}
testConnectionForDomain(fCallback, oDomain) {
this.defaultRequest(fCallback, 'AdminDomainTest', {
Name: oDomain.name(),
IncHost: oDomain.imapServer(),
IncPort: oDomain.imapPort(),
IncSecure: oDomain.imapSecure(),
UseSieve: oDomain.useSieve() ? 1 : 0,
SieveHost: oDomain.sieveServer(),
SievePort: oDomain.sievePort(),
SieveSecure: oDomain.sieveSecure(),
OutHost: oDomain.smtpServer(),
OutPort: oDomain.smtpPort(),
OutSecure: oDomain.smtpSecure(),
OutAuth: oDomain.smtpAuth() ? 1 : 0,
OutUsePhpMail: oDomain.smtpPhpMail() ? 1 : 0
});
}
/**
* @param {?Function} fCallback
* @param {?} oData
*/
testContacts(fCallback, oData) {
this.defaultRequest(fCallback, 'AdminContactsTest', oData);
}
/**
* @param {?Function} fCallback
* @param {?} oData
*/
saveNewAdminPassword(fCallback, oData) {
this.defaultRequest(fCallback, 'AdminPasswordUpdate', oData);
} }
} }

View file

@ -1,11 +1,9 @@
import { isArray, arrayLength, pString, pInt } from 'Common/Utils'; import { pString, pInt, b64EncodeJSONSafe } from 'Common/Utils';
import { import {
getFolderHash, getFolderHash,
getFolderInboxName,
getFolderUidNext, getFolderUidNext,
getFolderFromCacheList, getFolderFromCacheList
MessageFlagsCache
} from 'Common/Cache'; } from 'Common/Cache';
import { SettingsGet } from 'Common/Globals'; import { SettingsGet } from 'Common/Globals';
@ -17,173 +15,7 @@ import { FolderUserStore } from 'Stores/User/Folder';
import { AbstractFetchRemote } from 'Remote/AbstractFetch'; import { AbstractFetchRemote } from 'Remote/AbstractFetch';
import { FolderCollectionModel } from 'Model/FolderCollection';
//const toUTF8 = window.TextEncoder
// ? text => String.fromCharCode(...new TextEncoder().encode(text))
// : text => unescape(encodeURIComponent(text)),
const urlsafeArray = array => btoa(unescape(encodeURIComponent(array.join('\x00').replace(/\r\n/g, '\n'))))
.replace('+', '-')
.replace('/', '_')
.replace('=', '');
class RemoteUserFetch extends AbstractFetchRemote { class RemoteUserFetch extends AbstractFetchRemote {
/**
* @param {?Function} fCallback
*/
folders(fCallback) {
this.defaultRequest(
fCallback,
'Folders',
{
SentFolder: SettingsGet('SentFolder'),
DraftFolder: SettingsGet('DraftFolder'),
SpamFolder: SettingsGet('SpamFolder'),
TrashFolder: SettingsGet('TrashFolder'),
ArchiveFolder: SettingsGet('ArchiveFolder')
},
null,
'',
['Folders']
);
}
/**
* @param {?Function} fCallback
* @param {FormData} oData
*/
login(fCallback, oData) {
this.defaultRequest(fCallback, 'Login', oData);
}
/**
* @param {?Function} fCallback
*/
contactsSync(fCallback) {
this.defaultRequest(fCallback, 'ContactsSync', null, 200000);
}
/**
* @param {?Function} fCallback
* @param {boolean} bEnable
* @param {string} sUrl
* @param {string} sUser
* @param {string} sPassword
*/
saveContactsSyncData(fCallback, bEnable, sUrl, sUser, sPassword) {
this.defaultRequest(fCallback, 'SaveContactsSyncData', {
Enable: bEnable ? 1 : 0,
Url: sUrl,
User: sUser,
Password: sPassword
});
}
/**
* @param {?Function} fCallback
* @param {string} sEmail
* @param {string} sPassword
* @param {boolean=} bNew
*/
accountSetup(fCallback, sEmail, sPassword, bNew = true) {
this.defaultRequest(fCallback, 'AccountSetup', {
Email: sEmail,
Password: sPassword,
New: bNew ? 1 : 0
});
}
/**
* @param {?Function} fCallback
* @param {string} sEmailToDelete
*/
accountDelete(fCallback, sEmailToDelete) {
this.defaultRequest(fCallback, 'AccountDelete', {
EmailToDelete: sEmailToDelete
});
}
/**
* @param {?Function} fCallback
* @param {Array} aAccounts
* @param {Array} aIdentities
*/
accountsAndIdentitiesSortOrder(fCallback, aAccounts, aIdentities) {
this.defaultRequest(fCallback, 'AccountsAndIdentitiesSortOrder', {
Accounts: aAccounts,
Identities: aIdentities
});
}
/**
* @param {?Function} fCallback
* @param {string} sId
* @param {string} sEmail
* @param {string} sName
* @param {string} sReplyTo
* @param {string} sBcc
* @param {string} sSignature
* @param {boolean} bSignatureInsertBefore
*/
identityUpdate(fCallback, sId, sEmail, sName, sReplyTo, sBcc, sSignature, bSignatureInsertBefore) {
this.defaultRequest(fCallback, 'IdentityUpdate', {
Id: sId,
Email: sEmail,
Name: sName,
ReplyTo: sReplyTo,
Bcc: sBcc,
Signature: sSignature,
SignatureInsertBefore: bSignatureInsertBefore ? 1 : 0
});
}
/**
* @param {?Function} fCallback
* @param {string} sIdToDelete
*/
identityDelete(fCallback, sIdToDelete) {
this.defaultRequest(fCallback, 'IdentityDelete', {
IdToDelete: sIdToDelete
});
}
/**
* @param {?Function} fCallback
*/
accountsAndIdentities(fCallback) {
this.defaultRequest(fCallback, 'AccountsAndIdentities');
}
/**
* @param {?Function} fCallback
* @param {SieveScriptModel} script
*/
filtersScriptSave(fCallback, script) {
this.defaultRequest(fCallback, 'FiltersScriptSave', script.toJson());
}
/**
* @param {?Function} fCallback
* @param {string} name
*/
filtersScriptActivate(fCallback, name) {
this.defaultRequest(fCallback, 'FiltersScriptActivate', {name:name});
}
/**
* @param {?Function} fCallback
* @param {string} name
*/
filtersScriptDelete(fCallback, name) {
this.defaultRequest(fCallback, 'FiltersScriptDelete', {name:name});
}
/**
* @param {?Function} fCallback
*/
filtersGet(fCallback) {
this.defaultRequest(fCallback, 'Filters', {});
}
/** /**
* @param {?Function} fCallback * @param {?Function} fCallback
@ -233,23 +65,23 @@ class RemoteUserFetch extends AbstractFetchRemote {
*/ */
messageList(fCallback, params, bSilent = false) { messageList(fCallback, params, bSilent = false) {
const const
sFolderFullNameRaw = pString(params.Folder), sFolderFullName = pString(params.Folder),
folderHash = getFolderHash(sFolderFullNameRaw), folderHash = getFolderHash(sFolderFullName);
useThreads = AppUserStore.threadsAllowed() && SettingsUserStore.useThreads() ? 1 : 0,
inboxUidNext = getFolderInboxName() === sFolderFullNameRaw ? getFolderUidNext(sFolderFullNameRaw) : '';
params.Folder = sFolderFullNameRaw;
params.ThreadUid = useThreads ? params.ThreadUid : 0;
params = Object.assign({ params = Object.assign({
Folder: '',
Offset: 0, Offset: 0,
Limit: SettingsUserStore.messagesPerPage(), Limit: SettingsUserStore.messagesPerPage(),
Search: '', Search: '',
UidNext: inboxUidNext, UidNext: getFolderUidNext(sFolderFullName), // Used to check for new messages
UseThreads: useThreads, Sort: FolderUserStore.sortMode(),
ThreadUid: 0, Hash: folderHash + SettingsGet('AccountHash')
Sort: FolderUserStore.sortMode()
}, params); }, params);
params.Folder = sFolderFullName;
if (AppUserStore.threadsAllowed() && SettingsUserStore.useThreads()) {
params.UseThreads = 1;
} else {
params.ThreadUid = 0;
}
let sGetAdd = ''; let sGetAdd = '';
@ -257,13 +89,12 @@ class RemoteUserFetch extends AbstractFetchRemote {
sGetAdd = 'MessageList/' + sGetAdd = 'MessageList/' +
SUB_QUERY_PREFIX + SUB_QUERY_PREFIX +
'/' + '/' +
urlsafeArray([SettingsGet('ProjectHash'),folderHash].concat(Object.values(params))); b64EncodeJSONSafe(params);
params = {}; params = {};
} }
this.defaultRequest( this.request('MessageList',
fCallback, fCallback,
'MessageList',
params, params,
30000, 30000,
sGetAdd, sGetAdd,
@ -273,43 +104,27 @@ class RemoteUserFetch extends AbstractFetchRemote {
/** /**
* @param {?Function} fCallback * @param {?Function} fCallback
* @param {Array} aDownloads * @param {string} sFolderFullName
*/
messageUploadAttachments(fCallback, aDownloads) {
this.defaultRequest(
fCallback,
'MessageUploadAttachments',
{
Attachments: aDownloads
},
999000
);
}
/**
* @param {?Function} fCallback
* @param {string} sFolderFullNameRaw
* @param {number} iUid * @param {number} iUid
* @returns {boolean} * @returns {boolean}
*/ */
message(fCallback, sFolderFullNameRaw, iUid) { message(fCallback, sFolderFullName, iUid) {
sFolderFullNameRaw = pString(sFolderFullNameRaw); sFolderFullName = pString(sFolderFullName);
iUid = pInt(iUid); iUid = pInt(iUid);
if (getFolderFromCacheList(sFolderFullNameRaw) && 0 < iUid) { if (getFolderFromCacheList(sFolderFullName) && 0 < iUid) {
this.defaultRequest( this.request('Message',
fCallback, fCallback,
'Message',
{}, {},
null, null,
'Message/' + 'Message/' +
SUB_QUERY_PREFIX + SUB_QUERY_PREFIX +
'/' + '/' +
urlsafeArray([ b64EncodeJSONSafe([
sFolderFullNameRaw, sFolderFullName,
iUid, iUid,
SettingsGet('ProjectHash'), AppUserStore.threadsAllowed() && SettingsUserStore.useThreads() ? 1 : 0,
AppUserStore.threadsAllowed() && SettingsUserStore.useThreads() ? 1 : 0 SettingsGet('AccountHash')
]), ]),
['Message'] ['Message']
); );
@ -320,186 +135,12 @@ class RemoteUserFetch extends AbstractFetchRemote {
return false; return false;
} }
/**
* @param {?Function} fCallback
* @param {Array} aExternals
*/
composeUploadExternals(fCallback, aExternals) {
this.defaultRequest(
fCallback,
'ComposeUploadExternals',
{
Externals: aExternals
},
999000
);
}
/**
* @param {?Function} fCallback
* @param {string} sUrl
* @param {string} sAccessToken
*/
composeUploadDrive(fCallback, sUrl, sAccessToken) {
this.defaultRequest(
fCallback,
'ComposeUploadDrive',
{
AccessToken: sAccessToken,
Url: sUrl
},
999000
);
}
/**
* @param {?Function} fCallback
* @param {string} folder
* @param {Array=} list = []
*/
folderInformation(fCallback, folder, list = []) {
let request = true;
const uids = [];
if (arrayLength(list)) {
request = false;
list.forEach(messageListItem => {
if (!MessageFlagsCache.getFor(messageListItem.folder, messageListItem.uid)) {
uids.push(messageListItem.uid);
}
if (messageListItem.threads.length) {
messageListItem.threads.forEach(uid => {
if (!MessageFlagsCache.getFor(messageListItem.folder, uid)) {
uids.push(uid);
}
});
}
});
if (uids.length) {
request = true;
}
}
if (request) {
this.defaultRequest(fCallback, 'FolderInformation', {
Folder: folder,
FlagsUids: isArray(uids) ? uids : [],
UidNext: getFolderInboxName() === folder ? getFolderUidNext(folder) : 0
});
} else if (SettingsUserStore.useThreads()) {
rl.app.reloadFlagsCurrentMessageListAndMessageFromCache();
}
}
/**
* @param {?Function} fCallback
* @param {Array} aFolders
*/
folderInformationMultiply(fCallback, aFolders) {
this.defaultRequest(fCallback, 'FolderInformationMultiply', {
Folders: aFolders
});
}
/**
* @param {?Function} fCallback
*/
logout(fCallback) {
this.defaultRequest(fCallback, 'Logout');
}
/**
* @param {?Function} fCallback
* @param {string} sFolderFullNameRaw
* @param {Array} aUids
* @param {boolean} bSetFlagged
*/
messageSetFlagged(fCallback, sFolderFullNameRaw, aUids, bSetFlagged) {
this.defaultRequest(fCallback, 'MessageSetFlagged', {
Folder: sFolderFullNameRaw,
Uids: aUids.join(','),
SetAction: bSetFlagged ? 1 : 0
});
}
/**
* @param {?Function} fCallback
* @param {string} sFolderFullNameRaw
* @param {Array} aUids
* @param {boolean} bSetSeen
*/
messageSetSeen(fCallback, sFolderFullNameRaw, aUids, bSetSeen) {
this.defaultRequest(fCallback, 'MessageSetSeen', {
Folder: sFolderFullNameRaw,
Uids: aUids.join(','),
SetAction: bSetSeen ? 1 : 0
});
}
/**
* @param {?Function} fCallback
* @param {string} sFolderFullNameRaw
* @param {boolean} bSetSeen
* @param {Array} aThreadUids = null
*/
messageSetSeenToAll(fCallback, sFolderFullNameRaw, bSetSeen, aThreadUids = null) {
this.defaultRequest(fCallback, 'MessageSetSeenToAll', {
Folder: sFolderFullNameRaw,
SetAction: bSetSeen ? 1 : 0,
ThreadUids: aThreadUids ? aThreadUids.join(',') : ''
});
}
/**
* @param {?Function} fCallback
* @param {Object} oData
*/
saveMessage(fCallback, oData) {
this.defaultRequest(fCallback, 'SaveMessage', oData, 200000);
}
/**
* @param {?Function} fCallback
* @param {string} sMessageFolder
* @param {number} iMessageUid
* @param {string} sReadReceipt
* @param {string} sSubject
* @param {string} sText
*/
sendReadReceiptMessage(fCallback, sMessageFolder, iMessageUid, sReadReceipt, sSubject, sText) {
this.defaultRequest(fCallback, 'SendReadReceiptMessage', {
MessageFolder: sMessageFolder,
MessageUid: iMessageUid,
ReadReceipt: sReadReceipt,
Subject: sSubject,
Text: sText
});
}
/**
* @param {?Function} fCallback
* @param {Object} oData
*/
sendMessage(fCallback, oData) {
this.defaultRequest(fCallback, 'SendMessage', oData, 30000);
}
/**
* @param {?Function} fCallback
* @param {Object} oData
*/
saveSystemFolders(fCallback, oData) {
this.defaultRequest(fCallback, 'SystemFoldersUpdate', oData);
}
/** /**
* @param {?Function} fCallback * @param {?Function} fCallback
* @param {Object} oData * @param {Object} oData
*/ */
saveSettings(fCallback, oData) { saveSettings(fCallback, oData) {
this.defaultRequest(fCallback, 'SettingsUpdate', oData); this.request('SettingsUpdate', fCallback, oData);
} }
/** /**
@ -513,234 +154,15 @@ class RemoteUserFetch extends AbstractFetchRemote {
}); });
} }
/** /*
* @param {?Function} fCallback folderMove(sPrevFolderFullName, sNewFolderFullName, bSubscribe) {
* @param {string} sFolderFullNameRaw return this.post('FolderMove', FolderUserStore.foldersRenaming, {
*/ Folder: sPrevFolderFullName,
folderClear(fCallback, sFolderFullNameRaw) { NewFolder: sNewFolderFullName,
this.defaultRequest(fCallback, 'FolderClear', {
Folder: sFolderFullNameRaw
});
}
/**
* @param {?Function} fCallback
* @param {string} sFolderFullNameRaw
* @param {boolean} bSubscribe
*/
folderSetSubscribe(fCallback, sFolderFullNameRaw, bSubscribe) {
this.defaultRequest(fCallback, 'FolderSubscribe', {
Folder: sFolderFullNameRaw,
Subscribe: bSubscribe ? 1 : 0 Subscribe: bSubscribe ? 1 : 0
}); });
} }
*/
/**
* @param {?Function} fCallback
* @param {string} sFolderFullNameRaw
* @param {boolean} bCheckable
*/
folderSetCheckable(fCallback, sFolderFullNameRaw, bCheckable) {
this.defaultRequest(fCallback, 'FolderCheckable', {
Folder: sFolderFullNameRaw,
Checkable: bCheckable ? 1 : 0
});
}
/**
* @param {?Function} fCallback
* @param {string} sFolder
* @param {string} sToFolder
* @param {Array} aUids
* @param {string=} sLearning
* @param {boolean=} bMarkAsRead
*/
messagesMove(fCallback, sFolder, sToFolder, aUids, sLearning, bMarkAsRead) {
this.defaultRequest(
fCallback,
'MessageMove',
{
FromFolder: sFolder,
ToFolder: sToFolder,
Uids: aUids.join(','),
MarkAsRead: bMarkAsRead ? 1 : 0,
Learning: sLearning || ''
},
null,
'',
['MessageList']
);
}
/**
* @param {?Function} fCallback
* @param {string} sFolder
* @param {string} sToFolder
* @param {Array} aUids
*/
messagesCopy(fCallback, sFolder, sToFolder, aUids) {
this.defaultRequest(fCallback, 'MessageCopy', {
FromFolder: sFolder,
ToFolder: sToFolder,
Uids: aUids.join(',')
});
}
/**
* @param {?Function} fCallback
* @param {string} sFolder
* @param {Array} aUids
*/
messagesDelete(fCallback, sFolder, aUids) {
this.defaultRequest(
fCallback,
'MessageDelete',
{
Folder: sFolder,
Uids: aUids.join(',')
},
null,
'',
['MessageList']
);
}
/**
* @param {?Function} fCallback
*/
appDelayStart(fCallback) {
this.defaultRequest(fCallback, 'AppDelayStart');
}
/**
* @param {?Function} fCallback
*/
quota(fCallback) {
this.defaultRequest(fCallback, 'Quota');
}
/**
* @param {?Function} fCallback
* @param {number} iOffset
* @param {number} iLimit
* @param {string} sSearch
*/
contacts(fCallback, iOffset, iLimit, sSearch) {
this.defaultRequest(
fCallback,
'Contacts',
{
Offset: iOffset,
Limit: iLimit,
Search: sSearch
},
null,
'',
['Contacts']
);
}
/**
* @param {?Function} fCallback
* @param {string} sRequestUid
* @param {string} sUid
* @param {Array} aProperties
*/
contactSave(fCallback, sRequestUid, sUid, aProperties) {
this.defaultRequest(fCallback, 'ContactSave', {
RequestUid: sRequestUid,
Uid: sUid,
Properties: aProperties
});
}
/**
* @param {?Function} fCallback
* @param {Array} aUids
*/
contactsDelete(fCallback, aUids) {
this.defaultRequest(fCallback, 'ContactsDelete', {
Uids: aUids.join(',')
});
}
/**
* @param {?Function} fCallback
* @param {string} sQuery
* @param {number} iPage
*/
suggestions(fCallback, sQuery, iPage) {
this.defaultRequest(
fCallback,
'Suggestions',
{
Query: sQuery,
Page: iPage
},
null,
'',
['Suggestions']
);
}
/**
* @param {?Function} fCallback
*/
clearUserBackground(fCallback) {
this.defaultRequest(fCallback, 'ClearUserBackground');
}
foldersReload(fCallback) {
this.abort('Folders')
.postRequest('Folders', FolderUserStore.foldersLoading)
.then(data => {
data = FolderCollectionModel.reviveFromJson(data.Result);
data && data.storeIt();
fCallback && fCallback(true);
})
.catch(() => fCallback && setTimeout(() => fCallback(false), 1));
}
foldersReloadWithTimeout() {
this.setTrigger(FolderUserStore.foldersLoading, true);
clearTimeout(this.foldersTimeout);
this.foldersTimeout = setTimeout(() => this.foldersReload(), 500);
}
folderDelete(sFolderFullNameRaw) {
return this.postRequest('FolderDelete', FolderUserStore.foldersDeleting, {
Folder: sFolderFullNameRaw
});
}
folderCreate(sNewFolderName, sParentName) {
return this.postRequest('FolderCreate', FolderUserStore.foldersCreating, {
Folder: sNewFolderName,
Parent: sParentName
});
}
folderMove(sPrevFolderFullNameRaw, sNewFolderFullName) {
return this.postRequest('FolderMove', FolderUserStore.foldersRenaming, {
Folder: sPrevFolderFullNameRaw,
NewFolder: sNewFolderFullName
});
}
folderRename(sPrevFolderFullNameRaw, sNewFolderName) {
return this.postRequest('FolderRename', FolderUserStore.foldersRenaming, {
Folder: sPrevFolderFullNameRaw,
NewFolderName: sNewFolderName
});
}
attachmentsActions(sAction, aHashes, fTrigger) {
return this.postRequest('AttachmentsActions', fTrigger, {
Do: sAction,
Hashes: aHashes
});
}
} }
export default new RemoteUserFetch(); export default new RemoteUserFetch();

View file

@ -2,7 +2,7 @@ import ko from 'ko';
import { pString } from 'Common/Utils'; import { pString } from 'Common/Utils';
import { settings } from 'Common/Links'; import { settings } from 'Common/Links';
import { doc, elementById } from 'Common/Globals'; import { createElement, elementById } from 'Common/Globals';
import { AbstractScreen } from 'Knoin/AbstractScreen'; import { AbstractScreen } from 'Knoin/AbstractScreen';
@ -18,52 +18,38 @@ export class AbstractSettingsScreen extends AbstractScreen {
this.menu = ko.observableArray(); this.menu = ko.observableArray();
this.oCurrentSubScreen = null; this.oCurrentSubScreen = null;
this.setupSettings();
}
/**
* @param {Function=} fCallback
*/
setupSettings(fCallback = null) {
fCallback && fCallback();
} }
onRoute(subName) { onRoute(subName) {
let settingsScreen = null, let settingsScreen = null,
RoutedSettingsViewModel = null, viewModelDom = null,
viewModelDom = null; RoutedSettingsViewModel = VIEW_MODELS.find(
SettingsViewModel => subName === SettingsViewModel.__rlSettingsData.route
RoutedSettingsViewModel = VIEW_MODELS.find( );
SettingsViewModel =>
SettingsViewModel && SettingsViewModel.__rlSettingsData && subName === SettingsViewModel.__rlSettingsData.Route
);
if (RoutedSettingsViewModel) { if (RoutedSettingsViewModel) {
if (RoutedSettingsViewModel.__builded && RoutedSettingsViewModel.__vm) { if (RoutedSettingsViewModel.__vm) {
settingsScreen = RoutedSettingsViewModel.__vm; settingsScreen = RoutedSettingsViewModel.__vm;
} else { } else {
const vmPlace = elementById('rl-settings-subscreen'); const vmPlace = elementById('rl-settings-subscreen');
if (vmPlace) { if (vmPlace) {
settingsScreen = new RoutedSettingsViewModel(); viewModelDom = createElement('div',{
id: 'V-Settings-' + RoutedSettingsViewModel.name.replace(/(User|Admin)Settings/,''),
viewModelDom = Element.fromHTML('<div class="rl-settings-view-model" hidden=""></div>'); hidden: ''
})
vmPlace.append(viewModelDom); vmPlace.append(viewModelDom);
settingsScreen = new RoutedSettingsViewModel();
settingsScreen.viewModelDom = viewModelDom; settingsScreen.viewModelDom = viewModelDom;
settingsScreen.__rlSettingsData = RoutedSettingsViewModel.__rlSettingsData;
RoutedSettingsViewModel.__dom = viewModelDom; RoutedSettingsViewModel.__dom = viewModelDom;
RoutedSettingsViewModel.__builded = true;
RoutedSettingsViewModel.__vm = settingsScreen; RoutedSettingsViewModel.__vm = settingsScreen;
const tmpl = { name: RoutedSettingsViewModel.__rlSettingsData.Template };
ko.applyBindingAccessorsToNode( ko.applyBindingAccessorsToNode(
viewModelDom, viewModelDom,
{ {
i18nInit: true, i18nInit: true,
template: () => tmpl template: () => ({ name: RoutedSettingsViewModel.__rlSettingsData.template })
}, },
settingsScreen settingsScreen
); );
@ -75,74 +61,55 @@ export class AbstractSettingsScreen extends AbstractScreen {
} }
if (settingsScreen) { if (settingsScreen) {
const o = this;
setTimeout(() => { setTimeout(() => {
// hide // hide
if (o.oCurrentSubScreen) { this.onHide();
o.oCurrentSubScreen.onHide && o.oCurrentSubScreen.onHide();
o.oCurrentSubScreen.viewModelDom.hidden = true;
}
// -- // --
o.oCurrentSubScreen = settingsScreen; this.oCurrentSubScreen = settingsScreen;
// show // show
if (o.oCurrentSubScreen) { settingsScreen.beforeShow && settingsScreen.beforeShow();
o.oCurrentSubScreen.onBeforeShow && o.oCurrentSubScreen.onBeforeShow(); settingsScreen.viewModelDom.hidden = false;
o.oCurrentSubScreen.viewModelDom.hidden = false; settingsScreen.onShow && settingsScreen.onShow();
o.oCurrentSubScreen.onShow && o.oCurrentSubScreen.onShow();
o.menu.forEach(item => { this.menu.forEach(item => {
item.selected( item.selected(
settingsScreen && item.route === RoutedSettingsViewModel.__rlSettingsData.route
settingsScreen.__rlSettingsData && );
item.route === settingsScreen.__rlSettingsData.Route });
);
});
doc.querySelector('#rl-content .b-settings .b-content').scrollTop = 0; (elementById('rl-settings-subscreen') || {}).scrollTop = 0;
}
// -- // --
}, 1); }, 1);
} }
} else { } else {
rl.route.setHash(settings(), false, true); hasher.replaceHash(settings());
} }
} }
onHide() { onHide() {
if (this.oCurrentSubScreen && this.oCurrentSubScreen.viewModelDom) { let subScreen = this.oCurrentSubScreen;
this.oCurrentSubScreen.onHide && this.oCurrentSubScreen.onHide(); if (subScreen) {
this.oCurrentSubScreen.viewModelDom.hidden = true; subScreen.onHide && subScreen.onHide();
subScreen.viewModelDom.hidden = true;
} }
} }
onBuild() { onBuild() {
VIEW_MODELS.forEach(SettingsViewModel => { VIEW_MODELS.forEach(SettingsViewModel => this.menu.push(SettingsViewModel.__rlSettingsData));
if (
SettingsViewModel &&
SettingsViewModel.__rlSettingsData
) {
this.menu.push({
route: SettingsViewModel.__rlSettingsData.Route,
label: SettingsViewModel.__rlSettingsData.Label,
selected: ko.observable(false)
});
}
});
} }
routes() { routes() {
const DefaultViewModel = VIEW_MODELS.find( const DefaultViewModel = VIEW_MODELS.find(
SettingsViewModel => SettingsViewModel => SettingsViewModel.__rlSettingsData.isDefault
SettingsViewModel && SettingsViewModel.__rlSettingsData && SettingsViewModel.__rlSettingsData.IsDefault
), ),
defaultRoute = defaultRoute =
DefaultViewModel && DefaultViewModel.__rlSettingsData ? DefaultViewModel.__rlSettingsData.Route : 'general', DefaultViewModel ? DefaultViewModel.__rlSettingsData.route : 'general',
rules = { rules = {
subname: /^(.*)$/, subname: /^(.*)$/,
normalize_: (rquest, vals) => { normalize_: (rquest, vals) => {
vals.subname = undefined === vals.subname ? defaultRoute : pString(vals.subname); vals.subname = null == vals.subname ? defaultRoute : pString(vals.subname);
return [vals.subname]; return [vals.subname];
} }
}; };
@ -164,11 +131,13 @@ export class AbstractSettingsScreen extends AbstractScreen {
* @returns {void} * @returns {void}
*/ */
export function settingsAddViewModel(SettingsViewModelClass, template, labelName, route, isDefault = false) { export function settingsAddViewModel(SettingsViewModelClass, template, labelName, route, isDefault = false) {
let name = SettingsViewModelClass.name.replace(/(User|Admin)Settings/, '');
SettingsViewModelClass.__rlSettingsData = { SettingsViewModelClass.__rlSettingsData = {
Label: labelName, label: labelName || 'SETTINGS_LABELS/LABEL_' + name.toUpperCase() + '_NAME',
Template: template, route: route || name.toLowerCase(),
Route: route, selected: ko.observable(false),
IsDefault: !!isDefault template: template || SettingsViewModelClass.name,
isDefault: !!isDefault
}; };
VIEW_MODELS.push(SettingsViewModelClass); VIEW_MODELS.push(SettingsViewModelClass);

View file

@ -1,10 +1,10 @@
import { AbstractScreen } from 'Knoin/AbstractScreen'; import { AbstractScreen } from 'Knoin/AbstractScreen';
import { LoginAdminView } from 'View/Admin/Login'; import { AdminLoginView } from 'View/Admin/Login';
export class LoginAdminScreen extends AbstractScreen { export class LoginAdminScreen extends AbstractScreen {
constructor() { constructor() {
super('login', [LoginAdminView]); super('login', [AdminLoginView]);
} }
onShow() { onShow() {

View file

@ -2,14 +2,15 @@ import { runSettingsViewModelHooks } from 'Common/Plugins';
import { AbstractSettingsScreen, settingsAddViewModel } from 'Screen/AbstractSettings'; import { AbstractSettingsScreen, settingsAddViewModel } from 'Screen/AbstractSettings';
import { GeneralAdminSettings } from 'Settings/Admin/General'; import { AdminSettingsGeneral } from 'Settings/Admin/General';
import { DomainsAdminSettings } from 'Settings/Admin/Domains'; import { AdminSettingsDomains } from 'Settings/Admin/Domains';
import { LoginAdminSettings } from 'Settings/Admin/Login'; import { AdminSettingsLogin } from 'Settings/Admin/Login';
import { ContactsAdminSettings } from 'Settings/Admin/Contacts'; import { AdminSettingsContacts } from 'Settings/Admin/Contacts';
import { SecurityAdminSettings } from 'Settings/Admin/Security'; import { AdminSettingsSecurity } from 'Settings/Admin/Security';
import { PackagesAdminSettings } from 'Settings/Admin/Packages'; import { AdminSettingsPackages } from 'Settings/Admin/Packages';
import { AboutAdminSettings } from 'Settings/Admin/About'; import { AdminSettingsAbout } from 'Settings/Admin/About';
import { BrandingAdminSettings } from 'Settings/Admin/Branding'; import { AdminSettingsBranding } from 'Settings/Admin/Branding';
import { AdminSettingsConfig } from 'Settings/Admin/Config';
import { MenuSettingsAdminView } from 'View/Admin/Settings/Menu'; import { MenuSettingsAdminView } from 'View/Admin/Settings/Menu';
import { PaneSettingsAdminView } from 'View/Admin/Settings/Pane'; import { PaneSettingsAdminView } from 'View/Admin/Settings/Pane';
@ -17,40 +18,22 @@ import { PaneSettingsAdminView } from 'View/Admin/Settings/Pane';
export class SettingsAdminScreen extends AbstractSettingsScreen { export class SettingsAdminScreen extends AbstractSettingsScreen {
constructor() { constructor() {
super([MenuSettingsAdminView, PaneSettingsAdminView]); super([MenuSettingsAdminView, PaneSettingsAdminView]);
}
/**
* @param {Function=} fCallback = null
*/
setupSettings(fCallback = null) {
settingsAddViewModel(
GeneralAdminSettings,
'AdminSettingsGeneral',
'TABS_LABELS/LABEL_GENERAL_NAME',
'general',
true
);
[ [
[DomainsAdminSettings, 'Domains'], AdminSettingsGeneral,
[LoginAdminSettings, 'Login'], AdminSettingsDomains,
[BrandingAdminSettings, 'Branding'], AdminSettingsLogin,
[ContactsAdminSettings, 'Contacts'], AdminSettingsBranding,
[SecurityAdminSettings, 'Security'], AdminSettingsContacts,
[PackagesAdminSettings, 'Packages'], AdminSettingsSecurity,
[AboutAdminSettings, 'About'], AdminSettingsPackages,
].forEach(item => AdminSettingsConfig,
settingsAddViewModel( AdminSettingsAbout
item[0], ].forEach((item, index) =>
'AdminSettings'+item[1], settingsAddViewModel(item, 0, 0, 0, 0 === index)
'TABS_LABELS/LABEL_'+item[1].toUpperCase()+'_NAME',
item[1].toLowerCase()
)
); );
runSettingsViewModelHooks(true); runSettingsViewModelHooks(true);
fCallback && fCallback();
} }
onShow() { onShow() {

View file

@ -1,29 +1,32 @@
import { Scope } from 'Common/Enums'; import { Scope } from 'Common/Enums';
import { doc, leftPanelDisabled, moveAction, Settings } from 'Common/Globals'; import { Layout, ClientSideKeyNameMessageListSize } from 'Common/EnumsUser';
import { doc, leftPanelDisabled, moveAction, Settings, elementById } from 'Common/Globals';
import { pString, pInt } from 'Common/Utils'; import { pString, pInt } from 'Common/Utils';
import { getFolderFromCacheList, getFolderFullNameRaw, getFolderInboxName } from 'Common/Cache'; import { setLayoutResizer } from 'Common/UtilsUser';
import { getFolderFromCacheList, getFolderFullName, getFolderInboxName } from 'Common/Cache';
import { i18n } from 'Common/Translator'; import { i18n } from 'Common/Translator';
import { SettingsUserStore } from 'Stores/User/Settings';
import { AppUserStore } from 'Stores/User/App'; import { AppUserStore } from 'Stores/User/App';
import { AccountUserStore } from 'Stores/User/Account'; import { AccountUserStore } from 'Stores/User/Account';
import { FolderUserStore } from 'Stores/User/Folder'; import { FolderUserStore } from 'Stores/User/Folder';
import { MessageUserStore } from 'Stores/User/Message'; import { MessagelistUserStore } from 'Stores/User/Messagelist';
import { ThemeStore } from 'Stores/Theme'; import { ThemeStore } from 'Stores/Theme';
import { SystemDropDownMailBoxUserView } from 'View/User/MailBox/SystemDropDown'; import { SystemDropDownUserView } from 'View/User/SystemDropDown';
import { FolderListMailBoxUserView } from 'View/User/MailBox/FolderList'; import { MailFolderList } from 'View/User/MailBox/FolderList';
import { MessageListMailBoxUserView } from 'View/User/MailBox/MessageList'; import { MailMessageList } from 'View/User/MailBox/MessageList';
import { MessageViewMailBoxUserView } from 'View/User/MailBox/MessageView'; import { MailMessageView } from 'View/User/MailBox/MessageView';
import { AbstractScreen } from 'Knoin/AbstractScreen'; import { AbstractScreen } from 'Knoin/AbstractScreen';
export class MailBoxUserScreen extends AbstractScreen { export class MailBoxUserScreen extends AbstractScreen {
constructor() { constructor() {
super('mailbox', [ super('mailbox', [
SystemDropDownMailBoxUserView, SystemDropDownUserView,
FolderListMailBoxUserView, MailFolderList,
MessageListMailBoxUserView, MailMessageList,
MessageViewMailBoxUserView MailMessageView
]); ]);
} }
@ -48,7 +51,7 @@ export class MailBoxUserScreen extends AbstractScreen {
onShow() { onShow() {
this.updateWindowTitle(); this.updateWindowTitle();
AppUserStore.focusedState(Scope.None); AppUserStore.focusedState('none');
AppUserStore.focusedState(Scope.MessageList); AppUserStore.focusedState(Scope.MessageList);
ThemeStore.isMobile() && leftPanelDisabled(true); ThemeStore.isMobile() && leftPanelDisabled(true);
@ -61,17 +64,17 @@ export class MailBoxUserScreen extends AbstractScreen {
* @returns {void} * @returns {void}
*/ */
onRoute(folderHash, page, search) { onRoute(folderHash, page, search) {
const folder = getFolderFromCacheList(getFolderFullNameRaw(folderHash.replace(/~([\d]+)$/, ''))); const folder = getFolderFromCacheList(getFolderFullName(folderHash.replace(/~([\d]+)$/, '')));
if (folder) { if (folder) {
let threadUid = folderHash.replace(/^.+~(\d+)$/, '$1'); let threadUid = folderHash.replace(/^.+~(\d+)$/, '$1');
FolderUserStore.currentFolder(folder); FolderUserStore.currentFolder(folder);
MessageUserStore.listPage(1 > page ? 1 : page); MessagelistUserStore.page(1 > page ? 1 : page);
MessageUserStore.listSearch(search); MessagelistUserStore.listSearch(search);
MessageUserStore.listThreadUid((folderHash === threadUid) ? 0 : pInt(threadUid)); MessagelistUserStore.threadUid((folderHash === threadUid) ? 0 : pInt(threadUid));
rl.app.reloadMessageList(); MessagelistUserStore.reload();
} }
} }
@ -79,27 +82,41 @@ export class MailBoxUserScreen extends AbstractScreen {
* @returns {void} * @returns {void}
*/ */
onStart() { onStart() {
if (!this.__started) { super.onStart();
super.onStart();
addEventListener('mailbox.inbox-unread-count', e => { addEventListener('mailbox.inbox-unread-count', e => {
FolderUserStore.foldersInboxUnreadCount(e.detail); FolderUserStore.foldersInboxUnreadCount(e.detail);
/* // Disabled in SystemDropDown.html
const email = AccountUserStore.email(); const email = AccountUserStore.email();
AccountUserStore.accounts.forEach(item => AccountUserStore.accounts.forEach(item =>
item && email === item.email && item.count(e.detail) item && email === item.email && item.count(e.detail)
); );
*/
this.updateWindowTitle(); this.updateWindowTitle();
}); });
}
} }
/** /**
* @returns {void} * @returns {void}
*/ */
onBuild() { onBuild() {
setTimeout(() => rl.app.initHorizontalLayoutResizer(), 1); setTimeout(() => {
// initMailboxLayoutResizer
const top = elementById('V-MailMessageList'),
bottom = elementById('V-MailMessageView'),
fToggle = () => {
let layout = SettingsUserStore.layout();
setLayoutResizer(top, bottom, ClientSideKeyNameMessageListSize,
(ThemeStore.isMobile() || Layout.NoPreview === layout)
? 0
: (Layout.SidePreview === layout ? 'Width' : 'Height')
);
};
if (top && bottom) {
fToggle();
addEventListener('rl-layout', fToggle);
}
}, 1);
doc.addEventListener('click', event => doc.addEventListener('click', event =>
event.target.closest('#rl-right') && moveAction(false) event.target.closest('#rl-right') && moveAction(false)
@ -107,19 +124,23 @@ export class MailBoxUserScreen extends AbstractScreen {
} }
/** /**
* Parse link as generated by mailBox()
* @returns {Array} * @returns {Array}
*/ */
routes() { routes() {
const const
folder = (request, vals) => request ? decodeURI(pString(vals[0])) : getFolderInboxName(), folder = (request, vals) => request ? pString(vals[0]) : getFolderInboxName(),
fNormS = (request, vals) => [folder(request, vals), request ? pInt(vals[1]) : 1, decodeURI(pString(vals[2]))]; fNormS = (request, vals) => [folder(request, vals), request ? pInt(vals[1]) : 1, decodeURI(pString(vals[2]))];
return [ return [
// Folder: INBOX | INBOX.sub | Sent | fullNameHash
[/^([^/]*)$/, { normalize_: fNormS }], [/^([^/]*)$/, { normalize_: fNormS }],
[/^([a-zA-Z0-9~]+)\/(.+)\/?$/, { normalize_: (request, vals) => // Search: {folder}/{string}
[/^([a-zA-Z0-9.~_-]+)\/(.+)\/?$/, { normalize_: (request, vals) =>
[folder(request, vals), 1, decodeURI(pString(vals[1]))] [folder(request, vals), 1, decodeURI(pString(vals[1]))]
}], }],
[/^([a-zA-Z0-9~]+)\/p([1-9][0-9]*)(?:\/(.+)\/?)?$/, { normalize_: fNormS }] // Page: {folder}/p{int}(/{search})?
[/^([a-zA-Z0-9.~_-]+)\/p([1-9][0-9]*)(?:\/(.+))?$/, { normalize_: fNormS }]
]; ];
} }
} }

View file

@ -1,5 +1,5 @@
import { Capa, Scope } from 'Common/Enums'; import { Scope } from 'Common/Enums';
import { keyScope, leftPanelDisabled, Settings } from 'Common/Globals'; import { keyScope, leftPanelDisabled, SettingsCapa } from 'Common/Globals';
import { runSettingsViewModelHooks } from 'Common/Plugins'; import { runSettingsViewModelHooks } from 'Common/Plugins';
import { initOnStartOrLangChange, i18n } from 'Common/Translator'; import { initOnStartOrLangChange, i18n } from 'Common/Translator';
@ -9,23 +9,59 @@ import { ThemeStore } from 'Stores/Theme';
import { AbstractSettingsScreen, settingsAddViewModel } from 'Screen/AbstractSettings'; import { AbstractSettingsScreen, settingsAddViewModel } from 'Screen/AbstractSettings';
import { GeneralUserSettings } from 'Settings/User/General'; import { UserSettingsGeneral } from 'Settings/User/General';
import { ContactsUserSettings } from 'Settings/User/Contacts'; import { UserSettingsContacts } from 'Settings/User/Contacts';
import { AccountsUserSettings } from 'Settings/User/Accounts'; import { UserSettingsAccounts } from 'Settings/User/Accounts';
import { FiltersUserSettings } from 'Settings/User/Filters'; import { UserSettingsFilters } from 'Settings/User/Filters';
import { SecurityUserSettings } from 'Settings/User/Security'; import { UserSettingsSecurity } from 'Settings/User/Security';
import { TemplatesUserSettings } from 'Settings/User/Templates'; import { UserSettingsFolders } from 'Settings/User/Folders';
import { FoldersUserSettings } from 'Settings/User/Folders'; import { UserSettingsThemes } from 'Settings/User/Themes';
import { ThemesUserSettings } from 'Settings/User/Themes';
import { OpenPgpUserSettings } from 'Settings/User/OpenPgp';
import { SystemDropDownSettingsUserView } from 'View/User/Settings/SystemDropDown'; import { UserSettingsTemplates } from 'Settings/User/Templates';
import { MenuSettingsUserView } from 'View/User/Settings/Menu';
import { PaneSettingsUserView } from 'View/User/Settings/Pane'; import { SystemDropDownUserView } from 'View/User/SystemDropDown';
import { SettingsMenuUserView } from 'View/User/Settings/Menu';
import { SettingsPaneUserView } from 'View/User/Settings/Pane';
export class SettingsUserScreen extends AbstractSettingsScreen { export class SettingsUserScreen extends AbstractSettingsScreen {
constructor() { constructor() {
super([SystemDropDownSettingsUserView, MenuSettingsUserView, PaneSettingsUserView]); super([SystemDropDownUserView, SettingsMenuUserView, SettingsPaneUserView]);
const views = [
UserSettingsGeneral
];
if (AppUserStore.allowContacts()) {
views.push(UserSettingsContacts);
}
if (SettingsCapa('AdditionalAccounts') || SettingsCapa('Identities')) {
views.push(UserSettingsAccounts);
}
if (SettingsCapa('Sieve')) {
views.push(UserSettingsFilters);
}
if (SettingsCapa('AutoLogout') || SettingsCapa('OpenPGP') || SettingsCapa('GnuPG')) {
views.push(UserSettingsSecurity);
}
if (SettingsCapa('Templates')) {
views.push(UserSettingsTemplates);
}
views.push(UserSettingsFolders);
if (SettingsCapa('Themes')) {
views.push(UserSettingsThemes);
}
views.forEach((item, index) =>
settingsAddViewModel(item, item.name.replace('User', ''), 0, 0, 0 === index)
);
runSettingsViewModelHooks(false);
initOnStartOrLangChange( initOnStartOrLangChange(
() => this.sSettingsTitle = i18n('TITLES/SETTINGS'), () => this.sSettingsTitle = i18n('TITLES/SETTINGS'),
@ -33,67 +69,6 @@ export class SettingsUserScreen extends AbstractSettingsScreen {
); );
} }
/**
* @param {Function=} fCallback
*/
setupSettings(fCallback = null) {
if (!Settings.capa(Capa.Settings)) {
fCallback && fCallback();
return false;
}
settingsAddViewModel(GeneralUserSettings, 'SettingsGeneral', 'SETTINGS_LABELS/LABEL_GENERAL_NAME', 'general', true);
if (AppUserStore.allowContacts()) {
settingsAddViewModel(ContactsUserSettings, 'SettingsContacts', 'SETTINGS_LABELS/LABEL_CONTACTS_NAME', 'contacts');
}
if (Settings.capa(Capa.AdditionalAccounts) || Settings.capa(Capa.Identities)) {
settingsAddViewModel(
AccountsUserSettings,
'SettingsAccounts',
Settings.capa(Capa.AdditionalAccounts)
? 'SETTINGS_LABELS/LABEL_ACCOUNTS_NAME'
: 'SETTINGS_LABELS/LABEL_IDENTITIES_NAME',
'accounts'
);
}
if (Settings.capa(Capa.Sieve)) {
settingsAddViewModel(FiltersUserSettings, 'SettingsFilters', 'SETTINGS_LABELS/LABEL_FILTERS_NAME', 'filters');
}
if (Settings.capa(Capa.AutoLogout)) {
settingsAddViewModel(SecurityUserSettings, 'SettingsSecurity', 'SETTINGS_LABELS/LABEL_SECURITY_NAME', 'security');
}
if (Settings.capa(Capa.Templates)) {
settingsAddViewModel(
TemplatesUserSettings,
'SettingsTemplates',
'SETTINGS_LABELS/LABEL_TEMPLATES_NAME',
'templates'
);
}
settingsAddViewModel(FoldersUserSettings, 'SettingsFolders', 'SETTINGS_LABELS/LABEL_FOLDERS_NAME', 'folders');
if (Settings.capa(Capa.Themes)) {
settingsAddViewModel(ThemesUserSettings, 'SettingsThemes', 'SETTINGS_LABELS/LABEL_THEMES_NAME', 'themes');
}
if (Settings.capa(Capa.OpenPGP)) {
settingsAddViewModel(OpenPgpUserSettings, 'SettingsOpenPGP', 'OpenPGP', 'openpgp');
}
runSettingsViewModelHooks(false);
fCallback && fCallback();
return true;
}
onShow() { onShow() {
this.setSettingsTitle(); this.setSettingsTitle();
keyScope(Scope.Settings); keyScope(Scope.Settings);

View file

@ -1,8 +1,15 @@
import ko from 'ko'; import ko from 'ko';
import { Settings } from 'Common/Globals'; import { Settings } from 'Common/Globals';
import Remote from 'Remote/Admin/Fetch';
export class AboutAdminSettings { export class AdminSettingsAbout /*extends AbstractViewSettings*/ {
constructor() { constructor() {
this.version = ko.observable(Settings.app('version')); this.version = Settings.app('version');
this.phpextensions = ko.observableArray();
} }
onBuild() {
Remote.request('AdminPHPExtensions', (iError, data) => iError || this.phpextensions(data.Result));
}
} }

View file

@ -1,32 +1,10 @@
import ko from 'ko'; import { AbstractViewSettings } from 'Knoin/AbstractViews';
import { SettingsGet } from 'Common/Globals'; export class AdminSettingsBranding extends AbstractViewSettings {
import { settingsSaveHelperSimpleFunction } from 'Common/Utils';
import Remote from 'Remote/Admin/Fetch';
export class BrandingAdminSettings {
constructor() { constructor() {
this.title = ko.observable(SettingsGet('Title')).idleTrigger(); super();
this.loadingDesc = ko.observable(SettingsGet('LoadingDescription')).idleTrigger(); this.addSetting('Title');
this.faviconUrl = ko.observable(SettingsGet('FaviconUrl')).idleTrigger(); this.addSetting('LoadingDescription');
this.addSetting('FaviconUrl');
this.title.subscribe(value =>
Remote.saveAdminConfig(settingsSaveHelperSimpleFunction(this.title.trigger, this), {
Title: value.trim()
})
);
this.loadingDesc.subscribe(value =>
Remote.saveAdminConfig(settingsSaveHelperSimpleFunction(this.loadingDesc.trigger, this), {
LoadingDescription: value.trim()
})
);
this.faviconUrl.subscribe(value =>
Remote.saveAdminConfig(settingsSaveHelperSimpleFunction(this.faviconUrl.trigger, this), {
FaviconUrl: value.trim()
})
);
} }
} }

View file

@ -0,0 +1,48 @@
import ko from 'ko';
import Remote from 'Remote/Admin/Fetch';
import { forEachObjectEntry } from 'Common/Utils';
export class AdminSettingsConfig /*extends AbstractViewSettings*/ {
constructor() {
this.config = ko.observableArray();
}
beforeShow() {
Remote.request('AdminSettingsGet', (iError, data) => {
if (!iError) {
const cfg = [],
getInputType = (value, pass) => {
switch (typeof value)
{
case 'boolean': return 'checkbox';
case 'number': return 'number';
}
return pass ? 'password' : 'text';
};
forEachObjectEntry(data.Result, (key, items) => {
const section = {
name: key,
items: []
};
forEachObjectEntry(items, (skey, item) => {
section.items.push({
key: `config[${key}][${skey}]`,
name: skey,
value: item[0],
type: getInputType(item[0], skey.includes('password')),
comment: item[1]
});
});
cfg.push(section);
});
this.config(cfg);
}
});
}
saveConfig(form) {
Remote.post('AdminSettingsSet', null, new FormData(form));
}
}

View file

@ -1,35 +1,30 @@
import ko from 'ko'; import ko from 'ko';
import { SaveSettingsStep } from 'Common/Enums';
import { SettingsGet } from 'Common/Globals'; import { SettingsGet } from 'Common/Globals';
import { import { defaultOptionsAfterRender } from 'Common/Utils';
settingsSaveHelperSimpleFunction, import { addObservablesTo } from 'External/ko';
defaultOptionsAfterRender,
addObservablesTo,
addSubscribablesTo
} from 'Common/Utils';
import Remote from 'Remote/Admin/Fetch'; import Remote from 'Remote/Admin/Fetch';
import { decorateKoCommands } from 'Knoin/Knoin'; import { decorateKoCommands } from 'Knoin/Knoin';
import { AbstractViewSettings } from 'Knoin/AbstractViews';
export class ContactsAdminSettings { export class AdminSettingsContacts extends AbstractViewSettings {
constructor() { constructor() {
super();
this.defaultOptionsAfterRender = defaultOptionsAfterRender; this.defaultOptionsAfterRender = defaultOptionsAfterRender;
this.addSetting('ContactsPdoDsn');
this.addSetting('ContactsPdoUser');
this.addSetting('ContactsPdoPassword');
this.addSetting('ContactsPdoType', () => {
this.testContactsSuccess(false);
this.testContactsError(false);
this.testContactsErrorMessage('');
});
this.addSettings(['ContactsEnable','ContactsSync']);
addObservablesTo(this, { addObservablesTo(this, {
enableContacts: !!SettingsGet('ContactsEnable'),
contactsSync: !!SettingsGet('ContactsSync'),
contactsType: SettingsGet('ContactsPdoType'),
pdoDsn: SettingsGet('ContactsPdoDsn'),
pdoUser: SettingsGet('ContactsPdoUser'),
pdoPassword: SettingsGet('ContactsPdoPassword'),
pdoDsnTrigger: SaveSettingsStep.Idle,
pdoUserTrigger: SaveSettingsStep.Idle,
pdoPasswordTrigger: SaveSettingsStep.Idle,
contactsTypeTrigger: SaveSettingsStep.Idle,
testing: false, testing: false,
testContactsSuccess: false, testContactsSuccess: false,
testContactsError: false, testContactsError: false,
@ -54,59 +49,23 @@ export class ContactsAdminSettings {
this.mainContactsType = ko this.mainContactsType = ko
.computed({ .computed({
read: this.contactsType, read: this.contactsPdoType,
write: value => { write: value => {
if (value !== this.contactsType()) { if (value !== this.contactsPdoType()) {
if (supportedTypes.includes(value)) { if (supportedTypes.includes(value)) {
this.contactsType(value); this.contactsPdoType(value);
} else if (types.length) { } else if (types.length) {
this.contactsType(''); this.contactsPdoType('');
} }
} else { } else {
this.contactsType.valueHasMutated(); this.contactsPdoType.valueHasMutated();
} }
} }
}) })
.extend({ notify: 'always' }); .extend({ notify: 'always' });
addSubscribablesTo(this, {
enableContacts: value =>
Remote.saveAdminConfig(null, {
ContactsEnable: value ? 1 : 0
}),
contactsSync: value =>
Remote.saveAdminConfig(null, {
ContactsSync: value ? 1 : 0
}),
contactsType: value => {
this.testContactsSuccess(false);
this.testContactsError(false);
this.testContactsErrorMessage('');
Remote.saveAdminConfig(settingsSaveHelperSimpleFunction(this.contactsTypeTrigger, this), {
ContactsPdoType: value.trim()
})
},
pdoDsn: value =>
Remote.saveAdminConfig(settingsSaveHelperSimpleFunction(this.pdoDsnTrigger, this), {
ContactsPdoDsn: value.trim()
}),
pdoUser: value =>
Remote.saveAdminConfig(settingsSaveHelperSimpleFunction(this.pdoUserTrigger, this), {
ContactsPdoUser: value.trim()
}),
pdoPassword: value =>
Remote.saveAdminConfig(settingsSaveHelperSimpleFunction(this.pdoPasswordTrigger, this), {
ContactsPdoPassword: value.trim()
})
})
decorateKoCommands(this, { decorateKoCommands(this, {
testContactsCommand: self => self.pdoDsn() && self.pdoUser() testContactsCommand: self => self.contactsPdoDsn() && self.contactsPdoUser()
}); });
} }
@ -116,29 +75,31 @@ export class ContactsAdminSettings {
this.testContactsErrorMessage(''); this.testContactsErrorMessage('');
this.testing(true); this.testing(true);
Remote.testContacts((iError, data) => { Remote.request('AdminContactsTest',
this.testContactsSuccess(false); (iError, data) => {
this.testContactsError(false); this.testContactsSuccess(false);
this.testContactsErrorMessage(''); this.testContactsError(false);
this.testContactsErrorMessage('');
if (!iError && data.Result.Result) { if (!iError && data.Result.Result) {
this.testContactsSuccess(true); this.testContactsSuccess(true);
} else {
this.testContactsError(true);
if (data && data.Result) {
this.testContactsErrorMessage(data.Result.Message || '');
} else { } else {
this.testContactsErrorMessage(''); this.testContactsError(true);
if (data && data.Result) {
this.testContactsErrorMessage(data.Result.Message || '');
} else {
this.testContactsErrorMessage('');
}
} }
}
this.testing(false); this.testing(false);
}, { }, {
ContactsPdoType: this.contactsType(), ContactsPdoType: this.contactsPdoType(),
ContactsPdoDsn: this.pdoDsn(), ContactsPdoDsn: this.contactsPdoDsn(),
ContactsPdoUser: this.pdoUser(), ContactsPdoUser: this.contactsPdoUser(),
ContactsPdoPassword: this.pdoPassword() ContactsPdoPassword: this.contactsPdoPassword()
}); }
);
} }
onShow() { onShow() {

View file

@ -8,11 +8,11 @@ import Remote from 'Remote/Admin/Fetch';
import { DomainPopupView } from 'View/Popup/Domain'; import { DomainPopupView } from 'View/Popup/Domain';
import { DomainAliasPopupView } from 'View/Popup/DomainAlias'; import { DomainAliasPopupView } from 'View/Popup/DomainAlias';
export class DomainsAdminSettings { export class AdminSettingsDomains /*extends AbstractViewSettings*/ {
constructor() { constructor() {
this.domains = DomainAdminStore; this.domains = DomainAdminStore;
this.domainForDeletion = ko.observable(null).deleteAccessHelper(); this.domainForDeletion = ko.observable(null).askDeleteHelper();
} }
createDomain() { createDomain() {
@ -26,19 +26,29 @@ export class DomainsAdminSettings {
deleteDomain(domain) { deleteDomain(domain) {
DomainAdminStore.remove(domain); DomainAdminStore.remove(domain);
Remote.domainDelete(DomainAdminStore.fetch, domain.name); Remote.domainDelete(DomainAdminStore.fetch, domain.name);
Remote.request('AdminDomainDelete', DomainAdminStore.fetch, {
Name: domain.name
});
} }
disableDomain(domain) { disableDomain(domain) {
domain.disabled(!domain.disabled()); domain.disabled(!domain.disabled());
Remote.domainDisable(DomainAdminStore.fetch, domain.name, domain.disabled()); Remote.request('AdminDomainDisable', DomainAdminStore.fetch, {
Name: domain.name,
Disabled: domain.disabled() ? 1 : 0
});
} }
onBuild(oDom) { onBuild(oDom) {
oDom.addEventListener('click', event => { oDom.addEventListener('click', event => {
let el = event.target.closestWithin('.b-admin-domains-list-table .e-action', oDom); let el = event.target.closestWithin('.b-admin-domains-list-table .e-action', oDom);
el && ko.dataFor(el) && Remote.domain( el && ko.dataFor(el) && Remote.request('AdminDomainLoad',
(iError, oData) => iError || showScreenPopup(DomainPopupView, [oData.Result]), ko.dataFor(el).name (iError, oData) => iError || showScreenPopup(DomainPopupView, [oData.Result]),
{
Name: ko.dataFor(el).name
}
); );
}); });
DomainAdminStore.fetch(); DomainAdminStore.fetch();

View file

@ -2,29 +2,29 @@ import ko from 'ko';
import { import {
isArray, isArray,
pInt,
settingsSaveHelperSimpleFunction,
changeTheme, changeTheme,
convertThemeName, convertThemeName
addObservablesTo,
addSubscribablesTo,
addComputablesTo
} from 'Common/Utils'; } from 'Common/Utils';
import { Capa, SaveSettingsStep } from 'Common/Enums'; import { addObservablesTo, addSubscribablesTo, addComputablesTo } from 'External/ko';
import { Settings, SettingsGet } from 'Common/Globals';
import { reload as translatorReload, convertLangName } from 'Common/Translator';
import { SaveSettingsStep } from 'Common/Enums';
import { Settings, SettingsGet, SettingsCapa } from 'Common/Globals';
import { translatorReload, convertLangName } from 'Common/Translator';
import { AbstractViewSettings } from 'Knoin/AbstractViews';
import { showScreenPopup } from 'Knoin/Knoin'; import { showScreenPopup } from 'Knoin/Knoin';
import Remote from 'Remote/Admin/Fetch'; import Remote from 'Remote/Admin/Fetch';
import { ThemeStore } from 'Stores/Theme'; import { ThemeStore } from 'Stores/Theme';
import { LanguageStore } from 'Stores/Language'; import { LanguageStore } from 'Stores/Language';
import LanguagesPopupView from 'View/Popup/Languages'; import { LanguagesPopupView } from 'View/Popup/Languages';
export class GeneralAdminSettings { export class AdminSettingsGeneral extends AbstractViewSettings {
constructor() { constructor() {
super();
this.language = LanguageStore.language; this.language = LanguageStore.language;
this.languages = LanguageStore.languages; this.languages = LanguageStore.languages;
@ -37,18 +37,17 @@ export class GeneralAdminSettings {
this.theme = ThemeStore.theme; this.theme = ThemeStore.theme;
this.themes = ThemeStore.themes; this.themes = ThemeStore.themes;
this.addSettings(['AllowLanguagesOnSettings','NewMoveToFolder']);
addObservablesTo(this, { addObservablesTo(this, {
allowLanguagesOnSettings: !!SettingsGet('AllowLanguagesOnSettings'),
newMoveToFolder: !!SettingsGet('NewMoveToFolder'),
attachmentLimitTrigger: SaveSettingsStep.Idle, attachmentLimitTrigger: SaveSettingsStep.Idle,
languageTrigger: SaveSettingsStep.Idle,
themeTrigger: SaveSettingsStep.Idle, themeTrigger: SaveSettingsStep.Idle,
capaThemes: Settings.capa(Capa.Themes), capaThemes: SettingsCapa('Themes'),
capaUserBackground: Settings.capa(Capa.UserBackground), capaUserBackground: SettingsCapa('UserBackground'),
capaAdditionalAccounts: Settings.capa(Capa.AdditionalAccounts), capaAdditionalAccounts: SettingsCapa('AdditionalAccounts'),
capaIdentities: Settings.capa(Capa.Identities), capaIdentities: SettingsCapa('Identities'),
capaAttachmentThumbnails: Settings.capa(Capa.AttachmentThumbnails), capaAttachmentThumbnails: SettingsCapa('AttachmentThumbnails'),
capaTemplates: Settings.capa(Capa.Templates), capaTemplates: SettingsCapa('Templates'),
dataFolderAccess: false dataFolderAccess: false
}); });
@ -60,10 +59,14 @@ export class GeneralAdminSettings {
} }
*/ */
this.mainAttachmentLimit = ko this.attachmentLimit = ko
.observable(pInt(SettingsGet('AttachmentLimit')) / (1024 * 1024)) .observable(SettingsGet('AttachmentLimit') / (1024 * 1024))
.extend({ debounce: 500 }); .extend({ debounce: 500 });
this.addSetting('Language');
this.addSetting('AttachmentLimit');
this.addSetting('Theme', value => changeTheme(value, this.themeTrigger));
this.uploadData = SettingsGet('PhpUploadSizes'); this.uploadData = SettingsGet('PhpUploadSizes');
this.uploadDataDesc = this.uploadDataDesc =
this.uploadData && (this.uploadData.upload_max_filesize || this.uploadData.post_max_size) this.uploadData && (this.uploadData.upload_max_filesize || this.uploadData.post_max_size)
@ -88,55 +91,27 @@ export class GeneralAdminSettings {
this.languageAdminTrigger(saveSettingsStep); this.languageAdminTrigger(saveSettingsStep);
setTimeout(() => this.languageAdminTrigger(SaveSettingsStep.Idle), 1000); setTimeout(() => this.languageAdminTrigger(SaveSettingsStep.Idle), 1000);
}, },
fSaveBoolHelper = (key, fn) => fSaveHelper = key => value => Remote.saveSetting(key, value);
value => {
const data = {};
data[key] = value ? 1 : 0;
Remote.saveAdminConfig(fn, data);
};
addSubscribablesTo(this, { addSubscribablesTo(this, {
mainAttachmentLimit: value =>
Remote.saveAdminConfig(settingsSaveHelperSimpleFunction(this.attachmentLimitTrigger, this), {
AttachmentLimit: pInt(value)
}),
language: value =>
Remote.saveAdminConfig(settingsSaveHelperSimpleFunction(this.languageTrigger, this), {
Language: value.trim()
}),
languageAdmin: value => { languageAdmin: value => {
this.languageAdminTrigger(SaveSettingsStep.Animate); this.languageAdminTrigger(SaveSettingsStep.Animate);
translatorReload(true, value) translatorReload(true, value)
.then(fReloadLanguageHelper(SaveSettingsStep.TrueResult), fReloadLanguageHelper(SaveSettingsStep.FalseResult)) .then(fReloadLanguageHelper(SaveSettingsStep.TrueResult), fReloadLanguageHelper(SaveSettingsStep.FalseResult))
.then(() => Remote.saveAdminConfig(null, { .then(() => Remote.saveSetting('LanguageAdmin', value));
LanguageAdmin: value.trim()
}));
}, },
theme: value => { capaAdditionalAccounts: fSaveHelper('CapaAdditionalAccounts'),
changeTheme(value, this.themeTrigger);
Remote.saveAdminConfig(settingsSaveHelperSimpleFunction(this.themeTrigger, this), {
Theme: value.trim()
});
},
capaAdditionalAccounts: fSaveBoolHelper('CapaAdditionalAccounts'), capaIdentities: fSaveHelper('CapaIdentities'),
capaIdentities: fSaveBoolHelper('CapaIdentities'), capaTemplates: fSaveHelper('CapaTemplates'),
capaTemplates: fSaveBoolHelper('CapaTemplates'), capaAttachmentThumbnails: fSaveHelper('CapaAttachmentThumbnails'),
capaAttachmentThumbnails: fSaveBoolHelper('CapaAttachmentThumbnails'), capaThemes: fSaveHelper('CapaThemes'),
capaThemes: fSaveBoolHelper('CapaThemes'), capaUserBackground: fSaveHelper('CapaUserBackground')
capaUserBackground: fSaveBoolHelper('CapaUserBackground'),
allowLanguagesOnSettings: fSaveBoolHelper('AllowLanguagesOnSettings'),
newMoveToFolder: fSaveBoolHelper('NewMoveToFolder')
}); });
} }

View file

@ -1,46 +1,9 @@
import ko from 'ko'; import { AbstractViewSettings } from 'Knoin/AbstractViews';
import { Settings, SettingsGet } from 'Common/Globals'; export class AdminSettingsLogin extends AbstractViewSettings {
import { settingsSaveHelperSimpleFunction, addObservablesTo, addSubscribablesTo } from 'Common/Utils';
import Remote from 'Remote/Admin/Fetch';
export class LoginAdminSettings {
constructor() { constructor() {
addObservablesTo(this, { super();
determineUserLanguage: !!SettingsGet('DetermineUserLanguage'), this.addSetting('LoginDefaultDomain');
determineUserDomain: !!SettingsGet('DetermineUserDomain'), this.addSettings(['DetermineUserLanguage','DetermineUserDomain','AllowLanguagesOnLogin','hideSubmitButton']);
allowLanguagesOnLogin: !!SettingsGet('AllowLanguagesOnLogin'),
hideSubmitButton: !!Settings.app('hideSubmitButton')
});
this.defaultDomain = ko.observable(SettingsGet('LoginDefaultDomain')).idleTrigger();
addSubscribablesTo(this, {
determineUserLanguage: value =>
Remote.saveAdminConfig(null, {
DetermineUserLanguage: value ? 1 : 0
}),
determineUserDomain: value =>
Remote.saveAdminConfig(null, {
DetermineUserDomain: value ? 1 : 0
}),
allowLanguagesOnLogin: value =>
Remote.saveAdminConfig(null, {
AllowLanguagesOnLogin: value ? 1 : 0
}),
hideSubmitButton: value =>
Remote.saveAdminConfig(null, {
hideSubmitButton: value ? 1 : 0
}),
defaultDomain: value =>
Remote.saveAdminConfig(settingsSaveHelperSimpleFunction(this.defaultDomain.trigger, this), {
LoginDefaultDomain: value.trim()
})
});
} }
} }

View file

@ -8,29 +8,28 @@ import Remote from 'Remote/Admin/Fetch';
import { showScreenPopup } from 'Knoin/Knoin'; import { showScreenPopup } from 'Knoin/Knoin';
import { PluginPopupView } from 'View/Popup/Plugin'; import { PluginPopupView } from 'View/Popup/Plugin';
import { SettingsGet } from 'Common/Globals'; import { addObservablesTo, addComputablesTo } from 'External/ko';
import { addComputablesTo } from 'Common/Utils'; import { AbstractViewSettings } from 'Knoin/AbstractViews';
export class PackagesAdminSettings { export class AdminSettingsPackages extends AbstractViewSettings {
constructor() { constructor() {
this.packagesError = ko.observable(''); super();
this.addSettings(['EnabledPlugins']);
addObservablesTo(this, {
packagesError: ''
});
this.packages = PackageAdminStore; this.packages = PackageAdminStore;
addComputablesTo(this, { addComputablesTo(this, {
packagesCurrent: () => PackageAdminStore.filter(item => item && item.installed && !item.canBeUpdated), packagesCurrent: () => PackageAdminStore().filter(item => item && item.installed && !item.canBeUpdated),
packagesAvailableForUpdate: () => PackageAdminStore.filter(item => item && item.installed && !!item.canBeUpdated), packagesUpdate: () => PackageAdminStore().filter(item => item && item.installed && item.canBeUpdated),
packagesAvailableForInstallation: () => PackageAdminStore.filter(item => item && !item.installed), packagesAvailable: () => PackageAdminStore().filter(item => item && !item.installed),
visibility: () => (PackageAdminStore.loading() ? 'visible' : 'hidden') visibility: () => (PackageAdminStore.loading() ? 'visible' : 'hidden')
}); });
this.enabledPlugins = ko.observable(!!SettingsGet('EnabledPlugins'));
this.enabledPlugins.subscribe(value =>
Remote.saveAdminConfig(null, {
EnabledPlugins: value ? 1 : 0
})
);
} }
onShow() { onShow() {
@ -44,7 +43,12 @@ export class PackagesAdminSettings {
// configurePlugin // configurePlugin
let el = event.target.closestWithin('.package-configure', oDom), let el = event.target.closestWithin('.package-configure', oDom),
data = el ? ko.dataFor(el) : 0; data = el ? ko.dataFor(el) : 0;
data && Remote.plugin((iError, data) => iError || showScreenPopup(PluginPopupView, [data.Result]), data.id) data && Remote.request('AdminPluginLoad',
(iError, data) => iError || showScreenPopup(PluginPopupView, [data.Result]),
{
Id: data.id
}
);
// disablePlugin // disablePlugin
el = event.target.closestWithin('.package-active', oDom); el = event.target.closestWithin('.package-active', oDom);
data = el ? ko.dataFor(el) : 0; data = el ? ko.dataFor(el) : 0;
@ -77,31 +81,49 @@ export class PackagesAdminSettings {
deletePackage(packageToDelete) { deletePackage(packageToDelete) {
if (packageToDelete) { if (packageToDelete) {
packageToDelete.loading(true); packageToDelete.loading(true);
Remote.packageDelete(this.requestHelper(packageToDelete, false), packageToDelete); Remote.request('AdminPackageDelete',
this.requestHelper(packageToDelete, false),
{
Id: packageToDelete.id
}
);
} }
} }
installPackage(packageToInstall) { installPackage(packageToInstall) {
if (packageToInstall) { if (packageToInstall) {
packageToInstall.loading(true); packageToInstall.loading(true);
Remote.packageInstall(this.requestHelper(packageToInstall, true), packageToInstall); Remote.request('AdminPackageInstall',
this.requestHelper(packageToInstall, true),
{
Id: packageToInstall.id,
Type: packageToInstall.type,
File: packageToInstall.file
},
60000
);
} }
} }
disablePlugin(plugin) { disablePlugin(plugin) {
let b = plugin.enabled(); let disable = plugin.enabled();
plugin.enabled(!b); plugin.enabled(!disable);
Remote.pluginDisable((iError, data) => { Remote.request('AdminPluginDisable',
if (iError) { (iError, data) => {
plugin.enabled(b); if (iError) {
this.packagesError( plugin.enabled(disable);
(Notification.UnsupportedPluginPackage === iError && data && data.ErrorMessage) this.packagesError(
? data.ErrorMessage (Notification.UnsupportedPluginPackage === iError && data && data.ErrorMessage)
: getNotification(iError) ? data.ErrorMessage
); : getNotification(iError)
);
}
// PackageAdminStore.fetch();
}, {
Id: plugin.id,
Disabled: disable ? 1 : 0
} }
// PackageAdminStore.fetch(); );
}, plugin.id, b);
} }
} }

View file

@ -1,34 +1,40 @@
import { Capa } from 'Common/Enums'; import { SettingsGet, SettingsCapa } from 'Common/Globals';
import { Settings, SettingsGet } from 'Common/Globals'; import { addObservablesTo, addSubscribablesTo } from 'External/ko';
import { addObservablesTo, addSubscribablesTo } from 'Common/Utils';
import Remote from 'Remote/Admin/Fetch'; import Remote from 'Remote/Admin/Fetch';
import { decorateKoCommands } from 'Knoin/Knoin'; import { decorateKoCommands } from 'Knoin/Knoin';
import { AbstractViewSettings } from 'Knoin/AbstractViews';
export class SecurityAdminSettings { export class AdminSettingsSecurity extends AbstractViewSettings {
constructor() { constructor() {
super();
this.addSettings(['UseLocalProxyForExternalImages','VerifySslCertificate','AllowSelfSigned']);
this.weakPassword = rl.app.weakPassword; this.weakPassword = rl.app.weakPassword;
addObservablesTo(this, { addObservablesTo(this, {
useLocalProxyForExternalImages: !!SettingsGet('UseLocalProxyForExternalImages'),
verifySslCertificate: !!SettingsGet('VerifySslCertificate'),
allowSelfSigned: !!SettingsGet('AllowSelfSigned'),
adminLogin: SettingsGet('AdminLogin'), adminLogin: SettingsGet('AdminLogin'),
adminLoginError: false, adminLoginError: false,
adminPassword: '', adminPassword: '',
adminPasswordNew: '', adminPasswordNew: '',
adminPasswordNew2: '', adminPasswordNew2: '',
adminPasswordNewError: false, adminPasswordNewError: false,
adminTOTP: SettingsGet('AdminTOTP'),
adminPasswordUpdateError: false, adminPasswordUpdateError: false,
adminPasswordUpdateSuccess: false, adminPasswordUpdateSuccess: false,
capaOpenPGP: Settings.capa(Capa.OpenPGP) capaOpenPGP: SettingsCapa('OpenPGP')
}); });
const reset = () => {
this.adminPasswordUpdateError(false);
this.adminPasswordUpdateSuccess(false);
this.adminPasswordNewError(false);
};
addSubscribablesTo(this, { addSubscribablesTo(this, {
adminPassword: () => { adminPassword: () => {
this.adminPasswordUpdateError(false); this.adminPasswordUpdateError(false);
@ -37,39 +43,11 @@ export class SecurityAdminSettings {
adminLogin: () => this.adminLoginError(false), adminLogin: () => this.adminLoginError(false),
adminPasswordNew: () => { adminPasswordNew: reset,
this.adminPasswordUpdateError(false);
this.adminPasswordUpdateSuccess(false);
this.adminPasswordNewError(false);
},
adminPasswordNew2: () => { adminPasswordNew2: reset,
this.adminPasswordUpdateError(false);
this.adminPasswordUpdateSuccess(false);
this.adminPasswordNewError(false);
},
capaOpenPGP: value => capaOpenPGP: value => Remote.saveSetting('CapaOpenPGP', value)
Remote.saveAdminConfig(null, {
CapaOpenPGP: value ? 1 : 0
}),
useLocalProxyForExternalImages: value =>
Remote.saveAdminConfig(null, {
UseLocalProxyForExternalImages: value ? 1 : 0
}),
verifySslCertificate: value => {
value => value || this.allowSelfSigned(true);
Remote.saveAdminConfig(null, {
VerifySslCertificate: value ? 1 : 0
});
},
allowSelfSigned: value =>
Remote.saveAdminConfig(null, {
AllowSelfSigned: value ? 1 : 0
})
}); });
decorateKoCommands(this, { decorateKoCommands(this, {
@ -91,7 +69,7 @@ export class SecurityAdminSettings {
this.adminPasswordUpdateError(false); this.adminPasswordUpdateError(false);
this.adminPasswordUpdateSuccess(false); this.adminPasswordUpdateSuccess(false);
Remote.saveNewAdminPassword((iError, data) => { Remote.request('AdminPasswordUpdate', (iError, data) => {
if (iError) { if (iError) {
this.adminPasswordUpdateError(true); this.adminPasswordUpdateError(true);
} else { } else {
@ -106,7 +84,8 @@ export class SecurityAdminSettings {
}, { }, {
'Login': this.adminLogin(), 'Login': this.adminLogin(),
'Password': this.adminPassword(), 'Password': this.adminPassword(),
'NewPassword': this.adminPasswordNew() 'NewPassword': this.adminPasswordNew(),
'TOTP': this.adminTOTP()
}); });
return true; return true;

View file

@ -1,7 +1,6 @@
import ko from 'ko'; import ko from 'ko';
import { Capa } from 'Common/Enums'; import { SettingsCapa, SettingsGet } from 'Common/Globals';
import { Settings } from 'Common/Globals';
import { AccountUserStore } from 'Stores/User/Account'; import { AccountUserStore } from 'Stores/User/Account';
import { IdentityUserStore } from 'Stores/User/Identity'; import { IdentityUserStore } from 'Stores/User/Identity';
@ -12,17 +11,18 @@ import { showScreenPopup } from 'Knoin/Knoin';
import { AccountPopupView } from 'View/Popup/Account'; import { AccountPopupView } from 'View/Popup/Account';
import { IdentityPopupView } from 'View/Popup/Identity'; import { IdentityPopupView } from 'View/Popup/Identity';
export class AccountsUserSettings { export class UserSettingsAccounts /*extends AbstractViewSettings*/ {
constructor() { constructor() {
this.allowAdditionalAccount = Settings.capa(Capa.AdditionalAccounts); this.allowAdditionalAccount = SettingsCapa('AdditionalAccounts');
this.allowIdentities = Settings.capa(Capa.Identities); this.allowIdentities = SettingsCapa('Identities');
this.accounts = AccountUserStore.accounts; this.accounts = AccountUserStore.accounts;
this.loading = AccountUserStore.loading; this.loading = AccountUserStore.loading;
this.identities = IdentityUserStore; this.identities = IdentityUserStore;
this.mainEmail = SettingsGet('MainEmail');
this.accountForDeletion = ko.observable(null).deleteAccessHelper(); this.accountForDeletion = ko.observable(null).askDeleteHelper();
this.identityForDeletion = ko.observable(null).deleteAccessHelper(); this.identityForDeletion = ko.observable(null).askDeleteHelper();
} }
addNewAccount() { addNewAccount() {
@ -30,7 +30,7 @@ export class AccountsUserSettings {
} }
editAccount(account) { editAccount(account) {
if (account && account.canBeEdit()) { if (account && account.isAdditional()) {
showScreenPopup(AccountPopupView, [account]); showScreenPopup(AccountPopupView, [account]);
} }
} }
@ -48,19 +48,21 @@ export class AccountsUserSettings {
* @returns {void} * @returns {void}
*/ */
deleteAccount(accountToRemove) { deleteAccount(accountToRemove) {
if (accountToRemove && accountToRemove.deleteAccess()) { if (accountToRemove && accountToRemove.askDelete()) {
this.accountForDeletion(null); this.accountForDeletion(null);
if (accountToRemove) { if (accountToRemove) {
this.accounts.remove((account) => accountToRemove === account); this.accounts.remove((account) => accountToRemove === account);
Remote.accountDelete((iError, data) => { Remote.request('AccountDelete', (iError, data) => {
if (!iError && data.Reload) { if (!iError && data.Reload) {
rl.route.root(); rl.route.root();
setTimeout(() => location.reload(), 1); setTimeout(() => location.reload(), 1);
} else { } else {
rl.app.accountsAndIdentities(); rl.app.accountsAndIdentities();
} }
}, accountToRemove.email); }, {
EmailToDelete: accountToRemove.email
});
} }
} }
} }
@ -70,18 +72,23 @@ export class AccountsUserSettings {
* @returns {void} * @returns {void}
*/ */
deleteIdentity(identityToRemove) { deleteIdentity(identityToRemove) {
if (identityToRemove && identityToRemove.deleteAccess()) { if (identityToRemove && identityToRemove.askDelete()) {
this.identityForDeletion(null); this.identityForDeletion(null);
if (identityToRemove) { if (identityToRemove) {
IdentityUserStore.remove(oIdentity => identityToRemove === oIdentity); IdentityUserStore.remove(oIdentity => identityToRemove === oIdentity);
Remote.identityDelete(() => rl.app.accountsAndIdentities(), identityToRemove.id()); Remote.request('IdentityDelete', () => rl.app.accountsAndIdentities(), {
IdToDelete: identityToRemove.id()
});
} }
} }
} }
accountsAndIdentitiesAfterMove() { accountsAndIdentitiesAfterMove() {
Remote.accountsAndIdentitiesSortOrder(null, AccountUserStore.getEmailAddresses(), IdentityUserStore.getIDS()); Remote.request('AccountsAndIdentitiesSortOrder', null, {
Accounts: AccountUserStore.getEmailAddresses().filter(v => v != SettingsGet('MainEmail')),
Identities: IdentityUserStore.getIDS()
});
} }
onBuild(oDom) { onBuild(oDom) {

View file

@ -1,10 +1,11 @@
import ko from 'ko'; import ko from 'ko';
import { koComputable } from 'External/ko';
import { SettingsGet } from 'Common/Globals'; import { SettingsGet } from 'Common/Globals';
import { ContactUserStore } from 'Stores/User/Contact'; import { ContactUserStore } from 'Stores/User/Contact';
import Remote from 'Remote/User/Fetch'; import Remote from 'Remote/User/Fetch';
export class ContactsUserSettings { export class UserSettingsContacts /*extends AbstractViewSettings*/ {
constructor() { constructor() {
this.contactsAutosave = ko.observable(!!SettingsGet('ContactsAutosave')); this.contactsAutosave = ko.observable(!!SettingsGet('ContactsAutosave'));
@ -14,8 +15,7 @@ export class ContactsUserSettings {
this.contactsSyncUser = ContactUserStore.syncUser; this.contactsSyncUser = ContactUserStore.syncUser;
this.contactsSyncPass = ContactUserStore.syncPass; this.contactsSyncPass = ContactUserStore.syncPass;
this.saveTrigger = ko this.saveTrigger = koComputable(() =>
.computed(() =>
[ [
ContactUserStore.enableSync() ? '1' : '0', ContactUserStore.enableSync() ? '1' : '0',
ContactUserStore.syncUrl(), ContactUserStore.syncUrl(),
@ -26,19 +26,16 @@ export class ContactsUserSettings {
.extend({ debounce: 500 }); .extend({ debounce: 500 });
this.contactsAutosave.subscribe(value => this.contactsAutosave.subscribe(value =>
Remote.saveSettings(null, { Remote.saveSettings(null, { ContactsAutosave: value })
ContactsAutosave: value ? 1 : 0
})
); );
this.saveTrigger.subscribe(() => this.saveTrigger.subscribe(() =>
Remote.saveContactsSyncData( Remote.request('SaveContactsSyncData', null, {
null, Enable: ContactUserStore.enableSync() ? 1 : 0,
ContactUserStore.enableSync(), Url: ContactUserStore.syncUrl(),
ContactUserStore.syncUrl(), User: ContactUserStore.syncUser(),
ContactUserStore.syncUser(), Password: ContactUserStore.syncPass()
ContactUserStore.syncPass() })
)
); );
} }
} }

View file

@ -1,100 +1,44 @@
import ko from 'ko'; import { addObservablesTo } from 'External/ko';
import { FolderUserStore } from 'Stores/User/Folder';
import { SettingsGet } from 'Common/Globals';
import { getNotification } from 'Common/Translator'; //export class UserSettingsFilters /*extends AbstractViewSettings*/ {
import { addObservablesTo } from 'Common/Utils'; export class UserSettingsFilters /*extends AbstractViewSettings*/ {
import { delegateRunOnDestroy } from 'Common/UtilsUser';
import { SieveUserStore } from 'Stores/User/Sieve';
import Remote from 'Remote/User/Fetch';
import { SieveScriptModel } from 'Model/SieveScript';
import { showScreenPopup } from 'Knoin/Knoin';
import { SieveScriptPopupView } from 'View/Popup/SieveScript';
export class FiltersUserSettings {
constructor() { constructor() {
this.scripts = SieveUserStore.scripts; this.scripts = ko.observableArray();
this.loading = ko.observable(false).extend({ debounce: 200 }); this.loading = ko.observable(true).extend({ debounce: 200 });
addObservablesTo(this, { addObservablesTo(this, {
serverError: false, serverError: false,
serverErrorDesc: '' serverErrorDesc: ''
}); });
this.scriptForDeletion = ko.observable(null).deleteAccessHelper(); rl.loadScript(SettingsGet('StaticLibsJs').replace('/libs.', '/sieve.')).then(() => {
} const Sieve = window.Sieve;
Sieve.folderList = FolderUserStore.folderList;
Sieve.serverError.subscribe(value => this.serverError(value));
Sieve.serverErrorDesc.subscribe(value => this.serverErrorDesc(value));
Sieve.loading.subscribe(value => this.loading(value));
Sieve.scripts.subscribe(value => this.scripts(value));
Sieve.updateList();
}).catch(e => console.error(e));
setError(text) { this.scriptForDeletion = ko.observable(null).askDeleteHelper();
this.serverError(true);
this.serverErrorDesc(text);
}
updateList() {
if (!this.loading()) {
this.loading(true);
this.serverError(false);
Remote.filtersGet((iError, data) => {
this.loading(false);
this.scripts([]);
if (iError) {
SieveUserStore.capa([]);
this.setError(getNotification(iError));
} else {
SieveUserStore.capa(data.Result.Capa);
/*
this.scripts(
data.Result.Scripts.map(aItem => SieveScriptModel.reviveFromJson(aItem)).filter(v => v)
);
*/
Object.values(data.Result.Scripts).forEach(value => {
value = SieveScriptModel.reviveFromJson(value);
value && this.scripts.push(value)
});
}
});
}
} }
addScript() { addScript() {
showScreenPopup(SieveScriptPopupView, [new SieveScriptModel()]); this.editScript();
} }
editScript(script) { editScript(script) {
showScreenPopup(SieveScriptPopupView, [script]); window.Sieve.ScriptView.showModal(script ? [script] : null);
} }
deleteScript(script) { deleteScript(script) {
this.serverError(false); window.Sieve.deleteScript(script);
Remote.filtersScriptDelete(
(iError, data) => {
if (iError) {
this.setError((data && data.ErrorMessageAdditional) || getNotification(iError));
} else {
this.scripts.remove(script);
delegateRunOnDestroy(script);
}
},
script.name()
);
} }
toggleScript(script) { toggleScript(script) {
let name = script.active() ? '' : script.name(); window.Sieve.toggleScript(script);
this.serverError(false);
Remote.filtersScriptActivate(
(iError, data) => {
if (iError) {
this.setError((data && data.ErrorMessageAdditional) || iError)
} else {
this.scripts.forEach(script => script.active(script.name() === name));
}
},
name
);
} }
onBuild(oDom) { onBuild(oDom) {
@ -106,6 +50,6 @@ export class FiltersUserSettings {
} }
onShow() { onShow() {
this.updateList(); window.Sieve && window.Sieve.updateList();
} }
} }

View file

@ -1,13 +1,15 @@
import ko from 'ko'; import ko from 'ko';
import { koComputable } from 'External/ko';
import { Notification } from 'Common/Enums'; import { Notification } from 'Common/Enums';
import { ClientSideKeyName } from 'Common/EnumsUser'; import { FolderMetadataKeys } from 'Common/EnumsUser';
import { Settings } from 'Common/Globals'; import { SettingsCapa } from 'Common/Globals';
import { getNotification } from 'Common/Translator'; import { getNotification } from 'Common/Translator';
import { removeFolderFromCacheList } from 'Common/Cache'; import { setFolder, getFolderFromCacheList, removeFolderFromCacheList } from 'Common/Cache';
import { defaultOptionsAfterRender } from 'Common/Utils';
import * as Local from 'Storage/Client'; import { sortFolders } from 'Common/Folders';
import { initOnStartOrLangChange, i18n } from 'Common/Translator';
import { FolderUserStore } from 'Stores/User/Folder'; import { FolderUserStore } from 'Stores/User/Folder';
import { SettingsUserStore } from 'Stores/User/Settings'; import { SettingsUserStore } from 'Stores/User/Settings';
@ -18,16 +20,36 @@ import { showScreenPopup } from 'Knoin/Knoin';
import { FolderCreatePopupView } from 'View/Popup/FolderCreate'; import { FolderCreatePopupView } from 'View/Popup/FolderCreate';
import { FolderSystemPopupView } from 'View/Popup/FolderSystem'; import { FolderSystemPopupView } from 'View/Popup/FolderSystem';
import { loadFolders } from 'Model/FolderCollection';
export class FoldersUserSettings { const folderForDeletion = ko.observable(null).askDeleteHelper();
export class UserSettingsFolders /*extends AbstractViewSettings*/ {
constructor() { constructor() {
this.showKolab = koComputable(() => FolderUserStore.hasCapability('METADATA') && SettingsCapa('Kolab'));
this.defaultOptionsAfterRender = defaultOptionsAfterRender;
this.kolabTypeOptions = ko.observableArray();
let i18nFilter = key => i18n('SETTINGS_FOLDERS/TYPE_' + key);
initOnStartOrLangChange(()=>{
this.kolabTypeOptions([
{ id: '', name: '' },
{ id: 'event', name: i18nFilter('CALENDAR') },
{ id: 'contact', name: i18nFilter('CONTACTS') },
{ id: 'task', name: i18nFilter('TASKS') },
{ id: 'note', name: i18nFilter('NOTES') },
{ id: 'file', name: i18nFilter('FILES') },
{ id: 'journal', name: i18nFilter('JOURNAL') },
{ id: 'configuration', name: i18nFilter('CONFIGURATION') }
]);
});
this.displaySpecSetting = FolderUserStore.displaySpecSetting; this.displaySpecSetting = FolderUserStore.displaySpecSetting;
this.folderList = FolderUserStore.folderList; this.folderList = FolderUserStore.folderList;
this.folderListOptimized = FolderUserStore.folderListOptimized; this.folderListOptimized = FolderUserStore.folderListOptimized;
this.folderListError = FolderUserStore.folderListError; this.folderListError = FolderUserStore.folderListError;
this.hideUnsubscribed = SettingsUserStore.hideUnsubscribed; this.hideUnsubscribed = SettingsUserStore.hideUnsubscribed;
this.loading = ko.computed(() => { this.loading = koComputable(() => {
const loading = FolderUserStore.foldersLoading(), const loading = FolderUserStore.foldersLoading(),
creating = FolderUserStore.foldersCreating(), creating = FolderUserStore.foldersCreating(),
deleting = FolderUserStore.foldersDeleting(), deleting = FolderUserStore.foldersDeleting(),
@ -36,28 +58,43 @@ export class FoldersUserSettings {
return loading || creating || deleting || renaming; return loading || creating || deleting || renaming;
}); });
this.folderForDeletion = ko.observable(null).deleteAccessHelper(); this.folderForDeletion = folderForDeletion;
this.folderForEdit = ko.observable(null).extend({ toggleSubscribeProperty: [this, 'edited'] }); this.folderForEdit = ko.observable(null).extend({ toggleSubscribeProperty: [this, 'edited'] });
this.useImapSubscribe = Settings.app('useImapSubscribe'); SettingsUserStore.hideUnsubscribed.subscribe(value => Remote.saveSetting('HideUnsubscribed', value));
SettingsUserStore.hideUnsubscribed.subscribe(value => Remote.saveSetting('HideUnsubscribed', value ? 1 : 0));
} }
folderEditOnEnter(folder) { folderEditOnEnter(folder) {
const nameToEdit = folder ? folder.nameForEdit().trim() : ''; const nameToEdit = folder ? folder.nameForEdit().trim() : '';
if (nameToEdit && folder.name() !== nameToEdit) { if (nameToEdit && folder.name() !== nameToEdit) {
Local.set(ClientSideKeyName.FoldersLashHash, ''); Remote.abort('Folders').post('FolderRename', FolderUserStore.foldersRenaming, {
Folder: folder.fullName,
rl.app.foldersPromisesActionHelper( NewFolderName: nameToEdit,
Remote.folderRename(folder.fullNameRaw, nameToEdit), Subscribe: folder.subscribed() ? 1 : 0
Notification.CantRenameFolder })
); .then(data => {
folder.name(nameToEdit/*data.Name*/);
removeFolderFromCacheList(folder.fullNameRaw); if (folder.subFolders.length) {
Remote.setTrigger(FolderUserStore.foldersLoading, true);
folder.name(nameToEdit); // clearTimeout(Remote.foldersTimeout);
// Remote.foldersTimeout = setTimeout(loadFolders, 500);
setTimeout(loadFolders, 500);
// TODO: rename all subfolders with folder.delimiter to prevent reload?
} else {
removeFolderFromCacheList(folder.fullName);
folder.fullName = data.Result.FullName;
setFolder(folder);
const parent = getFolderFromCacheList(folder.parentName);
sortFolders(parent ? parent.subFolders : FolderUserStore.folderList);
}
})
.catch(error => {
FolderUserStore.folderListError(
getNotification(error.code, '', Notification.CantRenameFolder)
+ '.\n' + error.message);
});
} }
folder.edited(false); folder.edited(false);
@ -85,48 +122,69 @@ export class FoldersUserSettings {
} }
deleteFolder(folderToRemove) { deleteFolder(folderToRemove) {
if ( if (folderToRemove
folderToRemove && && folderToRemove.canBeDeleted()
folderToRemove.canBeDeleted() && && folderToRemove.askDelete()
folderToRemove.deleteAccess() &&
0 === folderToRemove.privateMessageCountAll()
) { ) {
this.folderForDeletion(null); if (0 < folderToRemove.privateMessageCountAll()) {
// FolderUserStore.folderListError(getNotification(Notification.CantDeleteNonEmptyFolder));
folderToRemove.errorMsg(getNotification(Notification.CantDeleteNonEmptyFolder));
} else {
folderForDeletion(null);
if (folderToRemove) { if (folderToRemove) {
Local.set(ClientSideKeyName.FoldersLashHash, ''); Remote.abort('Folders').post('FolderDelete', FolderUserStore.foldersDeleting, {
Folder: folderToRemove.fullName
// rl.app.foldersPromisesActionHelper }).then(
Remote.abort('Folders') () => {
.folderDelete(folderToRemove.fullNameRaw) // folderToRemove.flags.push('\\nonexistent');
.then( folderToRemove.selectable(false);
() => { // folderToRemove.subscribed(false);
folderToRemove.selectable(false) // folderToRemove.checkable(false);
removeFolderFromCacheList(folderToRemove.fullNameRaw); if (!folderToRemove.subFolders.length) {
}, removeFolderFromCacheList(folderToRemove.fullName);
error => { const folder = getFolderFromCacheList(folderToRemove.parentName);
FolderUserStore.folderListError( (folder ? folder.subFolders : FolderUserStore.folderList).remove(folderToRemove);
getNotification(error.code, '', Notification.CantDeleteFolder) }
+ '.\n' + error.message },
); error => {
} FolderUserStore.folderListError(
); getNotification(error.code, '', Notification.CantDeleteFolder)
+ '.\n' + error.message
);
}
);
}
} }
} else if (0 < folderToRemove.privateMessageCountAll()) {
FolderUserStore.folderListError(getNotification(Notification.CantDeleteNonEmptyFolder));
} }
} }
toggleFolderKolabType(folder, event) {
let type = event.target.value;
// TODO: append '.default' ?
Remote.request('FolderSetMetadata', null, {
Folder: folder.fullName,
Key: FolderMetadataKeys.KolabFolderType,
Value: type
});
folder.kolabType(type);
}
toggleFolderSubscription(folder) { toggleFolderSubscription(folder) {
let subscribe = !folder.subscribed(); let subscribe = !folder.subscribed();
Local.set(ClientSideKeyName.FoldersLashHash, ''); Remote.request('FolderSubscribe', null, {
Remote.folderSetSubscribe(()=>0, folder.fullNameRaw, subscribe); Folder: folder.fullName,
Subscribe: subscribe ? 1 : 0
});
folder.subscribed(subscribe); folder.subscribed(subscribe);
} }
toggleFolderCheckable(folder) { toggleFolderCheckable(folder) {
let checkable = !folder.checkable(); let checkable = !folder.checkable();
Remote.folderSetCheckable(()=>0, folder.fullNameRaw, checkable); Remote.request('FolderCheckable', null, {
Folder: folder.fullName,
Checkable: checkable ? 1 : 0
});
folder.checkable(checkable); folder.checkable(checkable);
} }
} }

View file

@ -3,9 +3,11 @@ import ko from 'ko';
import { SaveSettingsStep } from 'Common/Enums'; import { SaveSettingsStep } from 'Common/Enums';
import { EditorDefaultType, Layout } from 'Common/EnumsUser'; import { EditorDefaultType, Layout } from 'Common/EnumsUser';
import { Settings, SettingsGet } from 'Common/Globals'; import { Settings, SettingsGet } from 'Common/Globals';
import { isArray, settingsSaveHelperSimpleFunction, addObservablesTo, addSubscribablesTo, addComputablesTo } from 'Common/Utils'; import { isArray } from 'Common/Utils';
import { i18n, trigger as translatorTrigger, reload as translatorReload, convertLangName } from 'Common/Translator'; import { addSubscribablesTo, addComputablesTo } from 'External/ko';
import { i18n, trigger as translatorTrigger, translatorReload, convertLangName } from 'Common/Translator';
import { AbstractViewSettings } from 'Knoin/AbstractViews';
import { showScreenPopup } from 'Knoin/Knoin'; import { showScreenPopup } from 'Knoin/Knoin';
import { AppUserStore } from 'Stores/User/App'; import { AppUserStore } from 'Stores/User/App';
@ -14,14 +16,17 @@ import { SettingsUserStore } from 'Stores/User/Settings';
import { IdentityUserStore } from 'Stores/User/Identity'; import { IdentityUserStore } from 'Stores/User/Identity';
import { NotificationUserStore } from 'Stores/User/Notification'; import { NotificationUserStore } from 'Stores/User/Notification';
import { MessageUserStore } from 'Stores/User/Message'; import { MessageUserStore } from 'Stores/User/Message';
import { MessagelistUserStore } from 'Stores/User/Messagelist';
import Remote from 'Remote/User/Fetch'; import Remote from 'Remote/User/Fetch';
import { IdentityPopupView } from 'View/Popup/Identity'; import { IdentityPopupView } from 'View/Popup/Identity';
import { LanguagesPopupView } from 'View/Popup/Languages'; import { LanguagesPopupView } from 'View/Popup/Languages';
export class GeneralUserSettings { export class UserSettingsGeneral extends AbstractViewSettings {
constructor() { constructor() {
super();
this.language = LanguageStore.language; this.language = LanguageStore.language;
this.languages = LanguageStore.languages; this.languages = LanguageStore.languages;
this.messageReadDelay = SettingsUserStore.messageReadDelay; this.messageReadDelay = SettingsUserStore.messageReadDelay;
@ -30,13 +35,14 @@ export class GeneralUserSettings {
this.editorDefaultType = SettingsUserStore.editorDefaultType; this.editorDefaultType = SettingsUserStore.editorDefaultType;
this.layout = SettingsUserStore.layout; this.layout = SettingsUserStore.layout;
this.enableSoundNotification = NotificationUserStore.enableSoundNotification; this.soundNotification = NotificationUserStore.enableSoundNotification;
this.notificationSound = ko.observable(SettingsGet('NotificationSound')); this.notificationSound = ko.observable(SettingsGet('NotificationSound'));
this.notificationSounds = ko.observableArray(SettingsGet('NewMailSounds')); this.notificationSounds = ko.observableArray(SettingsGet('NewMailSounds'));
this.enableDesktopNotification = NotificationUserStore.enableDesktopNotification; this.desktopNotification = NotificationUserStore.enableDesktopNotification;
this.isDesktopNotificationAllowed = NotificationUserStore.isDesktopNotificationAllowed; this.isDesktopNotificationAllowed = NotificationUserStore.isDesktopNotificationAllowed;
this.viewHTML = SettingsUserStore.viewHTML;
this.showImages = SettingsUserStore.showImages; this.showImages = SettingsUserStore.showImages;
this.removeColors = SettingsUserStore.removeColors; this.removeColors = SettingsUserStore.removeColors;
this.useCheckboxesInList = SettingsUserStore.useCheckboxesInList; this.useCheckboxesInList = SettingsUserStore.useCheckboxesInList;
@ -45,14 +51,7 @@ export class GeneralUserSettings {
this.replySameFolder = SettingsUserStore.replySameFolder; this.replySameFolder = SettingsUserStore.replySameFolder;
this.allowLanguagesOnSettings = !!SettingsGet('AllowLanguagesOnSettings'); this.allowLanguagesOnSettings = !!SettingsGet('AllowLanguagesOnSettings');
this.languageTrigger = ko.observable(SaveSettingsStep.Idle).extend({ debounce: 100 }); this.languageTrigger = ko.observable(SaveSettingsStep.Idle);
addObservablesTo(this, {
mppTrigger: SaveSettingsStep.Idle,
messageReadDelayTrigger: SaveSettingsStep.Idle,
editorDefaultTypeTrigger: SaveSettingsStep.Idle,
layoutTrigger: SaveSettingsStep.Idle
});
this.identities = IdentityUserStore; this.identities = IdentityUserStore;
@ -89,58 +88,43 @@ export class GeneralUserSettings {
} }
}); });
this.addSetting('EditorDefaultType');
this.addSetting('MessageReadDelay');
this.addSetting('MessagesPerPage');
this.addSetting('Layout', () => MessagelistUserStore([]));
this.addSettings(['ViewHTML', 'ShowImages', 'UseCheckboxesInList', 'ReplySameFolder',
'DesktopNotifications', 'SoundNotification']);
const fReloadLanguageHelper = (saveSettingsStep) => () => { const fReloadLanguageHelper = (saveSettingsStep) => () => {
this.languageTrigger(saveSettingsStep); this.languageTrigger(saveSettingsStep);
setTimeout(() => this.languageTrigger(SaveSettingsStep.Idle), 1000); setTimeout(() => this.languageTrigger(SaveSettingsStep.Idle), 1000);
}; };
addSubscribablesTo(this, { addSubscribablesTo(this, {
language: value => { language: value => {
this.languageTrigger(SaveSettingsStep.Animate); this.languageTrigger(SaveSettingsStep.Animate);
translatorReload(false, value) translatorReload(false, value)
.then(fReloadLanguageHelper(SaveSettingsStep.TrueResult), .then(fReloadLanguageHelper(SaveSettingsStep.TrueResult), fReloadLanguageHelper(SaveSettingsStep.FalseResult))
fReloadLanguageHelper(SaveSettingsStep.FalseResult))
.then(() => Remote.saveSetting('Language', value)); .then(() => Remote.saveSetting('Language', value));
}, },
editorDefaultType: value => Remote.saveSetting('EditorDefaultType', value,
settingsSaveHelperSimpleFunction(this.editorDefaultTypeTrigger, this)),
messageReadDelay: value => Remote.saveSetting('MessageReadDelay', value,
settingsSaveHelperSimpleFunction(this.messageReadDelayTrigger, this)),
messagesPerPage: value => Remote.saveSetting('MPP', value,
settingsSaveHelperSimpleFunction(this.mppTrigger, this)),
showImages: value => Remote.saveSetting('ShowImages', value ? 1 : 0),
removeColors: value => { removeColors: value => {
let dom = MessageUserStore.messagesBodiesDom(); let dom = MessageUserStore.bodiesDom();
if (dom) { if (dom) {
dom.innerHTML = ''; dom.innerHTML = '';
} }
Remote.saveSetting('RemoveColors', value ? 1 : 0); Remote.saveSetting('RemoveColors', value);
}, },
useCheckboxesInList: value => Remote.saveSetting('UseCheckboxesInList', value ? 1 : 0),
enableDesktopNotification: value => Remote.saveSetting('DesktopNotifications', value ? 1 : 0),
enableSoundNotification: value => Remote.saveSetting('SoundNotification', value ? 1 : 0),
notificationSound: value => { notificationSound: value => {
Remote.saveSetting('NotificationSound', value); Remote.saveSetting('NotificationSound', value);
Settings.set('NotificationSound', value); Settings.set('NotificationSound', value);
}, },
replySameFolder: value => Remote.saveSetting('ReplySameFolder', value ? 1 : 0),
useThreads: value => { useThreads: value => {
MessageUserStore.list([]); MessagelistUserStore([]);
Remote.saveSetting('UseThreads', value ? 1 : 0); Remote.saveSetting('UseThreads', value);
},
layout: value => {
MessageUserStore.list([]);
Remote.saveSetting('Layout', value, settingsSaveHelperSimpleFunction(this.layoutTrigger, this));
} }
}); });
} }

View file

@ -1,66 +0,0 @@
import ko from 'ko';
import { delegateRunOnDestroy } from 'Common/UtilsUser';
import { PgpUserStore } from 'Stores/User/Pgp';
import { SettingsUserStore } from 'Stores/User/Settings';
import Remote from 'Remote/User/Fetch';
import { showScreenPopup } from 'Knoin/Knoin';
import { AddOpenPgpKeyPopupView } from 'View/Popup/AddOpenPgpKey';
import { NewOpenPgpKeyPopupView } from 'View/Popup/NewOpenPgpKey';
import { ViewOpenPgpKeyPopupView } from 'View/Popup/ViewOpenPgpKey';
export class OpenPgpUserSettings {
constructor() {
this.openpgpkeys = PgpUserStore.openpgpkeys;
this.openpgpkeysPublic = PgpUserStore.openpgpkeysPublic;
this.openpgpkeysPrivate = PgpUserStore.openpgpkeysPrivate;
this.openPgpKeyForDeletion = ko.observable(null).deleteAccessHelper();
this.allowDraftAutosave = SettingsUserStore.allowDraftAutosave;
this.allowDraftAutosave.subscribe(value => Remote.saveSetting('AllowDraftAutosave', value ? 1 : 0))
}
addOpenPgpKey() {
showScreenPopup(AddOpenPgpKeyPopupView);
}
generateOpenPgpKey() {
showScreenPopup(NewOpenPgpKeyPopupView);
}
viewOpenPgpKey(openPgpKey) {
if (openPgpKey) {
showScreenPopup(ViewOpenPgpKeyPopupView, [openPgpKey]);
}
}
/**
* @param {OpenPgpKeyModel} openPgpKeyToRemove
* @returns {void}
*/
deleteOpenPgpKey(openPgpKeyToRemove) {
if (openPgpKeyToRemove && openPgpKeyToRemove.deleteAccess()) {
this.openPgpKeyForDeletion(null);
if (openPgpKeyToRemove && PgpUserStore.openpgpKeyring) {
const findedItem = PgpUserStore.openpgpkeys.find(key => openPgpKeyToRemove === key);
if (findedItem) {
PgpUserStore.openpgpkeys.remove(findedItem);
delegateRunOnDestroy(findedItem);
PgpUserStore.openpgpKeyring[findedItem.isPrivate ? 'privateKeys' : 'publicKeys'].removeForId(findedItem.guid);
PgpUserStore.openpgpKeyring.store();
}
rl.app.reloadOpenPgpKeys();
}
}
}
}

View file

@ -1,23 +1,32 @@
import ko from 'ko'; import { koComputable } from 'External/ko';
import { pInt, settingsSaveHelperSimpleFunction } from 'Common/Utils'; import { SettingsCapa } from 'Common/Globals';
import { Capa, SaveSettingsStep } from 'Common/Enums';
import { Settings } from 'Common/Globals';
import { i18n, trigger as translatorTrigger } from 'Common/Translator'; import { i18n, trigger as translatorTrigger } from 'Common/Translator';
import { AbstractViewSettings } from 'Knoin/AbstractViews';
import { SettingsUserStore } from 'Stores/User/Settings'; import { SettingsUserStore } from 'Stores/User/Settings';
import { GnuPGUserStore } from 'Stores/User/GnuPG';
import { OpenPGPUserStore } from 'Stores/User/OpenPGP';
import Remote from 'Remote/User/Fetch'; import Remote from 'Remote/User/Fetch';
export class SecurityUserSettings { import { showScreenPopup } from 'Knoin/Knoin';
import { OpenPgpImportPopupView } from 'View/Popup/OpenPgpImport';
import { OpenPgpGeneratePopupView } from 'View/Popup/OpenPgpGenerate';
export class UserSettingsSecurity extends AbstractViewSettings {
constructor() { constructor() {
this.capaAutoLogout = Settings.capa(Capa.AutoLogout); super();
this.capaAutoLogout = SettingsCapa('AutoLogout');
this.autoLogout = SettingsUserStore.autoLogout; this.autoLogout = SettingsUserStore.autoLogout;
this.autoLogoutTrigger = ko.observable(SaveSettingsStep.Idle);
let i18nLogout = (key, params) => i18n('SETTINGS_SECURITY/AUTOLOGIN_' + key, params); let i18nLogout = (key, params) => i18n('SETTINGS_SECURITY/AUTOLOGIN_' + key, params);
this.autoLogoutOptions = ko.computed(() => { this.autoLogoutOptions = koComputable(() => {
translatorTrigger(); translatorTrigger();
return [ return [
{ id: 0, name: i18nLogout('NEVER_OPTION_NAME') }, { id: 0, name: i18nLogout('NEVER_OPTION_NAME') },
@ -32,10 +41,37 @@ export class SecurityUserSettings {
}); });
if (this.capaAutoLogout) { if (this.capaAutoLogout) {
this.autoLogout.subscribe(value => Remote.saveSetting( this.addSetting('AutoLogout');
'AutoLogout', pInt(value),
settingsSaveHelperSimpleFunction(this.autoLogoutTrigger, this)
));
} }
this.gnupgPublicKeys = GnuPGUserStore.publicKeys;
this.gnupgPrivateKeys = GnuPGUserStore.privateKeys;
this.openpgpkeysPublic = OpenPGPUserStore.publicKeys;
this.openpgpkeysPrivate = OpenPGPUserStore.privateKeys;
this.canOpenPGP = SettingsCapa('OpenPGP');
this.canGnuPG = GnuPGUserStore.isSupported();
this.canMailvelope = !!window.mailvelope;
this.allowDraftAutosave = SettingsUserStore.allowDraftAutosave;
this.allowDraftAutosave.subscribe(value => Remote.saveSetting('AllowDraftAutosave', value))
}
addOpenPgpKey() {
showScreenPopup(OpenPgpImportPopupView);
}
generateOpenPgpKey() {
showScreenPopup(OpenPgpGeneratePopupView);
}
onBuild() {
/**
* Create an iframe to display the Mailvelope keyring settings.
* The iframe will be injected into the container identified by selector.
*/
window.mailvelope && mailvelope.createSettingsContainer('#mailvelope-settings'/*[, keyring], options*/);
} }
} }

View file

@ -10,7 +10,7 @@ import { showScreenPopup } from 'Knoin/Knoin';
import { TemplatePopupView } from 'View/Popup/Template'; import { TemplatePopupView } from 'View/Popup/Template';
import { addComputablesTo } from 'Common/Utils'; import { addComputablesTo } from 'Common/Utils';
export class TemplatesUserSettings { export class UserSettingsTemplates {
constructor() { constructor() {
this.templates = TemplateUserStore.templates; this.templates = TemplateUserStore.templates;

View file

@ -1,33 +1,37 @@
import ko from 'ko'; import { addObservablesTo } from 'External/ko';
import { SaveSettingsStep, UploadErrorCode, Capa } from 'Common/Enums'; import { SaveSettingsStep, UploadErrorCode } from 'Common/Enums';
import { changeTheme, convertThemeName } from 'Common/Utils'; import { changeTheme, convertThemeName } from 'Common/Utils';
import { themePreviewLink, serverRequest } from 'Common/Links'; import { themePreviewLink, serverRequest } from 'Common/Links';
import { i18n } from 'Common/Translator'; import { i18n } from 'Common/Translator';
import { Settings } from 'Common/Globals'; import { SettingsCapa } from 'Common/Globals';
import { ThemeStore } from 'Stores/Theme'; import { ThemeStore } from 'Stores/Theme';
import Remote from 'Remote/User/Fetch'; import Remote from 'Remote/User/Fetch';
export class ThemesUserSettings { const themeBackground = {
name: ThemeStore.userBackgroundName,
hash: ThemeStore.userBackgroundHash
};
addObservablesTo(themeBackground, {
uploaderButton: null,
loading: false,
error: ''
});
export class UserSettingsThemes /*extends AbstractViewSettings*/ {
constructor() { constructor() {
this.theme = ThemeStore.theme; this.theme = ThemeStore.theme;
this.themes = ThemeStore.themes; this.themes = ThemeStore.themes;
this.themesObjects = ko.observableArray(); this.themesObjects = ko.observableArray();
this.background = {}; themeBackground.enabled = SettingsCapa('UserBackground');
this.background.name = ThemeStore.userBackgroundName; this.background = themeBackground;
this.background.hash = ThemeStore.userBackgroundHash;
this.background.uploaderButton = ko.observable(null);
this.background.loading = ko.observable(false);
this.background.error = ko.observable('');
this.capaUserBackground = ko.observable(Settings.capa(Capa.UserBackground));
this.themeTrigger = ko.observable(SaveSettingsStep.Idle).extend({ debounce: 100 }); this.themeTrigger = ko.observable(SaveSettingsStep.Idle).extend({ debounce: 100 });
this.theme.subscribe((value) => { ThemeStore.theme.subscribe(value => {
this.themesObjects.forEach(theme => { this.themesObjects.forEach(theme => {
theme.selected(value === theme.name); theme.selected(value === theme.name);
}); });
@ -41,10 +45,10 @@ export class ThemesUserSettings {
} }
onBuild() { onBuild() {
const currentTheme = this.theme(); const currentTheme = ThemeStore.theme();
this.themesObjects( this.themesObjects(
this.themes.map(theme => ({ ThemeStore.themes.map(theme => ({
name: theme, name: theme,
nameDisplay: convertThemeName(theme), nameDisplay: convertThemeName(theme),
selected: ko.observable(theme === currentTheme), selected: ko.observable(theme === currentTheme),
@ -54,28 +58,28 @@ export class ThemesUserSettings {
// initUploader // initUploader
if (this.background.uploaderButton() && this.capaUserBackground()) { if (themeBackground.uploaderButton() && themeBackground.enabled) {
const oJua = new Jua({ const oJua = new Jua({
action: serverRequest('UploadBackground'), action: serverRequest('UploadBackground'),
limit: 1, limit: 1,
clickElement: this.background.uploaderButton() clickElement: themeBackground.uploaderButton()
}); });
oJua oJua
.on('onStart', () => { .on('onStart', () => {
this.background.loading(true); themeBackground.loading(true);
this.background.error(''); themeBackground.error('');
return true; return true;
}) })
.on('onComplete', (id, result, data) => { .on('onComplete', (id, result, data) => {
this.background.loading(false); themeBackground.loading(false);
if (result && id && data && data.Result && data.Result.Name && data.Result.Hash) { if (result && id && data && data.Result && data.Result.Name && data.Result.Hash) {
this.background.name(data.Result.Name); themeBackground.name(data.Result.Name);
this.background.hash(data.Result.Hash); themeBackground.hash(data.Result.Hash);
} else { } else {
this.background.name(''); themeBackground.name('');
this.background.hash(''); themeBackground.hash('');
let errorMsg = ''; let errorMsg = '';
if (data.ErrorCode) { if (data.ErrorCode) {
@ -94,7 +98,7 @@ export class ThemesUserSettings {
errorMsg = data.ErrorMessage; errorMsg = data.ErrorMessage;
} }
this.background.error(errorMsg || i18n('SETTINGS_THEMES/ERROR_UNKNOWN')); themeBackground.error(errorMsg || i18n('SETTINGS_THEMES/ERROR_UNKNOWN'));
} }
return true; return true;
@ -103,14 +107,14 @@ export class ThemesUserSettings {
} }
onShow() { onShow() {
this.background.error(''); themeBackground.error('');
} }
clearBackground() { clearBackground() {
if (this.capaUserBackground()) { if (themeBackground.enabled) {
Remote.clearUserBackground(() => { Remote.request('ClearUserBackground', () => {
this.background.name(''); themeBackground.name('');
this.background.hash(''); themeBackground.hash('');
}); });
} }
} }

185
dev/Sieve/Commands.js Normal file
View file

@ -0,0 +1,185 @@
/**
* https://tools.ietf.org/html/rfc5228#section-2.9
*/
import { capa } from 'Sieve/Utils';
import {
GrammarCommand,
GrammarString,
GrammarStringList,
GrammarQuotedString
} from 'Sieve/Grammar';
/**
* https://tools.ietf.org/html/rfc5228#section-3.1
* Usage:
* if <test1: test> <block1: block>
* elsif <test2: test> <block2: block>
* else <block3: block>
*/
export class ConditionalCommand extends GrammarCommand
{
constructor()
{
super();
this.test = null;
}
toString()
{
return this.identifier + ' ' + this.test + ' ' + this.commands;
}
/*
public function pushArguments(array $args): void
{
print_r($args);
exit;
}
*/
}
export class IfCommand extends ConditionalCommand
{
}
export class ElsIfCommand extends ConditionalCommand
{
}
export class ElseCommand extends ConditionalCommand
{
toString()
{
return this.identifier + ' ' + this.commands;
}
}
/**
* https://tools.ietf.org/html/rfc5228#section-3.2
*/
export class RequireCommand extends GrammarCommand
{
constructor()
{
super();
this.capabilities = new GrammarStringList();
}
toString()
{
return 'require ' + this.capabilities.toString() + ';';
}
pushArguments(args)
{
if (args[0] instanceof GrammarStringList) {
this.capabilities = args[0];
} else if (args[0] instanceof GrammarQuotedString) {
this.capabilities.push(args[0]);
}
}
}
/**
* https://tools.ietf.org/html/rfc5228#section-3.3
*/
export class StopCommand extends GrammarCommand
{
}
/**
* https://tools.ietf.org/html/rfc5228#section-4.1
*/
export class FileIntoCommand extends GrammarCommand
{
constructor()
{
super();
// QuotedString / MultiLine
this._mailbox = new GrammarQuotedString();
}
get require() { return 'fileinto'; }
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
+ ';';
}
get mailbox()
{
return this._mailbox.value;
}
set mailbox(value)
{
this._mailbox.value = value;
}
pushArguments(args)
{
if (args[0] instanceof GrammarString) {
this._mailbox = args[0];
}
}
}
/**
* https://tools.ietf.org/html/rfc5228#section-4.2
*/
export class RedirectCommand extends GrammarCommand
{
constructor()
{
super();
// QuotedString / MultiLine
this._address = new GrammarQuotedString();
}
toString()
{
return 'redirect '
// https://datatracker.ietf.org/doc/html/rfc3894
+ ((this.copy && capa.includes('copy')) ? ':copy ' : '')
+ this._address
+ ';';
}
get address()
{
return this._address.value;
}
set address(value)
{
this._address.value = value;
}
pushArguments(args)
{
if (args[0] instanceof GrammarString) {
this._address = args[0];
}
}
}
/**
* https://tools.ietf.org/html/rfc5228#section-4.3
*/
export class KeepCommand extends GrammarCommand
{
}
/**
* https://tools.ietf.org/html/rfc5228#section-4.4
*/
export class DiscardCommand extends GrammarCommand
{
}

View file

@ -0,0 +1,45 @@
/**
* https://tools.ietf.org/html/rfc5173
*/
import {
GrammarString,
GrammarStringList,
GrammarTest
} from 'Sieve/Grammar';
export class BodyTest extends GrammarTest
{
constructor()
{
super();
this.body_transform = ''; // :raw, :content <string-list>, :text
this.key_list = new GrammarStringList;
}
get require() { return 'body'; }
toString()
{
return 'body'
+ (this.comparator ? ' :comparator ' + this.comparator : '')
+ ' ' + this.match_type
+ ' ' + this.body_transform
+ ' ' + this.key_list.toString();
}
pushArguments(args)
{
args.forEach((arg, i) => {
if (':raw' === arg || ':text' === arg) {
this.body_transform = arg;
} else if (arg instanceof GrammarStringList || arg instanceof GrammarString) {
if (':content' === args[i-1]) {
this.body_transform = ':content ' + arg;
} else {
this[args[i+1] ? 'content_list' : 'key_list'] = arg;
}
}
});
}
}

View file

@ -0,0 +1,36 @@
/**
* https://tools.ietf.org/html/rfc5183
*/
import {
GrammarQuotedString,
GrammarStringList,
GrammarTest
} from 'Sieve/Grammar';
export class EnvironmentTest extends GrammarTest
{
constructor()
{
super();
this.name = new GrammarQuotedString;
this.key_list = new GrammarStringList;
}
get require() { return 'environment'; }
toString()
{
return 'environment'
+ (this.comparator ? ' :comparator ' + this.comparator : '')
+ ' ' + this.match_type
+ ' ' + this.name
+ ' ' + this.key_list.toString();
}
pushArguments(args)
{
this.key_list = args.pop();
this.name = args.pop();
}
}

View file

@ -0,0 +1,71 @@
/**
* https://tools.ietf.org/html/rfc5229
*/
import {
GrammarCommand,
GrammarQuotedString,
GrammarStringList,
GrammarTest
} from 'Sieve/Grammar';
export class SetCommand extends GrammarCommand
{
constructor()
{
super();
this.modifiers = [];
this._name = new GrammarQuotedString;
this._value = new GrammarQuotedString;
}
get require() { return 'variables'; }
toString()
{
return 'set'
+ ' ' + this.modifiers.join(' ')
+ ' ' + this._name
+ ' ' + 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 => {
args.includes(modifier) && this.modifiers.push(modifier);
});
this._value = args.pop();
this._name = args.pop();
}
}
export class StringTest extends GrammarTest
{
constructor()
{
super();
this.source = new GrammarStringList;
this.key_list = new GrammarStringList;
}
toString()
{
return 'string'
+ ' ' + this.match_type
+ (this.comparator ? ' :comparator ' + this.comparator : '')
+ ' ' + this.source.toString()
+ ' ' + this.key_list.toString();
}
pushArguments(args)
{
this.key_list = args.pop();
this.source = args.pop();
}
}

View file

@ -0,0 +1,87 @@
/**
* https://tools.ietf.org/html/rfc5230
* https://tools.ietf.org/html/rfc6131
*/
import { capa } from 'Sieve/Utils';
import {
GrammarCommand,
GrammarNumber,
GrammarQuotedString,
GrammarStringList
} from 'Sieve/Grammar';
export class VacationCommand extends GrammarCommand
{
constructor()
{
super();
this._days = new GrammarNumber;
this._seconds = new GrammarNumber;
this._subject = new GrammarQuotedString;
this._from = new GrammarQuotedString;
this.addresses = new GrammarStringList;
this.mime = false;
this._handle = new GrammarQuotedString;
this._reason = new GrammarQuotedString; // QuotedString / MultiLine
}
// 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.arguments[':from'];
}
if (this.addresses.length) {
result += ' :addresses ' + this.addresses.toString();
}
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; }
get from() { return this._from.value; }
get handle() { return this._handle.value; }
get reason() { return this._reason.value; }
set days(int) { this._days.value = int; }
set seconds(int) { this._seconds.value = int; }
set subject(str) { this._subject.value = str; }
set from(str) { this._from.value = str; }
set handle(str) { this._handle.value = str; }
set reason(str) { this._reason.value = str; }
pushArguments(args)
{
this._reason.value = args.pop().value; // GrammarQuotedString
args.forEach((arg, i) => {
if (':mime' === arg) {
this.mime = true;
} else if (':addresses' === args[i-1]) {
this.addresses = arg; // GrammarStringList
} else if (':' === args[i-1][0]) {
// :days, :seconds, :subject, :from, :handle
this[args[i-1].replace(':','_')].value = arg.value;
}
});
}
}

View file

@ -0,0 +1,94 @@
/**
* https://tools.ietf.org/html/rfc5232
*/
import {
GrammarCommand,
GrammarQuotedString,
GrammarString,
GrammarStringList,
GrammarTest
} from 'Sieve/Grammar';
class FlagCommand extends GrammarCommand
{
constructor()
{
super();
this._variablename = new GrammarQuotedString;
this.list_of_flags = new GrammarStringList;
}
get require() { return 'imap4flags'; }
toString()
{
return this.identifier + ' ' + this._variablename + ' ' + this.list_of_flags.toString() + ';';
}
get variablename()
{
return this._variablename.value;
}
set variablename(value)
{
this._variablename.value = value;
}
pushArguments(args)
{
if (args[1]) {
if (args[0] instanceof GrammarQuotedString) {
this._variablename = args[0];
}
if (args[1] instanceof GrammarString) {
this.list_of_flags = args[1];
}
} else if (args[0] instanceof GrammarString) {
this.list_of_flags = args[0];
}
}
}
export class SetFlagCommand extends FlagCommand
{
}
export class AddFlagCommand extends FlagCommand
{
}
export class RemoveFlagCommand extends FlagCommand
{
}
export class HasFlagTest extends GrammarTest
{
constructor()
{
super();
this.variable_list = new GrammarStringList;
this.list_of_flags = new GrammarStringList;
}
get require() { return 'imap4flags'; }
toString()
{
return 'hasflag'
+ ' ' + this.match_type
+ (this.comparator ? ' :comparator ' + this.comparator : '')
+ ' ' + this.variable_list.toString()
+ ' ' + this.list_of_flags.toString();
}
pushArguments(args)
{
args.forEach((arg, i) => {
if (arg instanceof GrammarStringList || arg instanceof GrammarString) {
this[args[i+1] ? 'variable_list' : 'list_of_flags'] = arg;
}
});
}
}

View file

@ -0,0 +1,70 @@
/**
* https://tools.ietf.org/html/rfc5235
*/
import {
GrammarQuotedString,
GrammarString,
GrammarTest
} from 'Sieve/Grammar';
export class SpamTestTest extends GrammarTest
{
constructor()
{
super();
this.percent = false, // 0 - 100 else 0 - 10
this.value = new GrammarQuotedString;
}
// get require() { return this.percent ? 'spamtestplus' : 'spamtest'; }
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.value;
}
pushArguments(args)
{
args.forEach(arg => {
if (':percent' === arg) {
this.percent = true;
} else if (arg instanceof GrammarString) {
this.value = arg;
}
});
}
}
export class VirusTestTest extends GrammarTest
{
constructor()
{
super();
this.value = new GrammarQuotedString; // 1 - 5
}
get require() { return /:value|:count/.test(this.match_type) ? ['virustest','relational'] : 'virustest'; }
toString()
{
return 'virustest'
+ (this.comparator ? ' :comparator ' + this.comparator : '')
+ ' ' + this.match_type
+ ' ' + this.value;
}
pushArguments(args)
{
args.forEach(arg => {
if (arg instanceof GrammarString) {
this.value = arg;
}
});
}
}

View file

@ -0,0 +1,93 @@
/**
* https://tools.ietf.org/html/rfc5260
*/
import {
GrammarNumber,
GrammarQuotedString,
GrammarStringList,
GrammarTest
} from 'Sieve/Grammar';
export class DateTest extends GrammarTest
{
constructor()
{
super();
this.zone = new GrammarQuotedString;
this.originalzone = false;
this.header_name = new GrammarQuotedString;
this.date_part = new GrammarQuotedString;
this.key_list = new GrammarStringList;
// rfc5260#section-6
this.index = new GrammarNumber;
this.last = false;
}
// get require() { return ['date','index']; }
get require() { return 'date'; }
toString()
{
return 'date'
+ (this.last ? ' :last' : (this.index.value ? ' :index ' + this.index : ''))
+ (this.originalzone ? ' :originalzone' : (this.zone.length ? ' :zone ' + this.zone : ''))
+ (this.comparator ? ' :comparator ' + this.comparator : '')
+ ' ' + this.match_type
+ ' ' + this.header_name
+ ' ' + this.date_part
+ ' ' + this.key_list.toString();
}
pushArguments(args)
{
this.key_list = 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 (':zone' === args[i-1]) {
this.zone.value = arg.value;
} else if (':index' === args[i-1]) {
this.index.value = arg.value;
}
});
}
}
export class CurrentDateTest extends GrammarTest
{
constructor()
{
super();
this.zone = new GrammarQuotedString;
this.date_part = new GrammarQuotedString;
this.key_list = new GrammarStringList;
}
get require() { return 'date'; }
toString()
{
return 'currentdate'
+ (this.zone.length ? ' :zone ' + this.zone : '')
+ (this.comparator ? ' :comparator ' + this.comparator : '')
+ ' ' + this.match_type
+ ' ' + this.date_part
+ ' ' + this.key_list.toString();
}
pushArguments(args)
{
this.key_list = args.pop();
this.date_part = args.pop();
args.forEach((arg, i) => {
if (':zone' === args[i-1]) {
this.zone.value = arg.value;
}
});
}
}

View file

@ -0,0 +1,85 @@
/**
* https://tools.ietf.org/html/rfc5293
*/
import {
GrammarCommand,
GrammarNumber,
GrammarQuotedString,
GrammarString,
GrammarStringList
} from 'Sieve/Grammar';
export class AddHeaderCommand extends GrammarCommand
{
constructor()
{
super();
this.last = false;
this.field_name = new GrammarQuotedString;
this.value = new GrammarQuotedString;
}
get require() { return 'editheader'; }
toString()
{
return this.identifier
+ (this.last ? ' :last' : '')
+ ' ' + this.field_name
+ ' ' + this.value + ';';
}
pushArguments(args)
{
this.value = args.pop();
this.field_name = args.pop();
this.last = args.includes(':last');
}
}
export class DeleteHeaderCommand extends GrammarCommand
{
constructor()
{
super();
this.index = new GrammarNumber;
this.last = false;
this.comparator = '',
this.match_type = ':is',
this.field_name = new GrammarQuotedString;
this.value_patterns = new GrammarStringList;
}
get require() { return 'editheader'; }
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.value_patterns + ';';
}
pushArguments(args)
{
let l = args.length - 1;
args.forEach((arg, i) => {
if (':last' === arg) {
this.last = true;
} else if (':index' === args[i-1]) {
this.index.value = arg.value;
args[i] = null;
}
});
if (args[l-1] instanceof GrammarString) {
this.field_name = args[l-1];
this.value_patterns = args[l];
} else {
this.field_name = args[l];
}
}
}

View file

@ -0,0 +1,81 @@
/**
* https://tools.ietf.org/html/rfc5429
*/
import {
GrammarCommand,
GrammarQuotedString,
GrammarString
} from 'Sieve/Grammar';
/**
* https://tools.ietf.org/html/rfc5429#section-2.1
*/
export class ErejectCommand extends GrammarCommand
{
constructor()
{
super();
this._reason = new GrammarQuotedString;
}
get require() { return 'ereject'; }
toString()
{
return 'ereject ' + this._reason + ';';
}
get reason()
{
return this._reason.value;
}
set reason(value)
{
this._reason.value = value;
}
pushArguments(args)
{
if (args[0] instanceof GrammarString) {
this._reason = args[0];
}
}
}
/**
* https://tools.ietf.org/html/rfc5429#section-2.2
*/
export class RejectCommand extends GrammarCommand
{
constructor()
{
super();
this._reason = new GrammarQuotedString;
}
get require() { return 'reject'; }
toString()
{
return 'reject ' + this._reason + ';';
}
get reason()
{
return this._reason.value;
}
set reason(value)
{
this._reason.value = value;
}
pushArguments(args)
{
if (args[0] instanceof GrammarString) {
this._reason = args[0];
}
}
}

View file

@ -0,0 +1,123 @@
/**
* https://tools.ietf.org/html/rfc5435
*/
import {
GrammarCommand,
GrammarNumber,
GrammarQuotedString,
GrammarStringList,
GrammarTest
} from 'Sieve/Grammar';
/**
* https://datatracker.ietf.org/doc/html/rfc5435#section-3
*/
export class NotifyCommand extends GrammarCommand
{
constructor()
{
super();
this._method = new GrammarQuotedString;
this._from = new GrammarQuotedString;
this._importance = new GrammarNumber;
this.options = new GrammarStringList;
this._message = new GrammarQuotedString;
}
get method() { return this._method.value; }
get from() { return this._from.value; }
get importance() { return this._importance.value; }
get message() { return this._message.value; }
set method(str) { this._method.value = str; }
set from(str) { this._from.value = str; }
set importance(int) { this._importance.value = int; }
set message(str) { this._message.value = str; }
get require() { return 'enotify'; }
toString()
{
let result = 'notify';
if (this._from.value) {
result += ' :from ' + this._from;
}
if (0 < this._importance.value) {
result += ' :importance ' + this._importance;
}
if (this.options.length) {
result += ' :options ' + this.options;
}
if (this._message.value) {
result += ' :message ' + this._message;
}
return result + ' ' + this._method;
}
pushArguments(args)
{
this._method.value = args.pop().value; // GrammarQuotedString
args.forEach((arg, i) => {
if (':options' === args[i-1]) {
this.options = arg; // GrammarStringList
} else if (':' === args[i-1][0]) {
// :from, :importance, :message
this[args[i-1].replace(':','_')].value = arg.value;
}
});
}
}
/**
* https://datatracker.ietf.org/doc/html/rfc5435#section-4
*/
export class ValidNotifyMethodTest extends GrammarTest
{
constructor()
{
super();
this.notification_uris = new GrammarStringList;
}
toString()
{
return 'valid_notify_method ' + this.notification_uris;
}
pushArguments(args)
{
this.notification_uris = args.pop();
}
}
/**
* https://datatracker.ietf.org/doc/html/rfc5435#section-5
*/
export class NotifyMethodCapabilityTest extends GrammarTest
{
constructor()
{
super();
this.notification_uri = new GrammarQuotedString;
this.notification_capability = new GrammarQuotedString;
this.key_list = new GrammarStringList;
}
toString()
{
return 'valid_notify_method '
+ (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();
}
}

View file

@ -0,0 +1,58 @@
/**
* https://tools.ietf.org/html/rfc5463
*/
import {
GrammarCommand,
GrammarTest,
GrammarQuotedString,
GrammarStringList
} from 'Sieve/Grammar';
/**
* https://datatracker.ietf.org/doc/html/rfc5463#section-4
*/
export class IHaveTest extends GrammarTest
{
constructor()
{
super();
this.capabilities = new GrammarStringList;
}
get require() { return 'ihave'; }
toString()
{
return 'ihave ' + this.capabilities;
}
pushArguments(args)
{
this.capabilities = args.pop();
}
}
/**
* https://datatracker.ietf.org/doc/html/rfc5463#section-5
*/
export class ErrorCommand extends GrammarCommand
{
constructor()
{
super();
this.message = new GrammarQuotedString;
}
get require() { return 'ihave'; }
toString()
{
return 'error ' + this.message + ';';
}
pushArguments(args)
{
this.message = args.pop();
}
}

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