Merge branch 'master' into plugin-hooks

# Conflicts:
#	dev/Common/Plugins.js
#	dev/Knoin/Knoin.js
#	dev/Remote/AbstractFetch.js
#	dev/View/User/Login.js
This commit is contained in:
djmaze 2022-03-15 14:27:54 +01:00
commit 827cfa5051
892 changed files with 110819 additions and 48675 deletions

View file

@ -15,7 +15,7 @@ RUN pecl install xxtea-1.0.11 && \
RUN docker-php-ext-configure intl && \
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
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 && \
docker-php-ext-configure ldap --with-libdir=lib/x86_64-linux-gnu/ && \
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 && \
php -m

View file

@ -1,4 +1,4 @@
; SnappyMail Webmail configuration file
; SnappyMail configuration file
; Please don't add custom parameters here, those will be overwritten
[webmail]
@ -27,38 +27,27 @@ allow_languages_on_settings = On
allow_additional_accounts = On
allow_additional_identities = On
; Number of messages displayed on page by default
; Number of messages displayed on page by default
messages_per_page = 20
; Mark message read after N seconds
message_read_delay = 5
; File size limit (MB) for file upload on compose screen
; 0 for unlimited.
attachment_size_limit = 25
attachment_size_limit = 2
[interface]
show_attachment_thumbnail = On
use_native_scrollbars = Off
new_move_to_folder_button = On
[branding]
login_logo = ""
login_background = ""
login_desc = ""
login_css = ""
user_css = ""
user_logo = ""
user_logo_title = ""
user_logo_message = ""
user_iframe_message = ""
welcome_page_url = ""
welcome_page_display = "none"
[contacts]
; Enable contacts
enable = Off
allow_sync = On
allow_sync = Off
sync_interval = 20
type = "sqlite"
pdo_dsn = "mysql:host=127.0.0.1;port=3306;dbname=snappymail"
pdo_dsn = "host=127.0.0.1;port=3306;dbname=snappymail"
pdo_user = "root"
pdo_password = ""
suggestions_limit = 30
@ -67,23 +56,23 @@ suggestions_limit = 30
; Enable CSRF protection (http://en.wikipedia.org/wiki/Cross-site_request_forgery)
csrf_protection = On
custom_server_signature = "SnappyMail"
x_frame_options_header = ""
x_frame_options_header = "DENY"
x_xss_protection_header = "1; mode=block"
openpgp = Off
; Login and password for web admin panel
admin_login = "admin"
admin_password = "12345"
admin_password = ""
admin_totp = ""
; Access settings
allow_admin_panel = On
allow_two_factor_auth = Off
force_two_factor_auth = Off
hide_x_mailer_header = Off
hide_x_mailer_header = On
admin_panel_host = ""
admin_panel_key = "admin"
content_security_policy = ""
core_install_access_domain = ""
csp_report = Off
encrypt_cipher = "aes-256-cbc-hmac-sha1"
[ssl]
; Require verification of SSL certificate used.
@ -102,20 +91,12 @@ capath = ""
client_cert = ""
[capa]
folders = On
composer = On
contacts = On
settings = On
quota = On
help = On
reload = On
search = On
search_adv = On
filters = On
x-templates = Off
dangerous_actions = On
message_actions = On
messagelist_actions = On
attachments_actions = On
[login]
@ -125,10 +106,7 @@ default_domain = ""
allow_languages_on_login = On
determine_user_language = On
determine_user_domain = Off
welcome_page = Off
hide_submit_button = On
forgot_password_link_url = ""
registration_link_url = ""
login_lowercase = On
; This option allows webmail to remember the logged in user
@ -155,6 +133,7 @@ view_editor_type = "Html"
view_layout = 1
view_use_checkboxes = On
autologout = 30
view_html = On
show_images = Off
contacts_autosave = On
mail_use_threads = Off
@ -165,6 +144,17 @@ mail_reply_same_folder = Off
; Enable logging
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)
write_on_error_only = Off
@ -177,9 +167,8 @@ write_on_timeout_only = 0
; Required for development purposes only.
; Disabling this option is not recommended.
hide_passwords = On
time_offset = "0"
time_zone = "UTC"
session_filter = ""
sentry_dsn = ""
; Log filename.
; For security reasons, some characters are removed from filename.
@ -207,6 +196,7 @@ sentry_dsn = ""
; filename = "log-{date:Y-m-d}.txt"
; filename = "{date:Y-m-d}/{user:domain}/{user:email}_{user:uid}.log"
; filename = "{user:email}-{date:Y-m-d}.txt"
; filename = "syslog"
filename = "log-{date:Y-m-d}.txt"
; Enable auth logging in a separate file (for fail2ban)
@ -214,35 +204,13 @@ auth_logging = Off
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}"
; Enable auth logging to syslog for fail2ban
auth_syslog = On
[debug]
; Special option required for development purposes
enable = Off
[social]
; Google
google_enable = Off
google_enable_auth = Off
google_enable_auth_gmail = Off
google_enable_drive = Off
google_enable_preview = Off
google_client_id = ""
google_client_secret = ""
google_api_key = ""
; Facebook
fb_enable = Off
fb_app_id = ""
fb_app_secret = ""
; Twitter
twitter_enable = Off
twitter_consumer_key = ""
twitter_consumer_secret = ""
; Dropbox
dropbox_enable = Off
dropbox_api_key = ""
[cache]
; The section controls caching of the entire application.
;
@ -252,7 +220,7 @@ enable = On
; Additional caching key. If changed, cache is purged
index = "v1"
; Can be: files, APC, memcache, redis (beta)
; Can be: files, APCU, memcache, redis (beta)
fast_cache_driver = "files"
; Additional caching key. If changed, fast cache is purged
@ -268,33 +236,21 @@ http_expires = 3600
server_uids = On
[labs]
; Experimental settings. Handle with care.
;
allow_mobile_version = On
ignore_folders_subscription = Off
check_new_password_strength = On
update_channel = "stable"
allow_gravatar = On
allow_prefetch = On
allow_smart_html_links = On
allow_prefetch = Off
cache_system_data = On
date_from_headers = On
autocreate_system_folders = On
autocreate_system_folders = Off
allow_message_append = Off
disable_iconv_if_mbstring_supported = Off
login_fault_delay = 1
log_ajax_response_write_limit = 300
allow_html_editor_source_button = Off
allow_html_editor_biti_buttons = Off
allow_ctrl_enter_on_compose = On
try_to_detect_hidden_images = Off
hide_dangerous_actions = Off
use_app_debug_js = Off
use_mobile_version_for_tablets = Off
use_app_debug_css = Off
use_imap_sort = On
use_imap_force_selection = Off
use_imap_list_subscribe = On
use_imap_thread = On
use_imap_move = Off
use_imap_expunge_all_on_delete = Off
@ -309,51 +265,41 @@ imap_message_all_headers = Off
imap_large_thread_limit = 50
imap_folder_list_limit = 200
imap_show_login_alert = On
imap_use_auth_plain = On
imap_use_auth_cram_md5 = Off
imap_use_list_status = On
imap_timeout = 300
smtp_show_server_errors = Off
smtp_use_auth_plain = On
smtp_use_auth_cram_md5 = Off
sieve_allow_raw_script = Off
sieve_utf8_folder_name = On
smtp_timeout = 60
sieve_auth_plain_initial = On
sieve_allow_fileinto_inbox = Off
imap_timeout = 300
smtp_timeout = 60
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_additional_parameters = Off
favicon_status = On
folders_spec_limit = 50
owncloud_save_folder = "Attachments"
owncloud_suggestions = On
curl_proxy = ""
curl_proxy_auth = ""
in_iframe = Off
force_https = Off
custom_login_link = ""
custom_logout_link = ""
allow_external_login = Off
allow_external_sso = Off
external_sso_key = ""
http_client_ip_check_proxy = Off
fast_cache_memcache_host = "127.0.0.1"
fast_cache_memcache_port = 11211
fast_cache_redis_host = "127.0.0.1"
fast_cache_redis_port = 6379
use_local_proxy_for_external_images = Off
use_local_proxy_for_external_images = On
detect_image_exif_orientation = On
cookie_default_path = ""
cookie_default_secure = Off
check_new_messages = On
replace_env_in_configuration = ""
startup_url = ""
strict_html_parser = Off
allow_cmd = Off
boundary_prefix = ""
kolab_enabled = Off
dev_email = ""
dev_password = ""
[version]
current = "1.14.0"
saved = "Wed, 08 Apr 2020 16:37:27 +0000"
current = "2.13.4"
saved = "Fri, 04 Mar 2022 08:55:26 +0000"

View file

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

1
.github/FUNDING.yml vendored
View file

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

8
.gitignore vendored
View file

@ -18,3 +18,11 @@
/include.php
.idea/
.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

@ -1,6 +1,8 @@
RewriteEngine On
# Redirect cPanel
RewriteRule cpsess.* https://%{HTTP_HOST}/ [L,R=301]
<IfModule mod_rewrite.c>
RewriteEngine On
# Redirect cPanel
RewriteRule cpsess.* https://%{HTTP_HOST}/ [L,R=301]
</IfModule>
<IfModule mod_expires.c>
ExpiresActive On
@ -18,51 +20,18 @@ RewriteRule cpsess.* https://%{HTTP_HOST}/ [L,R=301]
</IfModule>
<IfModule mod_headers.c>
Header set Cache-Control "public, max-age=31536000"
Header set Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; img-src 'self' data:; style-src 'self' 'unsafe-inline'"
Header set Referrer-Policy "no-referrer"
# Header set Cache-Control "public, max-age=31536000"
# Header set Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; img-src 'self' data:; style-src 'self' 'unsafe-inline'"
# Header set Referrer-Policy "no-referrer"
Header set Strict-Transport-Security "max-age=31536000"
Header set imagetoolbar "no"
Header set X-Content-Type-Options "nosniff"
Header set X-Frame-Options "DENY"
Header set X-XSS-Protection "1; mode=block"
# Header set X-Content-Type-Options "nosniff"
# Header set X-Frame-Options "DENY"
# Header set X-XSS-Protection "1; mode=block"
Header set Service-Worker-Allowed "/"
RewriteCond %{HTTP:Accept-encoding} br
RewriteCond "%{REQUEST_FILENAME}\.br" -s
RewriteRule "^(.+)" "$1\.br" [L,T=text/javascript,QSA]
RewriteCond %{HTTP:Accept-encoding} gzip
RewriteCond "%{REQUEST_FILENAME}\.gz" -s
RewriteRule "^(.+)" "$1\.gz" [L,T=text/javascript,QSA]
RewriteCond %{HTTP:Accept-encoding} br
RewriteCond "%{REQUEST_FILENAME}\.br" -s
RewriteRule "^(.+)" "$1\.br" [L,T=text/css,QSA]
RewriteCond %{HTTP:Accept-encoding} gzip
RewriteCond "%{REQUEST_FILENAME}\.gz" -s
RewriteRule "^(.+)" "$1\.gz" [L,T=text/css,QSA]
<FilesMatch "(\.js\.br|\.css\.br)$">
SetEnv no-gzip 1
SetEnv no-brotli 1
# Serve correct encoding type.
Header append Content-Encoding br
# Force proxies to cache brotli &
# non-brotli css/js files separately.
Header append Vary Accept-Encoding
</FilesMatch>
<FilesMatch "(\.js\.gz|\.css\.gz)$">
SetEnv no-gzip 1
SetEnv no-brotli 1
# Serve correct encoding type.
Header append Content-Encoding gzip
# Force proxies to cache gzipped &
# non-gzipped css/js files separately.
Header append Vary Accept-Encoding
</FilesMatch>
# Google FLoC
# Header set Permissions-Policy "interest-cohort=()"
</IfModule>
#<IfModule mod_brotli.c>

View file

@ -1,15 +0,0 @@
[main]
host = https://www.transifex.com
minimum_perc = 60
type = YAML
[snappymail-webmail.snappymail-webmail]
file_filter = snappymail/v/0.0.0/app/localization/webmail/<lang>.yml
source_file = snappymail/v/0.0.0/app/localization/webmail/_source.en.yml
source_lang = en
[snappymail-webmail.snappymail-admin]
file_filter = snappymail/v/0.0.0/app/localization/admin/<lang>.yml
source_file = snappymail/v/0.0.0/app/localization/admin/_source.en.yml
source_lang = en

View file

@ -7,14 +7,15 @@
**Getting started**
1. Install node.js - `https://nodejs.org/download/`
2. Install yarn - `https://yarnpkg.com/en/docs/install`
3. Install gulp - `npm install gulp -g`
4. Fork snappymail - `https://github.com/the-djmaze/snappymail/issues/new#fork-destination-box`
5. Clone snappymail - `git clone git@github.com:USERNAME/snappymail.git snappymail`
6. `cd snappymail`
7. Install install all dependencies - `yarn install`
8. Run gulp - `gulp`
1. Install PHP
2. Install node.js - `https://nodejs.org/download/`
3. Install yarn - `https://yarnpkg.com/en/docs/install`
4. Install gulp - `npm install gulp -g`
5. Fork snappymail from https://github.com/the-djmaze/snappymail
6. Clone snappymail - `git clone git@github.com:USERNAME/snappymail.git snappymail`
7. `cd snappymail`
8. Install all dependencies - `yarn install`
9. Run gulp - `gulp`
---
@ -30,6 +31,17 @@
1. Edit data/\_data_/\_default_/configs/application.ini
2. Set 'cache_system_data' to Off
**Release**
1. Install gzip
2. Install brotli
3. php release.php
Options:
* `php release.php --aur` = Build Arch Linux package
* `php release.php --docker` = Build Docker instance
* `php release.php --plugins` = Build plugins
---
If you have any questions, open an issue or email support@snappymail.eu.

View file

@ -1,75 +1,75 @@
#!make
rebuild: _down
docker-compose build --no-cache
docker compose build --no-cache
up: _up status
_up:
docker-compose up -d
docker compose up -d
stop: _stop status
_stop:
docker-compose stop
docker compose stop
down: _down status
_down:
docker-compose down
docker compose down
restart: _stop _up status
status:
@docker-compose ps
@docker compose ps
tx:
@docker-compose run --no-deps --rm tx tx pull -a -s -f -d
@docker compose run --no-deps --rm tx tx pull -a -s -f -d
console-node:
@docker-compose run --no-deps --rm node sh
@docker compose run --no-deps --rm node sh
console-tx:
@docker-compose run --no-deps --rm tx sh
@docker compose run --no-deps --rm tx sh
console-php:
@docker-compose exec php sh
@docker compose exec php sh
console: console-node
logs:
@docker-compose logs --tail=100 -f
@docker compose logs --tail=100 -f
logs-db:
@docker-compose logs --tail=100 -f db
@docker compose logs --tail=100 -f db
logs-php:
@docker-compose logs --tail=100 -f php
@docker compose logs --tail=100 -f php
logs-node:
@docker-compose logs --tail=100 -f node
@docker compose logs --tail=100 -f node
logs-nginx:
@docker-compose logs --tail=100 -f nginx
@docker compose logs --tail=100 -f nginx
logs-mail:
@docker-compose logs --tail=100 -f mail
@docker compose logs --tail=100 -f mail
logs-tx:
@docker-compose logs --tail=100 -f tx
@docker compose logs --tail=100 -f tx
rl-lint:
@docker-compose run --no-deps --rm node gulp lint
@docker compose run --no-deps --rm node gulp lint
rl-dev:
@docker-compose run --no-deps --rm node npm run watch-js
@docker compose run --no-deps --rm node npm run watch-js
rl-compile:
@docker-compose run --no-deps --rm node gulp build
@docker compose run --no-deps --rm node gulp build
rl-compile-with-source:
@docker-compose run --no-deps --rm node gulp build --source
@docker compose run --no-deps --rm node gulp build --source
rl-watch-css:
@docker-compose run --no-deps --rm node npm run watch-css
@docker compose run --no-deps --rm node npm run watch-css
rl-watch-js:
@docker-compose run --no-deps --rm node npm run watch-js
@docker compose run --no-deps --rm node npm run watch-js
rl-build:
@docker-compose run --no-deps --rm node gulp all
@docker compose run --no-deps --rm node gulp all
rl-build-pro:
@docker-compose run --no-deps --rm node gulp all --pro
@docker compose run --no-deps --rm node gulp all --pro
yarn-install:
@docker-compose run --no-deps --rm node yarn install
@docker compose run --no-deps --rm node yarn install
yarn-outdated:
@docker-compose run --no-deps --rm node yarn outdated
@docker compose run --no-deps --rm node yarn outdated
yarn-upgrade:
@docker-compose run --no-deps --rm node yarn upgrade-interactive --exact --latest
@docker compose run --no-deps --rm node yarn upgrade-interactive --exact --latest
gpg:
docker run -it --rm -w=/var/www \

118
README.md
View file

@ -1,6 +1,6 @@
<div align="center">
<a href="https://github.com/the-djmaze/snappymail">
<img width="200" heigth="200" src="https://snappymail.eu/static/img/logo-256x256.png">
<img src="https://snappymail.eu/static/img/logo-256x256-white.png">
</a>
<br>
<h1>SnappyMail</h1>
@ -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)**.
http://www.gnu.org/licenses/agpl-3.0.html
Copyright (c) 2020 - 2021 SnappyMail
Copyright (c) 2020 - 2022 SnappyMail
Copyright (c) 2013 - 2021 RainLoop
## Modifications
@ -45,24 +45,25 @@ This fork of RainLoop has the following changes:
* Admin uses password_hash/password_verify
* Auth failed attempts written to syslog
* Added Fail2ban instructions
* ES2015
* ES2018
* PHP 7.3+ required
* PHP mbstring extension required
* 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"
* Removed BackwardCapability (class \RainLoop\Account)
* Removed ChangePassword (re-implemented as plugin)
* Removed OAuth support
* Removed POP3 support
* Removed background video support
* Removed Sentry (Application Monitoring and Error Tracking Software)
* Removed Spyc yaml
* Replaced gulp-uglify with gulp-terser
* CRLF => LF line endings
* Embed boot.js and boot.css into index.html
* 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
* Split Admin specific JavaScript code from User code
* Split Sieve specific JavaScript code from User code
* JSON reviver
* Better memory garbage collection management
* Added serviceworker for Notifications
@ -71,27 +72,49 @@ This fork of RainLoop has the following changes:
* Replaced webpack with rollup
* No user-agent detection (use device width)
* Added support to load plugins as .phar
* Replaced old Sabre library
* AddressBook Contacts support MySQL/MariaDB utf8mb4
* Prevent Google FLoC
* Added [Fetch Metadata Request Headers](https://www.w3.org/TR/fetch-metadata/) checks
* 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
This fork uses downsized/simplified versions of scripts and has no support for Internet Explorer nor Edge Legacy.
Supported are:
* Chrome 69+
* Edge 79+
* Firefox 69+
* Opera 56+
* Safari 12+
### Removal of old JavaScript
This fork uses downsized/simplified versions of scripts and has no support for Internet Explorer.
The result is faster and smaller download code (good for mobile networks).
Things might work in Edge 18, Firefox 50-62 and Chrome 54-68 due to one polyfill for array.flat().
* Added dev/prototype.js for some additional features
* boot.js without webpack overhead
* Modified Jua.js to be without jQuery
* Replaced ProgressJS with simple native dropin
* Replaced Autolinker with simple https/email detection
* 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 bootstrap.js with native drop-in replacement
* Replaced dev/Common/ClientStorageDriver/* with Web Storage Objects polyfill
* Replaced *Ajax with *Fetch classes because we use the Fetch API, not jQuery.ajax
* Replaced knockoutjs 3.4 with a modified 3.5.1
* Replaced [knockoutjs](https://github.com/knockout/knockout) 3.4 with a modified 3.5.1
* Replaced knockout-sortable with native HTML5 drag&drop
* Replaced simplestatemanager with CSS @media
* Replaced inputosaurus with own code
@ -110,31 +133,35 @@ Things might work in Edge 18, Firefox 50-62 and Chrome 54-68 due to one polyfill
* Removed momentjs (use Intl)
* Removed opentip (use CSS)
* Removed non-community (aka Prem/Premium/License) code
* Removed ProgressJS
RainLoop 1.15 vs SnappyMail
|js/* |RainLoop |Snappy |
|--------------- |--------: |--------: |
|admin.js |2.158.025 | 94.549 |
|app.js |4.215.733 | 458.762 |
|boot.js | 672.433 | 4.554 |
|libs.js | 647.679 | 218.511 |
|admin.js |2.158.025 | 79.018 |
|app.js |4.215.733 | 407.697 |
|boot.js | 672.433 | 2.025 |
|libs.js | 647.679 | 200.131 |
|sieve.js | 0 | 75.642 |
|polyfills.js | 325.908 | 0 |
|serviceworker.js | 0 | 285 |
|TOTAL |8.019.778 | 776.661 |
|TOTAL |8.019.778 | 764.798 |
|js/min/* |RainLoop |Snappy |RL gzip |SM gzip |RL brotli |SM brotli |
|--------------- |--------: |--------: |------: |------: |--------: |--------: |
|admin.min.js | 255.514 | 49.871 | 73.899 | 14.771 | 60.674 | 13.213 |
|app.min.js | 516.000 | 235.474 |140.430 | 69.223 |110.657 | 58.398 |
|boot.min.js | 66.456 | 2.442 | 22.553 | 1.371 | 20.043 | 1.178 |
|libs.min.js | 574.626 | 106.574 |177.280 | 38.600 |151.855 | 34.532 |
|polyfills.min.js | 32.608 | 0 | 11.315 | 0 | 10.072 | 0 |
|TOTAL |1.445.204 | 394.361 |425.477 |123.965 |353.301 |107.321 |
|TOTAL (no admin) |1.189.690 | 344.490 |351.061 |109.194 |292.627 | 94.108 |
|admin.min.js | 255.514 | 39.256 | 73.899 | 13.076 | 60.674 | 11.702 |
|app.min.js | 516.000 | 194.148 |140.430 | 62.297 |110.657 | 53.432 |
|boot.min.js | 66.456 | 1.252 | 22.553 | 782 | 20.043 | 631 |
|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 |
|TOTAL user |1.189.690 | 291.601 |351.061 | 98.601 |292.627 | 85.809 |
|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 66% smaller and faster than traditional RainLoop.
For a user its around 70% smaller and faster than traditional RainLoop.
### CSS changes
@ -142,7 +169,7 @@ For a user its around 66% smaller and faster than traditional RainLoop.
* Themes work in mobile mode
* Bugfix invalid/conflicting css rules
* Use flexbox
* Split app.css to have seperate admin.css
* Split app.css to have separate admin.css
* Remove oldschool 'float'
* Remove unused css
* Removed html.no-css
@ -151,13 +178,7 @@ For a user its around 66% smaller and faster than traditional RainLoop.
* Removed Internet Explorer from normalize.css
* Removed node_modules/opentip/css/opentip.css
* Removed node_modules/pikaday/css/pikaday.css
* Removed vendors/bootstrap/less/breadcrumbs.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 unused vendors/bootstrap/less/*
* Removed vendors/jquery-nanoscroller/nanoscroller.css
* Removed vendors/jquery-letterfx/jquery-letterfx.min.css
* Removed vendors/Progress.js/minified/progressjs.min.css
@ -165,13 +186,28 @@ For a user its around 66% smaller and faster than traditional RainLoop.
|css/* |RainLoop |Snappy |RL gzip |SM gzip |SM brotli |
|------------ |-------: |-------: |------: |------: |--------: |
|app.css | 340.334 | 106.864 | 46,959 | 18.751 | 16.110 |
|app.min.css | 274.791 | 88.077 | 39.618 | 16.788 | 14.787 |
|boot.css | | 2.066 | | 913 | 742 |
|boot.min.css | | 1.696 | | 818 | 664 |
|admin.css | | 46.337 | | 9.221 | 8.058 |
|admin.min.css | | 37.251 | | 8.178 | 7.269 |
|------------ |-------: |------: |------: |------: |--------: |
|app.css | 340.334 | 80.865 | 46.959 | 16.751 | 14.420 |
|app.min.css | 274.791 | 65.086 | 39.618 | 14.855 | 13.088 |
|boot.css | | 1.326 | | 664 | 545 |
|boot.min.css | | 1.071 | | 590 | 474 |
|admin.css | | 29.977 | | 6.795 | 5.900 |
|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
@ -184,7 +220,7 @@ Still TODO:
| | normal | min | gzip | min gzip |
|-------- |-------: |-------: |------: |--------: |
|squire | 128.826 | 47.074 | 33.671 | 15.596 |
|squire | 122.321 | 41.906 | 31.867 | 14.330 |
|ckeditor | ? | 520.035 | ? | 155.916 |
CKEditor including the 7 asset requests (css,language,plugins,icons) is 633.46 KB / 180.47 KB (gzip).

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

50
assets/.htaccess Normal file
View file

@ -0,0 +1,50 @@
<IfModule mod_headers.c>
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteCond %{HTTP:Accept-encoding} br
RewriteCond "%{REQUEST_FILENAME}\.br" -s
RewriteRule "^(.+\.js)$" "$1\.br" [L,T=application/javascript,QSA]
RewriteCond %{HTTP:Accept-encoding} gzip
RewriteCond "%{REQUEST_FILENAME}\.gz" -s
RewriteRule "^(.+\.js)$" "$1\.gz" [L,T=application/javascript,QSA]
RewriteCond %{HTTP:Accept-encoding} br
RewriteCond "%{REQUEST_FILENAME}\.br" -s
RewriteRule "^(.+\.css)$" "$1\.br" [L,T=text/css,QSA]
RewriteCond %{HTTP:Accept-encoding} gzip
RewriteCond "%{REQUEST_FILENAME}\.gz" -s
RewriteRule "^(.+\.css)$" "$1\.gz" [L,T=text/css,QSA]
<FilesMatch "(\.js\.br|\.css\.br)$">
SetEnv no-gzip 1
SetEnv no-brotli 1
# Serve correct encoding type.
Header append Content-Encoding br
# Force proxies to cache brotli &
# non-brotli css/js files separately.
Header append Vary Accept-Encoding
</FilesMatch>
<FilesMatch "(\.js\.gz|\.css\.gz)$">
SetEnv no-gzip 1
SetEnv no-brotli 1
# Serve correct encoding type.
Header append Content-Encoding gzip
# Force proxies to cache gzipped &
# non-gzipped css/js files separately.
Header append Vary Accept-Encoding
</FilesMatch>
<FilesMatch "(\.js\.br|\.js\.gz)$">
Header set Content-Type "application/javascript; charset=utf-8"
ForceType application/javascript
</FilesMatch>
<FilesMatch "(\.css\.br|\.css\.gz)$">
Header set Content-Type "text/css; charset=utf-8"
ForceType text/css
</FilesMatch>
</IfModule>
</IfModule>

BIN
assets/sounds/alert.mp3 Normal file

Binary file not shown.

BIN
assets/sounds/alert.ogg Normal file

Binary file not shown.

BIN
assets/sounds/ping.mp3 Normal file

Binary file not shown.

BIN
assets/sounds/ping.ogg Normal file

Binary file not shown.

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

View file

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

File diff suppressed because it is too large Load diff

View file

@ -1,5 +1,5 @@
import * as Links from 'Common/Links';
import { doc } from 'Common/Globals';
import { doc, SettingsGet, fireEvent, addEventsListener } from 'Common/Globals';
let notificator = null,
player = null,
@ -12,7 +12,7 @@ let notificator = null,
player.src = url;
player.play();
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"');
if (player) {
const stopFn = () => this.pause();
player.addEventListener('ended', stopFn);
player.addEventListener('error', stopFn);
addEventsListener(player, ['ended','error'], stopFn);
addEventListener('audio.api.stop', stopFn);
}
}
@ -89,7 +88,7 @@ export const SMAudio = new class {
pause() {
player && player.pause();
dispatchEvent(new CustomEvent('audio.stop'));
fireEvent('audio.stop');
}
playMp3(url, name) {
@ -106,11 +105,11 @@ export const SMAudio = new class {
playNotification(silent) {
if ('running' == audioCtx.state && (this.supportedMp3 || this.supportedOgg)) {
if (!notificator) {
notificator = createNewObject();
notificator.src = Links.staticLink('sounds/new-mail.'+ (this.supportedMp3 ? 'mp3' : 'ogg'));
}
notificator = notificator || createNewObject();
if (notificator) {
notificator.src = Links.staticLink('sounds/'
+ SettingsGet('NotificationSound')
+ (this.supportedMp3 ? '.mp3' : '.ogg'));
notificator.volume = silent ? 0.01 : 1;
notificator.play();
}

View file

@ -1,174 +1,157 @@
import { MessageSetAction } from 'Common/EnumsUser';
import { isNonEmptyArray, pInt } from 'Common/Utils';
import { isArray } from 'Common/Utils';
let FOLDERS_CACHE = {},
FOLDERS_NAME_CACHE = {},
FOLDERS_HASH_CACHE = {},
FOLDERS_UID_NEXT_CACHE = {},
MESSAGE_FLAGS_CACHE = {},
NEW_MESSAGE_CACHE = {},
REQUESTED_MESSAGE_CACHE = {},
inboxFolderName = 'INBOX';
const REQUESTED_MESSAGE_CACHE = {};
export const
/**
* @returns {void}
*/
clearCache = () => {
FOLDERS_CACHE = {};
FOLDERS_NAME_CACHE = {};
MESSAGE_FLAGS_CACHE = {};
NEW_MESSAGE_CACHE = {};
REQUESTED_MESSAGE_CACHE = {};
},
/**
* @returns {void}
*/
export function clear() {
FOLDERS_CACHE = {};
FOLDERS_NAME_CACHE = {};
FOLDERS_HASH_CACHE = {};
FOLDERS_UID_NEXT_CACHE = {};
MESSAGE_FLAGS_CACHE = {};
}
/**
* @param {string} folderFullName
* @param {string} uid
* @returns {string}
*/
getMessageKey = (folderFullName, uid) => `${folderFullName}#${uid}`,
/**
* @param {string} folderFullNameRaw
* @param {string} uid
* @returns {string}
*/
export function getMessageKey(folderFullNameRaw, uid) {
return `${folderFullNameRaw}#${uid}`;
}
/**
* @param {string} folder
* @param {string} uid
*/
addRequestedMessage = (folder, uid) => REQUESTED_MESSAGE_CACHE[getMessageKey(folder, uid)] = true,
/**
* @param {string} folder
* @param {string} uid
*/
export function addRequestedMessage(folder, uid) {
REQUESTED_MESSAGE_CACHE[getMessageKey(folder, uid)] = true;
}
/**
* @param {string} folder
* @param {string} uid
* @returns {boolean}
*/
hasRequestedMessage = (folder, uid) => true === REQUESTED_MESSAGE_CACHE[getMessageKey(folder, uid)],
/**
* @param {string} folder
* @param {string} uid
* @returns {boolean}
*/
export function hasRequestedMessage(folder, uid) {
return true === REQUESTED_MESSAGE_CACHE[getMessageKey(folder, uid)];
}
/**
* @param {string} folderFullName
* @param {string} uid
*/
addNewMessageCache = (folderFullName, uid) => NEW_MESSAGE_CACHE[getMessageKey(folderFullName, uid)] = true,
/**
* @param {string} folderFullNameRaw
* @param {string} uid
*/
export function addNewMessageCache(folderFullNameRaw, uid) {
NEW_MESSAGE_CACHE[getMessageKey(folderFullNameRaw, uid)] = true;
}
/**
* @param {string} folderFullName
* @param {string} uid
*/
hasNewMessageAndRemoveFromCache = (folderFullName, uid) => {
if (NEW_MESSAGE_CACHE[getMessageKey(folderFullName, uid)]) {
NEW_MESSAGE_CACHE[getMessageKey(folderFullName, uid)] = null;
return true;
}
return false;
},
/**
* @param {string} folderFullNameRaw
* @param {string} uid
*/
export function hasNewMessageAndRemoveFromCache(folderFullNameRaw, uid) {
if (NEW_MESSAGE_CACHE[getMessageKey(folderFullNameRaw, uid)]) {
NEW_MESSAGE_CACHE[getMessageKey(folderFullNameRaw, uid)] = null;
return true;
}
return false;
}
/**
* @returns {void}
*/
clearNewMessageCache = () => NEW_MESSAGE_CACHE = {},
/**
* @returns {void}
*/
export function clearNewMessageCache() {
NEW_MESSAGE_CACHE = {};
}
/**
* @returns {string}
*/
getFolderInboxName = () => inboxFolderName,
/**
* @returns {string}
*/
export function getFolderInboxName() {
return inboxFolderName;
}
/**
* @returns {string}
*/
setFolderInboxName = name => inboxFolderName = name,
/**
* @returns {string}
*/
export function setFolderInboxName(name) {
inboxFolderName = name;
}
/**
* @param {string} folderHash
* @returns {string}
*/
getFolderFullName = folderHash =>
folderHash && FOLDERS_NAME_CACHE[folderHash] ? FOLDERS_NAME_CACHE[folderHash] : '',
/**
* @param {string} folderHash
* @returns {string}
*/
export function getFolderFullNameRaw(folderHash) {
return folderHash && FOLDERS_NAME_CACHE[folderHash] ? FOLDERS_NAME_CACHE[folderHash] : '';
}
/**
* @param {string} folderHash
* @param {string} folderFullName
* @param {?FolderModel} folder
*/
setFolder = folder => {
folder.hash = '';
FOLDERS_CACHE[folder.fullName] = folder;
FOLDERS_NAME_CACHE[folder.fullNameHash] = folder.fullName;
},
/**
* @param {string} folderHash
* @param {string} folderFullNameRaw
* @param {?FolderModel} folder
*/
export function setFolder(folderHash, folderFullNameRaw, folder) {
FOLDERS_CACHE[folderFullNameRaw] = folder;
FOLDERS_NAME_CACHE[folderHash] = folderFullNameRaw;
}
/**
* @param {string} folderFullName
* @returns {string}
*/
getFolderHash = folderFullName =>
FOLDERS_CACHE[folderFullName] ? FOLDERS_CACHE[folderFullName].hash : '',
/**
* @param {string} folderFullNameRaw
* @returns {string}
*/
export function getFolderHash(folderFullNameRaw) {
return folderFullNameRaw && FOLDERS_HASH_CACHE[folderFullNameRaw] ? FOLDERS_HASH_CACHE[folderFullNameRaw] : '';
}
/**
* @param {string} folderFullName
* @param {string} folderHash
*/
setFolderHash = (folderFullName, folderHash) =>
FOLDERS_CACHE[folderFullName] && (FOLDERS_CACHE[folderFullName].hash = folderHash),
/**
* @param {string} folderFullNameRaw
* @param {string} folderHash
*/
export function setFolderHash(folderFullNameRaw, folderHash) {
if (folderFullNameRaw) {
FOLDERS_HASH_CACHE[folderFullNameRaw] = folderHash;
}
}
/**
* @param {string} folderFullName
* @returns {string}
*/
getFolderUidNext = folderFullName =>
FOLDERS_CACHE[folderFullName] ? FOLDERS_CACHE[folderFullName].uidNext : 0,
/**
* @param {string} folderFullNameRaw
* @returns {string}
*/
export function getFolderUidNext(folderFullNameRaw) {
return folderFullNameRaw && FOLDERS_UID_NEXT_CACHE[folderFullNameRaw]
? FOLDERS_UID_NEXT_CACHE[folderFullNameRaw]
: '';
}
/**
* @param {string} folderFullName
* @param {string} uidNext
*/
setFolderUidNext = (folderFullName, uidNext) =>
FOLDERS_CACHE[folderFullName] && (FOLDERS_CACHE[folderFullName].uidNext = uidNext),
/**
* @param {string} folderFullNameRaw
* @param {string} uidNext
*/
export function setFolderUidNext(folderFullNameRaw, uidNext) {
FOLDERS_UID_NEXT_CACHE[folderFullNameRaw] = uidNext;
}
/**
* @param {string} folderFullName
* @returns {?FolderModel}
*/
getFolderFromCacheList = folderFullName =>
FOLDERS_CACHE[folderFullName] ? FOLDERS_CACHE[folderFullName] : null,
/**
* @param {string} folderFullNameRaw
* @returns {?FolderModel}
*/
export function getFolderFromCacheList(folderFullNameRaw) {
return folderFullNameRaw && FOLDERS_CACHE[folderFullNameRaw] ? FOLDERS_CACHE[folderFullNameRaw] : null;
}
/**
* @param {string} folderFullNameRaw
*/
export function removeFolderFromCacheList(folderFullNameRaw) {
delete FOLDERS_CACHE[folderFullNameRaw];
}
/**
* @param {string} folderFullName
*/
removeFolderFromCacheList = folderFullName => delete FOLDERS_CACHE[folderFullName];
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} uid
* @returns {?Array}
*/
static getFor(folderFullName, uid) {
return MESSAGE_FLAGS_CACHE[folderFullName] && MESSAGE_FLAGS_CACHE[folderFullName][uid]
? MESSAGE_FLAGS_CACHE[folderFullName][uid]
: null;
return MESSAGE_FLAGS_CACHE[folderFullName] && MESSAGE_FLAGS_CACHE[folderFullName][uid];
}
/**
@ -176,11 +159,13 @@ export class MessageFlagsCache
* @param {string} uid
* @param {Array} flagsCache
*/
static setFor(folderFullName, uid, flagsCache) {
if (!MESSAGE_FLAGS_CACHE[folderFullName]) {
MESSAGE_FLAGS_CACHE[folderFullName] = {};
static setFor(folderFullName, uid, flags) {
if (isArray(flags)) {
if (!MESSAGE_FLAGS_CACHE[folderFullName]) {
MESSAGE_FLAGS_CACHE[folderFullName] = {};
}
MESSAGE_FLAGS_CACHE[folderFullName][uid] = flags;
}
MESSAGE_FLAGS_CACHE[folderFullName][uid] = flagsCache;
}
/**
@ -196,39 +181,24 @@ export class MessageFlagsCache
static initMessage(message) {
if (message) {
const uid = message.uid,
flags = this.getFor(message.folder, uid);
flags = this.getFor(message.folder, uid),
thread = message.threads;
if (flags && flags.length) {
message.isFlagged(!!flags[1]);
if (!message.isSimpleMessage) {
message.isUnseen(!!flags[0]);
message.isAnswered(!!flags[2]);
message.isForwarded(!!flags[3]);
message.isReadReceipt(!!flags[4]);
message.isDeleted(!!flags[5]);
}
if (isArray(flags)) {
message.flags(flags);
}
if (message.threads.length) {
const unseenSubUid = message.threads.find(sSubUid => {
if (uid !== sSubUid) {
const subFlags = this.getFor(message.folder, sSubUid);
return subFlags && subFlags.length && !!subFlags[0];
}
return false;
});
if (thread.length) {
const unseenSubUid = thread.find(iSubUid =>
(uid !== iSubUid) && !this.hasFlag(message.folder, iSubUid, '\\seen')
);
const flaggedSubUid = message.threads.find(sSubUid => {
if (uid !== sSubUid) {
const subFlags = this.getFor(message.folder, sSubUid);
return subFlags && subFlags.length && !!subFlags[1];
}
return false;
});
const flaggedSubUid = thread.find(iSubUid =>
(uid !== iSubUid) && this.hasFlag(message.folder, iSubUid, '\\flagged')
);
message.hasUnseenSubMessage(unseenSubUid && 0 < pInt(unseenSubUid));
message.hasFlaggedSubMessage(flaggedSubUid && 0 < pInt(flaggedSubUid));
message.hasUnseenSubMessage(!!unseenSubUid);
message.hasFlaggedSubMessage(!!flaggedSubUid);
}
}
}
@ -238,25 +208,7 @@ export class MessageFlagsCache
*/
static store(message) {
if (message) {
this.setFor(message.folder, message.uid, [
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 (isNonEmptyArray(flags)) {
this.setFor(folder, uid, flags);
this.setFor(message.folder, message.uid, message.flags());
}
}
@ -266,33 +218,30 @@ export class MessageFlagsCache
* @param {number} setAction
*/
static storeBySetAction(folder, uid, setAction) {
let unread = 0;
const flags = this.getFor(folder, uid);
let 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 (isNonEmptyArray(flags)) {
if (flags[0]) {
unread = 1;
}
switch (setAction) {
case MessageSetAction.SetSeen:
flags[0] = false;
break;
case MessageSetAction.UnsetSeen:
flags[0] = true;
break;
case MessageSetAction.SetFlag:
flags[1] = true;
break;
case MessageSetAction.UnsetFlag:
flags[1] = false;
break;
// no default
}
this.setFor(folder, uid, flags);
switch (setAction) {
case MessageSetAction.SetSeen:
add('\\seen');
break;
case MessageSetAction.UnsetSeen:
remove('\\seen');
break;
case MessageSetAction.SetFlag:
add('\\flagged');
break;
case MessageSetAction.UnsetFlag:
remove('\\flagged');
break;
// no default
}
this.setFor(folder, uid, flags);
return unread;
}

View file

@ -1,3 +1 @@
export const MESSAGES_PER_PAGE_VALUES = [10, 20, 30, 50, 100];
export const UNUSED_OPTION_VALUE = '__UNUSE__';

View file

@ -1,85 +1,53 @@
/* eslint quote-props: 0 */
/**
* @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'
};
export const
/**
* @enum {string}
*/
export const Scope = {
All: 'all',
None: 'none',
Contacts: 'Contacts',
Scope = {
MessageList: 'MessageList',
FolderList: 'FolderList',
MessageView: 'MessageView',
Compose: 'Compose',
Settings: 'Settings',
Menu: 'Menu',
ComposeOpenPgp: 'ComposeOpenPgp',
MessageOpenPgp: 'MessageOpenPgp',
ViewOpenPgpKey: 'ViewOpenPgpKey',
KeyboardShortcutsHelp: 'KeyboardShortcutsHelp',
Ask: 'Ask'
};
Settings: 'Settings'
},
/**
* @enum {number}
*/
export const UploadErrorCode = {
UploadErrorCode = {
Normal: 0,
FileIsTooBig: 1,
FilePartiallyUploaded: 2,
NoFileUploaded: 3,
MissingTempFolder: 4,
OnSavingFile: 5,
FilePartiallyUploaded: 3,
NoFileUploaded: 4,
MissingTempFolder: 6,
OnSavingFile: 7,
FileType: 98,
Unknown: 99
};
},
/**
* @enum {number}
*/
export const SaveSettingsStep = {
SaveSettingsStep = {
Animate: -2,
Idle: -1,
TrueResult: 1,
FalseResult: 0
};
},
/**
* @enum {number}
*/
export const Notification = {
Notification = {
RequestError: 1,
RequestAborted: 2,
// Global
InvalidToken: 101,
AuthError: 102,
// User
ConnectionError: 104,
DomainNotAllowed: 109,
AccountNotAllowed: 110,
@ -110,20 +78,15 @@ export const Notification = {
CantDeleteNonEmptyFolder: 405,
// CantSaveSettings: 501,
CantSavePluginSettings: 502,
DomainAlreadyExists: 601,
CantInstallPackage: 701,
CantDeletePackage: 702,
InvalidPluginPackage: 703,
UnsupportedPluginPackage: 704,
DemoSendMessageError: 750,
DemoAccountError: 751,
AccountAlreadyExists: 801,
AccountDoesNotExist: 802,
AccountSwitchFailed: 803,
MailServerError: 901,
ClientViewError: 902,
@ -134,5 +97,12 @@ export const Notification = {
// JsonTimeout: 953,
UnknownNotification: 998,
UnknownError: 999
UnknownError: 999,
// Admin
CantInstallPackage: 701,
CantDeletePackage: 702,
InvalidPluginPackage: 703,
UnsupportedPluginPackage: 704,
CantSavePluginSettings: 705
};

View file

@ -4,14 +4,28 @@
* @enum {number}
*/
export const FolderType = {
Inbox: 10,
SentItems: 11,
Draft: 12,
Trash: 13,
Spam: 14,
Archive: 15,
NotSpam: 80,
User: 99
User: 0,
Inbox: 1,
Sent: 2,
Drafts: 3,
Spam: 4, // JUNK
Trash: 5,
Archive: 6,
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'
};
/**
@ -34,13 +48,13 @@ export const FolderSortMode = {
* @enum {string}
*/
export const ComposeType = {
Empty: 'empty',
Reply: 'reply',
ReplyAll: 'replyall',
Forward: 'forward',
ForwardAsAttachment: 'forward-as-attachment',
Draft: 'draft',
EditAsNew: 'editasnew'
Empty: 0,
Reply: 1,
ReplyAll: 2,
Forward: 3,
ForwardAsAttachment: 4,
Draft: 5,
EditAsNew: 6
};
/**
@ -58,19 +72,14 @@ export const SetSystemFoldersNotification = {
/**
* @enum {number}
*/
export const ClientSideKeyName = {
FoldersLashHash: 0,
MessagesInboxLastHash: 1,
MailBoxListSize: 2,
ExpandedFolders: 3,
FolderListSize: 4,
MessageListSize: 5,
LastReplyAction: 6,
LastSignMe: 7,
ComposeLastIdentityID: 8,
MessageHeaderFullInfo: 9,
MessageAttachmentControls: 10
};
export const
ClientSideKeyNameExpandedFolders = 3,
ClientSideKeyNameFolderListSize = 4,
ClientSideKeyNameMessageListSize = 5,
ClientSideKeyNameLastReplyAction = 6,
ClientSideKeyNameLastSignMe = 7,
ClientSideKeyNameMessageHeaderFullInfo = 9,
ClientSideKeyNameMessageAttachmentControls = 10;
/**
* @enum {number}

View file

@ -1,7 +1,7 @@
/* eslint key-spacing: 0 */
/* eslint quote-props: 0 */
import { isNonEmptyArray } from 'Common/Utils';
import { arrayLength } from 'Common/Utils';
const
cache = {},
@ -9,6 +9,7 @@ const
msOffice = app+'vnd.openxmlformats-officedocument.',
openDoc = app+'vnd.oasis.opendocument.',
sizes = ['B', 'KiB', 'MiB', 'GiB', 'TiB'],
lowerCase = text => text.toLowerCase().trim(),
exts = {
eml: 'message/rfc822',
@ -123,13 +124,13 @@ export const FileInfo = {
* @returns {string}
*/
getExtension: fileName => {
fileName = fileName.toLowerCase().trim();
fileName = lowerCase(fileName);
const result = fileName.split('.').pop();
return result === fileName ? '' : result;
},
getContentType: fileName => {
fileName = fileName.toLowerCase().trim();
fileName = lowerCase(fileName);
if ('winmail.dat' === fileName) {
return app + 'ms-tnef';
}
@ -158,8 +159,8 @@ export const FileInfo = {
* @returns {string}
*/
getType: (ext, mimeType) => {
ext = ext.toLowerCase().trim();
mimeType = mimeType.toLowerCase().trim().replace('csv/plain', 'text/csv');
ext = lowerCase(ext);
mimeType = lowerCase(mimeType).replace('csv/plain', 'text/csv');
let key = ext + mimeType;
if (cache[key]) {
@ -249,10 +250,10 @@ export const FileInfo = {
* @param {string} sFileType
* @returns {string}
*/
getCombinedIconClass: data => {
if (isNonEmptyArray(data)) {
getAttachmentsIconClass: data => {
if (arrayLength(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();
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,58 +1,78 @@
import ko from 'ko';
import { Scope } from 'Common/Enums';
export const doc = document;
let keyScopeFake = 'all';
export const $htmlCL = doc.documentElement.classList;
export const
ScopeMenu = 'Menu',
export const elementById = id => doc.getElementById(id);
doc = document,
export const Settings = rl.settings;
export const SettingsGet = rl.settings.get;
$htmlCL = doc.documentElement.classList,
export const dropdownVisibility = ko.observable(false).extend({ rateLimit: 0 });
elementById = id => doc.getElementById(id),
export const moveAction = ko.observable(false);
export const leftPanelDisabled = ko.observable(false);
exitFullscreen = () => getFullscreenElement() && (doc.exitFullscreen || doc.webkitExitFullscreen).call(doc),
getFullscreenElement = () => doc.fullscreenElement || doc.webkitFullscreenElement,
export const createElement = (name, attr) => {
let el = doc.createElement(name);
attr && Object.entries(attr).forEach(([k,v]) => el.setAttribute(k,v));
return el;
};
Settings = rl.settings,
SettingsGet = Settings.get,
SettingsCapa = Settings.capa,
dropdowns = [],
dropdownVisibility = ko.observable(false).extend({ rateLimit: 0 }),
moveAction = ko.observable(false),
leftPanelDisabled = ko.observable(false),
createElement = (name, attr) => {
let el = doc.createElement(name);
attr && Object.entries(attr).forEach(([k,v]) => el.setAttribute(k,v));
return el;
},
fireEvent = (name, detail) => dispatchEvent(new CustomEvent(name, {detail:detail})),
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 => {
if (!value) {
return keyScopeFake;
}
if (ScopeMenu !== value) {
keyScopeFake = value;
if (dropdownVisibility()) {
value = ScopeMenu;
}
}
keyScopeReal(value);
shortcuts.setScope(value);
};
dropdownVisibility.subscribe(value => {
if (value) {
keyScope(ScopeMenu);
} else if (ScopeMenu === shortcuts.getScope()) {
keyScope(keyScopeFake);
}
});
leftPanelDisabled.toggle = () => leftPanelDisabled(!leftPanelDisabled());
leftPanelDisabled.subscribe(value => {
value && moveAction() && moveAction(false);
$htmlCL.toggle('rl-left-panel-disabled', value);
});
moveAction.subscribe(value => value && leftPanelDisabled() && leftPanelDisabled(false));
// keys
export const keyScopeReal = ko.observable(Scope.All);
export const keyScope = (()=>{
let keyScopeFake = Scope.All;
dropdownVisibility.subscribe(value => {
if (value) {
keyScope(Scope.Menu);
} else if (Scope.Menu === shortcuts.getScope()) {
keyScope(keyScopeFake);
}
});
return ko.computed({
read: () => keyScopeFake,
write: value => {
if (Scope.Menu !== value) {
keyScopeFake = value;
if (dropdownVisibility()) {
value = Scope.Menu;
}
}
keyScopeReal(value);
}
});
})();
keyScopeReal.subscribe(value => shortcuts.setScope(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
tpl = createElement('template'),
htmlre = /[&<>"']/g,
htmlmap = {
'&': '&amp;',
@ -6,17 +11,489 @@ const
'>': '&gt;',
'"': '&quot;',
"'": '&#x27;'
},
replaceWithChildren = node => node.replaceWith(...[...node.childNodes]),
// Strip utm_* tracking
stripTracking = text => text.replace(/([?&])utm_[a-z]+=[^&?#]*/gsi, '$1').replace(/&&+/, '');
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>');
};
/**
* @param {string} text
* @returns {string}
*/
export function encodeHtml(text) {
return (text && text.toString ? text.toString() : ''+text).replace(htmlre, m => htmlmap[m]);
}
class HtmlEditor {
export class HtmlEditor {
/**
* @param {Object} element
* @param {Function=} onBlur
@ -24,25 +501,37 @@ class HtmlEditor {
* @param {Function=} onModeChange
*/
constructor(element, onBlur = null, onReady = null, onModeChange = null) {
this.editor;
this.blurTimer = 0;
this.__resizable = false;
this.__inited = false;
this.onBlur = onBlur;
this.onReady = onReady;
this.onModeChange = onModeChange;
this.element = element;
if (element) {
let editor;
this.resize = (() => {
try {
this.editor && this.__resizable && this.editor.resize(element.clientWidth, element.clientHeight);
} catch (e) {} // eslint-disable-line no-empty
}).throttle(100);
onReady = onReady ? [onReady] : [];
this.onReady = fn => onReady.push(fn);
const readyCallback = () => {
this.editor = editor;
this.onReady = fn => fn();
onReady.forEach(fn => fn());
};
this.init();
if (rl.createWYSIWYG) {
editor = rl.createWYSIWYG(element, readyCallback);
}
if (!editor) {
editor = new SquireUI(element);
setTimeout(readyCallback, 1);
}
editor.on('blur', () => this.blurTrigger());
editor.on('focus', () => this.blurTimer && clearTimeout(this.blurTimer));
editor.on('mode', () => {
this.blurTrigger();
this.onModeChange && this.onModeChange(!this.isPlain());
});
}
}
blurTrigger() {
@ -70,9 +559,9 @@ class HtmlEditor {
* @returns {void}
*/
clearCachedSignature() {
this.editor && this.editor.execCommand('insertSignature', {
this.onReady(() => this.editor.execCommand('insertSignature', {
clearCache: true
});
}));
}
/**
@ -82,75 +571,66 @@ class HtmlEditor {
* @returns {void}
*/
setSignature(signature, html, insertBefore = false) {
this.editor && this.editor.execCommand('insertSignature', {
this.onReady(() => this.editor.execCommand('insertSignature', {
isHtml: html,
insertBefore: insertBefore,
signature: signature
});
}));
}
/**
* @param {boolean=} wrapIsHtml = false
* @returns {string}
*/
getData(wrapIsHtml = false) {
getData() {
let result = '';
if (this.editor) {
try {
if (this.isPlain() && this.editor.plugins.plain && this.editor.__plain) {
result = this.editor.__plain.getRawData();
} else {
result = wrapIsHtml
? '<div data-html-editor-font-wrapper="true" style="font-family: arial, sans-serif; font-size: 13px;">' +
this.editor.getData() +
'</div>'
: this.editor.getData();
result = this.editor.getData();
}
} catch (e) {} // eslint-disable-line no-empty
}
return result;
}
/**
* @param {boolean=} wrapIsHtml = false
* @returns {string}
*/
getDataWithHtmlMark(wrapIsHtml = false) {
return (this.isHtml() ? ':HTML:' : '') + this.getData(wrapIsHtml);
getDataWithHtmlMark() {
return (this.isHtml() ? ':HTML:' : '') + this.getData();
}
modeWysiwyg() {
try {
this.editor && this.editor.setMode('wysiwyg');
} catch (e) { console.error(e); }
this.onReady(() => this.editor.setMode('wysiwyg'));
}
modePlain() {
try {
this.editor && this.editor.setMode('plain');
} catch (e) { console.error(e); }
this.onReady(() => this.editor.setMode('plain'));
}
setHtmlOrPlain(text) {
if (':HTML:' === text.substr(0, 6)) {
this.setHtml(text.substr(6));
if (':HTML:' === text.slice(0, 6)) {
this.setHtml(text.slice(6));
} else {
this.setPlain(text);
}
}
setData(mode, data) {
if (this.editor && this.__inited) {
this.onReady(() => {
const editor = this.editor;
this.clearCachedSignature();
try {
this.editor.setMode(mode);
if (this.isPlain() && this.editor.plugins.plain && this.editor.__plain) {
this.editor.__plain.setRawData(data);
editor.setMode(mode);
if (this.isPlain() && editor.plugins.plain && editor.__plain) {
editor.__plain.setRawData(data);
} else {
this.editor.setData(data);
editor.setData(data);
}
} catch (e) { console.error(e); }
}
});
}
setHtml(html) {
@ -161,46 +641,8 @@ class HtmlEditor {
this.setData('plain', txt);
}
init() {
if (this.element && !this.editor) {
const onReady = () => {
if (this.editor.removeMenuItem) {
this.editor.removeMenuItem('cut');
this.editor.removeMenuItem('copy');
this.editor.removeMenuItem('paste');
}
this.__resizable = true;
this.__inited = true;
this.resize();
this.onReady && this.onReady();
};
if (rl.createWYSIWYG) {
this.editor = rl.createWYSIWYG(this.element, onReady);
}
if (!this.editor) {
this.editor = new SquireUI(this.element);
setTimeout(onReady,1);
}
if (this.editor) {
this.editor.on('blur', () => this.blurTrigger());
this.editor.on('focus', () => this.blurTimer && clearTimeout(this.blurTimer));
this.editor.on('mode', () => {
this.blurTrigger();
this.onModeChange && this.onModeChange(!this.isPlain());
});
}
}
}
focus() {
try {
this.editor && this.editor.focus();
} catch (e) {} // eslint-disable-line no-empty
this.onReady(() => this.editor.focus());
}
hasFocus() {
@ -212,14 +654,15 @@ class HtmlEditor {
}
blur() {
try {
this.editor && this.editor.focusManager.blur(true);
} catch (e) {} // eslint-disable-line no-empty
this.onReady(() => this.editor.focusManager.blur(true));
}
clear() {
this.setHtml('');
this.onReady(() => this.isPlain() ? this.setPlain('') : this.setHtml(''));
}
}
export { HtmlEditor, HtmlEditor as default };
rl.Utils = {
htmlToPlain: htmlToPlain,
plainToHtml: plainToHtml
};

View file

@ -1,191 +1,140 @@
import { pString, pInt } from 'Common/Utils';
import { Settings, SettingsGet } from 'Common/Globals';
import { Settings } from 'Common/Globals';
const
ROOT = './',
HASH_PREFIX = '#/',
SERVER_PREFIX = './?',
VERSION = Settings.app('version'),
VERSION_PREFIX = Settings.app('webVersionPath') || 'snappymail/v/' + VERSION + '/',
VERSION_PREFIX = () => Settings.app('webVersionPath') || 'snappymail/v/' + VERSION + '/',
getHash = () => SettingsGet('AuthAccountHash') || '0';
adminPath = () => rl.adminArea() && !Settings.app('adminHostUse'),
/**
* @returns {string}
*/
export const SUB_QUERY_PREFIX = '&q[]=';
prefix = () => SERVER_PREFIX + (adminPath() ? Settings.app('adminPath') : '');
/**
* @param {string=} startupUrl
* @returns {string}
*/
export function root(startupUrl = '') {
return HASH_PREFIX + pString(startupUrl);
}
export const
SUB_QUERY_PREFIX = '&q[]=',
/**
* @returns {string}
*/
export function logoutLink() {
return (rl.adminArea() && !Settings.app('adminHostUse'))
? SERVER_PREFIX + (Settings.app('adminPath') || 'admin')
: ROOT;
}
/**
* @param {string=} startupUrl
* @returns {string}
*/
root = () => HASH_PREFIX,
/**
* @param {string} type
* @param {string} hash
* @param {string=} customSpecSuffix
* @returns {string}
*/
export function serverRequestRaw(type, hash, customSpecSuffix) {
return SERVER_PREFIX + '/Raw/' + SUB_QUERY_PREFIX + '/'
+ (null == customSpecSuffix ? getHash() : customSpecSuffix) + '/'
/**
* @returns {string}
*/
logoutLink = () => adminPath() ? prefix() : './',
/**
* @param {string} type
* @param {string} hash
* @param {string=} customSpecSuffix
* @returns {string}
*/
serverRequestRaw = (type, hash) =>
SERVER_PREFIX + '/Raw/' + SUB_QUERY_PREFIX + '/'
+ '0/' // AuthAccountHash ?
+ (type
? type + '/' + (hash ? SUB_QUERY_PREFIX + '/' + hash : '')
: '')
;
}
: ''),
/**
* @param {string} download
* @param {string=} customSpecSuffix
* @returns {string}
*/
export function attachmentDownload(download, customSpecSuffix) {
return serverRequestRaw('Download', download, customSpecSuffix);
}
/**
* @param {string} download
* @param {string=} customSpecSuffix
* @returns {string}
*/
attachmentDownload = (download, customSpecSuffix) =>
serverRequestRaw('Download', download, customSpecSuffix),
/**
* @param {string} type
* @returns {string}
*/
export function serverRequest(type) {
return SERVER_PREFIX + '/' + type + '/' + SUB_QUERY_PREFIX + '/' + getHash() + '/';
}
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} email
* @returns {string}
*/
export function change(email) {
return serverRequest('Change') + encodeURIComponent(email) + '/';
}
/**
* @param {string} type
* @returns {string}
*/
serverRequest = type => prefix() + '/' + type + '/' + SUB_QUERY_PREFIX + '/0/',
/**
* @param {string} hash
* @returns {string}
*/
export function userBackground(hash) {
return serverRequestRaw('UserBackground', hash);
}
/**
* @param {string} lang
* @param {boolean} isAdmin
* @returns {string}
*/
langLink = (lang, isAdmin) =>
SERVER_PREFIX + '/Lang/0/' + (isAdmin ? 'Admin' : 'App') + '/' + encodeURI(lang) + '/' + VERSION + '/',
/**
* @param {string} lang
* @param {boolean} isAdmin
* @returns {string}
*/
export function langLink(lang, isAdmin) {
return SERVER_PREFIX + '/Lang/0/' + (isAdmin ? 'Admin' : 'App') + '/' + encodeURI(lang) + '/' + VERSION + '/';
}
/**
* @param {string} path
* @returns {string}
*/
staticLink = path => VERSION_PREFIX() + 'static/' + path,
/**
* @param {string} path
* @returns {string}
*/
export function staticLink(path) {
return VERSION_PREFIX + 'static/' + path;
}
/**
* @param {string} theme
* @returns {string}
*/
themePreviewLink = theme => {
let prefix = VERSION_PREFIX();
if ('@custom' === theme.slice(-7)) {
theme = theme.slice(0, theme.length - 7).trim();
prefix = Settings.app('webPath') || '';
}
/**
* @returns {string}
*/
export function openPgpJs() {
return staticLink('js/min/openpgp.min.js');
}
return prefix + 'themes/' + encodeURI(theme) + '/images/preview.png';
},
/**
* @returns {string}
*/
export function openPgpWorkerJs() {
return staticLink('js/min/openpgp.worker.min.js');
}
/**
* @param {string} inboxFolderName = 'INBOX'
* @returns {string}
*/
mailbox = (inboxFolderName = 'INBOX') => HASH_PREFIX + 'mailbox/' + inboxFolderName,
/**
* @param {string} theme
* @returns {string}
*/
export function themePreviewLink(theme) {
let prefix = VERSION_PREFIX;
if ('@custom' === theme.substr(-7)) {
theme = theme.substr(0, theme.length - 7).trim();
prefix = Settings.app('webPath') || '';
}
/**
* @param {string=} screenName = ''
* @returns {string}
*/
settings = (screenName = '') => HASH_PREFIX + 'settings' + (screenName ? '/' + screenName : ''),
return prefix + 'themes/' + encodeURI(theme) + '/images/preview.png';
}
/**
* @param {string=} screenName
* @returns {string}
*/
admin = screenName => HASH_PREFIX + (
'AdminDomains' == screenName ? 'domains'
: 'AdminSecurity' == screenName ? 'security'
: ''
),
/**
* @param {string} inboxFolderName = 'INBOX'
* @returns {string}
*/
export function mailbox(inboxFolderName = 'INBOX') {
return HASH_PREFIX + 'mailbox/' + inboxFolderName;
}
/**
* @param {string} folder
* @param {number=} page = 1
* @param {string=} search = ''
* @param {number=} threadUid = 0
* @returns {string}
*/
mailBox = (folder, page, search, threadUid) => {
let result = [HASH_PREFIX + 'mailbox'];
/**
* @param {string=} screenName = ''
* @returns {string}
*/
export function settings(screenName = '') {
return HASH_PREFIX + 'settings' + (screenName ? '/' + screenName : '');
}
if (folder) {
result.push(folder + (threadUid ? '~' + threadUid : ''));
}
/**
* @param {string} screenName
* @returns {string}
*/
export function admin(screenName) {
let result = HASH_PREFIX;
switch (screenName) {
case 'AdminDomains':
result += 'domains';
break;
case 'AdminSecurity':
result += 'security';
break;
// no default
}
page = pInt(page, 1);
if (1 < page) {
result.push('p' + page);
}
return result;
}
/**
* @param {string} folder
* @param {number=} page = 1
* @param {string=} search = ''
* @param {string=} threadUid = ''
* @returns {string}
*/
export function mailBox(folder, page = 1, search = '', threadUid = '') {
page = pInt(page, 1);
search = pString(search);
let result = HASH_PREFIX + 'mailbox/';
if (folder) {
const resultThreadUid = pInt(threadUid);
result += encodeURI(folder) + (0 < resultThreadUid ? '~' + resultThreadUid : '');
}
if (1 < page) {
result = result.replace(/\/+$/, '') + '/p' + page;
}
if (search) {
result = result.replace(/\/+$/, '') + '/' + encodeURI(search);
}
return result;
}
search = pString(search);
if (search) {
result.push(encodeURI(search));
}
return result.join('/');
};

View file

@ -1,60 +0,0 @@
import { doc } from 'Common/Globals';
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);
}
}
addEventListener('reload-time', () => setTimeout(() =>
doc.querySelectorAll('[data-bind*="moment:"]').forEach(element => timeToNode(element))
, 1)
);

View file

@ -1,5 +1,6 @@
import { settingsAddViewModel } from 'Screen/AbstractSettings';
import { SettingsGet } from 'Common/Globals';
import { AbstractViewPopup } from 'Knoin/AbstractViews';
import { isArray, isFunction } from 'Common/Utils';
const SIMPLE_HOOKS = {},
@ -36,7 +37,7 @@ export function runHook(name, args = []) {
* @param {?number=} 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);
};
/**
@ -78,3 +79,5 @@ rl.pluginSettingsGet = (pluginSection, name) => {
plugins = plugins && null != plugins[pluginSection] ? plugins[pluginSection] : null;
return plugins ? (null == plugins[name] ? null : plugins[name]) : null;
};
rl.pluginPopupView = AbstractViewPopup;

View file

@ -1,5 +1,16 @@
import ko from 'ko';
import { addEventsListeners, addShortcut, registerShortcut } from 'Common/Globals';
import { isArray } from 'Common/Utils';
import { koComputable } from 'External/ko';
/*
oCallbacks:
ItemSelect
MiddleClick
AutoSelect
ItemGetUid
UpOrDown
*/
export class Selector {
/**
@ -19,8 +30,8 @@ export class Selector {
sItemFocusedSelector
) {
this.list = koList;
this.listChecked = ko.computed(() => this.list.filter(item => item.checked())).extend({ rateLimit: 0 });
this.isListChecked = ko.computed(() => 0 < this.listChecked().length);
this.listChecked = koComputable(() => this.list.filter(item => item.checked())).extend({ rateLimit: 0 });
this.isListChecked = koComputable(() => 0 < this.listChecked().length);
this.focusedItem = koFocusedItem || ko.observable(null);
this.selectedItem = koSelectedItem || ko.observable(null);
@ -209,11 +220,9 @@ export class Selector {
itemSelected(item) {
if (this.isListChecked()) {
if (!item) {
(this.oCallbacks.onItemSelect || (()=>{}))(item || null);
}
item || (this.oCallbacks.ItemSelect || (()=>0))(null);
} else if (item) {
(this.oCallbacks.onItemSelect || (()=>{}))(item);
(this.oCallbacks.ItemSelect || (()=>0))(item);
}
}
@ -226,13 +235,17 @@ export class Selector {
this.oContentScrollable = contentScrollable;
if (contentScrollable) {
contentScrollable.addEventListener('click', event => {
let el = event.target.closestWithin(this.sItemSelector, contentScrollable);
el && this.actionClick(ko.dataFor(el), event);
let getItem = selector => {
let el = event.target.closestWithin(selector, contentScrollable);
return el ? ko.dataFor(el) : null;
};
el = event.target.closestWithin(this.sItemCheckedSelector, contentScrollable);
if (el) {
const item = ko.dataFor(el);
addEventsListeners(contentScrollable, {
click: event => {
let el = event.target.closestWithin(this.sItemSelector, contentScrollable);
el && this.actionClick(ko.dataFor(el), event);
const item = getItem(this.sItemCheckedSelector);
if (item) {
if (event.shiftKey) {
this.actionClick(item, event);
@ -241,26 +254,33 @@ export class Selector {
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();
if (focused && !focused.selected()) {
this.actionClick(focused);
return false;
}
return true;
});
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);
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);
return false;
});
@ -271,7 +291,7 @@ export class Selector {
* @returns {boolean}
*/
autoSelect() {
return !!(this.oCallbacks.onAutoSelect || (()=>true))();
return !!(this.oCallbacks.AutoSelect || (()=>1))();
}
/**
@ -281,7 +301,7 @@ export class Selector {
getItemUid(item) {
let uid = '';
const getItemUidCallback = this.oCallbacks.onItemGetUid || null;
const getItemUidCallback = this.oCallbacks.ItemGetUid || null;
if (getItemUidCallback && item) {
uid = getItemUidCallback(item);
}
@ -312,7 +332,7 @@ export class Selector {
} else if (++i < listLen) {
result = list[i];
}
result || (this.oCallbacks.onUpUpOrDownDown || (()=>true))('ArrowUp' === sEventKey);
result || (this.oCallbacks.UpOrDown || (()=>0))('ArrowUp' === sEventKey);
} else if ('Home' === sEventKey) {
result = list[0];
} else if ('End' === sEventKey) {

View file

@ -2,158 +2,211 @@ import ko from 'ko';
import { Notification, UploadErrorCode } from 'Common/Enums';
import { langLink } from 'Common/Links';
import { doc, createElement } from 'Common/Globals';
import { getKeyByValue, forEachObjectEntry } from 'Common/Utils';
let I18N_DATA = window.snappymailI18N || {};
let I18N_DATA = {};
export const trigger = ko.observable(false);
/**
* @param {string} key
* @param {Object=} valueList
* @param {string=} defaulValue
* @returns {string}
*/
export function i18n(key, valueList, defaulValue) {
let path = key.split('/');
if (!I18N_DATA[path[0]] || !path[1]) {
return defaulValue || key;
}
let result = I18N_DATA[path[0]][path[1]] || defaulValue || key;
if (valueList) {
Object.entries(valueList).forEach(([key, value]) => {
result = result.replace('%' + key + '%', value);
});
}
return result;
}
const i18nToNode = element => {
const key = element.dataset.i18n;
if (key) {
if ('[' === key.substr(0, 1)) {
switch (key.substr(0, 6)) {
case '[html]':
element.innerHTML = i18n(key.substr(6));
break;
case '[place':
element.placeholder = i18n(key.substr(13));
break;
case '[title':
element.title = i18n(key.substr(7));
break;
// no default
const
i18nToNode = element => {
const key = element.dataset.i18n;
if (key) {
if ('[' === key.slice(0, 1)) {
switch (key.slice(0, 6)) {
case '[html]':
element.innerHTML = i18n(key.slice(6));
break;
case '[place':
element.placeholder = i18n(key.slice(13));
break;
case '[title':
element.title = i18n(key.slice(7));
break;
// no default
}
} else {
element.textContent = i18n(key);
}
} else {
element.textContent = i18n(key);
}
}
},
},
init = () => {
if (rl.I18N) {
I18N_DATA = rl.I18N;
Date.defineRelativeTimeFormat(rl.relativeTime || {});
rl.I18N = null;
return 1;
}
},
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 => {
let key = getKeyByValue(Notification, code);
return key ? I18N_DATA.NOTIFICATIONS[i18nKey(key).replace('_NOTIFICATION', '_ERROR')] : '';
};
/**
* @param {Object} elements
* @param {boolean=} animate = false
*/
export function i18nToNodes(element) {
setTimeout(() =>
element.querySelectorAll('[data-i18n]').forEach(item => i18nToNode(item))
, 1);
}
export const
trigger = ko.observable(false),
/**
* @param {Function} startCallback
* @param {Function=} langCallback = null
*/
export function initOnStartOrLangChange(startCallback, langCallback = null) {
startCallback && startCallback();
startCallback && trigger.subscribe(startCallback);
langCallback && trigger.subscribe(langCallback);
}
function getNotificationMessage(code) {
let key = getKeyByValue(Notification, code);
if (key) {
key = i18nKey(key).replace('_NOTIFICATION', '_ERROR');
return I18N_DATA.NOTIFICATIONS[key];
}
}
/**
* @param {number} code
* @param {*=} message = ''
* @param {*=} defCode = null
* @returns {string}
*/
export function getNotification(code, message = '', defCode = 0) {
code = parseInt(code, 10) || 0;
if (Notification.ClientViewError === code && message) {
return message;
}
return getNotificationMessage(code)
|| getNotificationMessage(parseInt(defCode, 10))
|| '';
}
/**
* @param {*} code
* @returns {string}
*/
export function getUploadErrorDescByCode(code) {
let result = 'UNKNOWN';
code = parseInt(code, 10) || 0;
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 {string} language
*/
export function reload(admin, language) {
return new Promise((resolve, reject) => {
const script = createElement('script');
script.onload = () => {
// reload the data
if (window.snappymailI18N) {
I18N_DATA = window.snappymailI18N;
i18nToNodes(doc);
dispatchEvent(new CustomEvent('reload-time'));
trigger(!trigger());
/**
* @param {string} key
* @param {Object=} valueList
* @param {string=} defaulValue
* @returns {string}
*/
i18n = (key, valueList, defaulValue) => {
let result = defaulValue || key;
if (key) {
let path = key.split('/');
if (I18N_DATA[path[0]] && path[1]) {
result = I18N_DATA[path[0]][path[1]] || result;
}
window.snappymailI18N = null;
script.remove();
resolve();
};
script.onerror = () => reject(new Error('Language '+language+' failed'));
script.src = langLink(language, admin);
// script.async = true;
doc.head.append(script);
});
}
}
if (valueList) {
forEachObjectEntry(valueList, (key, value) => {
result = result.replace('%' + key + '%', value);
});
}
return result;
},
/**
*
* @param {string} language
* @param {boolean=} isEng = false
* @returns {string}
*/
export function convertLangName(language, isEng = false) {
return i18n(
'LANGS_NAMES' + (true === isEng ? '_EN' : '') + '/' + language,
null,
language
);
}
/**
* @param {Object} elements
* @param {boolean=} animate = false
*/
i18nToNodes = element =>
setTimeout(() =>
element.querySelectorAll('[data-i18n]').forEach(item => i18nToNode(item))
, 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=} langCallback = null
*/
initOnStartOrLangChange = (startCallback, langCallback = null) => {
startCallback && startCallback();
startCallback && trigger.subscribe(startCallback);
langCallback && trigger.subscribe(langCallback);
},
/**
* @param {number} code
* @param {*=} message = ''
* @param {*=} defCode = null
* @returns {string}
*/
getNotification = (code, message = '', defCode = 0) => {
code = parseInt(code, 10) || 0;
if (Notification.ClientViewError === code && message) {
return message;
}
return getNotificationMessage(code)
|| getNotificationMessage(parseInt(defCode, 10))
|| '';
},
/**
* @param {*} code
* @returns {string}
*/
getUploadErrorDescByCode = code => {
let key = getKeyByValue(UploadErrorCode, parseInt(code, 10));
return i18n('UPLOAD/ERROR_' + (key ? i18nKey(key) : 'UNKNOWN'));
},
/**
* @param {boolean} admin
* @param {string} language
*/
translatorReload = (admin, language) =>
new Promise((resolve, reject) => {
const script = createElement('script');
script.onload = () => {
// reload the data
if (init()) {
i18nToNodes(doc);
admin || reloadTime();
trigger(!trigger());
}
script.remove();
resolve();
};
script.onerror = () => reject(new Error('Language '+language+' failed'));
script.src = langLink(language, admin);
// script.async = true;
doc.head.append(script);
}),
/**
*
* @param {string} language
* @param {boolean=} isEng = false
* @returns {string}
*/
convertLangName = (language, isEng = false) =>
i18n(
'LANGS_NAMES' + (true === isEng ? '_EN' : '') + '/' + language,
null,
language
);
init();

View file

@ -1,107 +1,50 @@
import { SaveSettingsStep } from 'Common/Enums';
import { doc, createElement, elementById } from 'Common/Globals';
export const
isArray = Array.isArray,
isNonEmptyArray = array => isArray(array) && array.length,
isFunction = v => typeof v === 'function';
/**
* @param {*} value
* @param {number=} defaultValue = 0
* @returns {number}
*/
export function pInt(value, defaultValue = 0) {
value = parseInt(value, 10);
return isNaN(value) || !isFinite(value) ? defaultValue : value;
}
/**
* @param {*} value
* @returns {string}
*/
export function pString(value) {
return null != value ? '' + value : '';
}
/**
* @returns {boolean}
*/
export function inFocus() {
try {
return doc.activeElement && doc.activeElement.matches(
'input,textarea,.cke_editable'
);
} catch (e) {
return false;
}
}
/**
* @param {string} theme
* @returns {string}
*/
export const convertThemeName = theme => {
if ('@custom' === theme.substr(-7)) {
theme = theme.substr(0, theme.length - 7).trim();
}
return theme
.replace(/([A-Z])/g, ' $1')
.replace(/[^a-zA-Z0-9]+/g, ' ')
.trim();
};
/**
* @param {object} domOption
* @param {object} item
* @returns {void}
*/
export function defaultOptionsAfterRender(domItem, item) {
if (item && undefined !== item.disabled && domItem) {
domItem.classList.toggle('disabled', domItem.disabled = item.disabled);
}
}
/**
* @param {object} koTrigger
* @param {mixed} context
* @returns {mixed}
*/
export function settingsSaveHelperSimpleFunction(koTrigger, context) {
return iError => {
koTrigger.call(context, iError ? SaveSettingsStep.FalseResult : SaveSettingsStep.TrueResult);
setTimeout(() => koTrigger.call(context, SaveSettingsStep.Idle), 1000);
};
}
import { elementById } from 'Common/Globals';
let __themeTimer = 0,
__themeJson = null;
/**
* @param {string} value
* @param {function=} themeTrigger = noop
* @returns {void}
*/
export function changeTheme(value, themeTrigger = ()=>{}) {
const themeLink = elementById('app-theme-link'),
clearTimer = () => {
__themeTimer = setTimeout(() => themeTrigger(SaveSettingsStep.Idle), 1000);
__themeJson = null;
};
export const
isArray = Array.isArray,
arrayLength = array => isArray(array) && array.length,
isFunction = v => typeof v === 'function',
pString = value => null != value ? '' + value : '',
let themeStyle = elementById('app-theme-style'),
url = (themeLink && themeLink.href) || (themeStyle && themeStyle.dataset.href);
forEachObjectValue = (obj, fn) => Object.values(obj).forEach(fn),
if (url) {
url = url.toString()
.replace(/\/-\/[^/]+\/-\//, '/-/' + value + '/-/')
.replace(/\/Css\/[^/]+\/User\//, '/Css/0/User/')
.replace(/\/Hash\/[^/]+\//, '/Hash/-/');
forEachObjectEntry = (obj, fn) => Object.entries(obj).forEach(([key, value]) => fn(key, value)),
if ('Json/' !== url.substr(-5)) {
url += 'Json/';
}
pInt = (value, defaultValue = 0) => {
value = parseInt(value, 10);
return isNaN(value) || !isFinite(value) ? defaultValue : value;
},
convertThemeName = theme => theme
.replace(/@custom$/, '')
.replace(/([A-Z])/g, ' $1')
.replace(/[^a-zA-Z0-9]+/g, ' ')
.trim(),
defaultOptionsAfterRender = (domItem, item) =>
domItem && item && undefined !== item.disabled
&& domItem.classList.toggle('disabled', domItem.disabled = item.disabled),
// unescape(encodeURIComponent()) makes the UTF-16 DOMString to an UTF-8 string
b64EncodeJSON = data => btoa(unescape(encodeURIComponent(JSON.stringify(data)))),
/* // Without deprecated 'unescape':
b64EncodeJSON = data => btoa(encodeURIComponent(JSON.stringify(data)).replace(
/%([0-9A-F]{2})/g, (match, p1) => String.fromCharCode('0x' + p1)
)),
*/
b64EncodeJSONSafe = data => b64EncodeJSON(data).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''),
changeTheme = (value, themeTrigger = ()=>0) => {
const themeStyle = elementById('app-theme-style'),
clearTimer = () => {
__themeTimer = setTimeout(() => themeTrigger(SaveSettingsStep.Idle), 1000);
__themeJson = null;
},
url = themeStyle.dataset.href.replace(/(Admin|User)\/-\/[^/]+\//, '$1/-/' + value + '/') + 'Json/';
clearTimeout(__themeTimer);
@ -117,36 +60,12 @@ export function changeTheme(value, themeTrigger = ()=>{}) {
}
rl.fetchJSON(url, init)
.then(data => {
if (data && isArray(data) && 2 === data.length) {
if (themeLink && !themeStyle) {
themeStyle = createElement('style');
themeStyle.id = 'app-theme-style';
themeLink.after(themeStyle);
themeLink.remove();
}
if (themeStyle) {
themeStyle.textContent = data[1];
themeStyle.dataset.href = url;
themeStyle.dataset.theme = data[0];
}
if (2 === arrayLength(data)) {
themeStyle.textContent = data[1];
themeTrigger(SaveSettingsStep.TrueResult);
}
})
.then(clearTimer, clearTimer);
}
}
},
export function addObservablesTo(target, observables) {
Object.entries(observables).forEach(([key, value]) =>
target[key] = /*isArray(value) ? ko.observableArray(value) :*/ ko.observable(value) );
}
export function addComputablesTo(target, computables) {
Object.entries(computables).forEach(([key, fn]) => target[key] = ko.computed(fn));
}
export function addSubscribablesTo(target, subscribables) {
Object.entries(subscribables).forEach(([key, fn]) => target[key].subscribe(fn));
}
getKeyByValue = (o, v) => Object.keys(o).find(key => o[key] === v);

View file

@ -1,341 +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 { encodeHtml } from 'Common/Html';
import { isArray } from 'Common/Utils';
import { createElement } from 'Common/Globals';
import { MessageModel } from 'Model/Message';
import { MessageUserStore } from 'Stores/User/Message';
import { MessagelistUserStore } from 'Stores/User/Messagelist';
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 {boolean=} includeZero = true
* @param {string} link
* @returns {boolean}
*/
export function isPosNumeric(value, includeZero = true) {
return null != value && (includeZero ? /^[0-9]*$/ : /^[1-9]+[0-9]*$/).test(value.toString());
}
/**
* @param {string} text
* @param {number=} len = 100
* @returns {string}
*/
function splitPlainText(text, len = 100) {
let prefix = '',
subText = '',
result = text,
spacePos = 0,
newLinePos = 0;
while (result.length > len) {
subText = result.substr(0, len);
spacePos = subText.lastIndexOf(' ');
newLinePos = subText.lastIndexOf('\n');
if (-1 !== newLinePos) {
spacePos = newLinePos;
}
if (-1 === spacePos) {
spacePos = len;
}
prefix += subText.substr(0, spacePos) + '\n';
result = result.substr(spacePos + 1);
download = (link, name = "") => {
if (ThemeStore.isMobile()) {
open(link, '_self');
focus();
} else {
const oLink = createElement('a');
oLink.href = link;
oLink.target = '_blank';
oLink.download = name;
doc.body.appendChild(oLink).click();
oLink.remove();
}
return prefix + result;
}
/**
* @param {string} html
* @returns {string}
*/
export function htmlToPlain(html) {
let pos = 0,
limit = 0,
iP1 = 0,
iP2 = 0,
iP3 = 0,
text = '';
const 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() + ' ' : ''
);
};
const convertDivs = (...args) => {
if (args && 1 < args.length) {
let divText = args[1].trim();
if (divText.length) {
divText = divText.replace(/<div[^>]*>([\s\S\r\n]*)<\/div>/gim, convertDivs);
divText = '\n' + divText.trim() + '\n';
}
return divText;
}
return '';
};
const
tpl = createElement('template'),
convertPre = (...args) =>
args && 1 < args.length
? args[1]
.toString()
.replace(/[\n]/gm, '<br/>')
.replace(/[\r]/gm, '')
: '',
fixAttibuteValue = (...args) => (args && 1 < args.length ? '' + args[1] + encodeHtml(args[2]) : ''),
convertLinks = (...args) => (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 = splitPlainText(tpl.content.textContent
.replace(/\n[ \t]+/gm, '\n')
.replace(/[\n]{3,}/gm, '\n\n')
.replace(/&gt;/gi, '>')
.replace(/&lt;/gi, '<')
.replace(/&amp;/gi, '&')
);
pos = 0;
limit = 800;
while (0 < limit) {
--limit;
iP1 = text.indexOf('__bq__start__', pos);
if (-1 < iP1) {
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;
}
} else {
break;
}
}
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} aSystem
* @param {Array} aList
* @param {Array=} aDisabled
* @param {Array=} aHeaderLines
* @param {Function=} fDisableCallback
* @param {Function=} fRenameCallback
* @param {boolean=} bSystem
* @param {boolean=} bBuildUnvisible
* @returns {Array}
*/
export function folderListOptionsBuilder(
aSystem,
aList,
aDisabled,
aHeaderLines,
fDisableCallback,
fRenameCallback,
bSystem,
bBuildUnvisible
) {
let /**
* @type {?FolderModel}
*/
bSep = false,
aResult = [];
const sDeepPrefix = '\u00A0\u00A0\u00A0';
bBuildUnvisible = undefined === bBuildUnvisible ? false : !!bBuildUnvisible;
bSystem = null == bSystem ? 0 < aSystem.length : bSystem;
fDisableCallback = null != fDisableCallback ? fDisableCallback : null;
fRenameCallback = null != fRenameCallback ? fRenameCallback : null;
if (!isArray(aDisabled)) {
aDisabled = [];
}
if (!isArray(aHeaderLines)) {
aHeaderLines = [];
}
aHeaderLines.forEach(line => {
aResult.push({
id: line[0],
name: line[1],
system: false,
dividerbar: false,
disabled: false
});
});
bSep = true;
aSystem.forEach(oItem => {
aResult.push({
id: oItem.fullNameRaw,
name: fRenameCallback ? fRenameCallback(oItem) : oItem.name(),
system: true,
dividerbar: bSep,
disabled:
!oItem.selectable ||
aDisabled.includes(oItem.fullNameRaw) ||
(fDisableCallback ? fDisableCallback(oItem) : false)
});
bSep = false;
});
bSep = true;
aList.forEach(oItem => {
// if (oItem.subscribed() || !oItem.exists || bBuildUnvisible)
if (
(oItem.subscribed() || !oItem.exists || bBuildUnvisible) &&
(oItem.selectable || oItem.hasSubscribedSubfolders())
) {
if (FolderType.User === oItem.type() || !bSystem || oItem.hasSubscribedSubfolders()) {
aResult.push({
id: oItem.fullNameRaw,
name:
sDeepPrefix.repeat(oItem.deep + 1) +
(fRenameCallback ? fRenameCallback(oItem) : oItem.name()),
system: false,
dividerbar: bSep,
disabled:
!oItem.selectable ||
aDisabled.includes(oItem.fullNameRaw) ||
(fDisableCallback ? fDisableCallback(oItem) : false)
});
bSep = false;
}
}
if (oItem.subscribed() && oItem.subFolders.length) {
aResult = aResult.concat(
folderListOptionsBuilder(
[],
oItem.subFolders(),
aDisabled,
[],
fDisableCallback,
fRenameCallback,
bSystem,
bBuildUnvisible
)
);
}
});
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}
*/
export function computedPaginatorHelper(koCurrentPage, koPageCount) {
computedPaginatorHelper = (koCurrentPage, koPageCount) => {
return () => {
const currentPage = koCurrentPage(),
pageCount = koPageCount(),
@ -395,13 +100,13 @@ export function computedPaginatorHelper(koCurrentPage, koPageCount) {
if (3 === prev) {
fAdd(2, false);
} else if (3 < prev) {
fAdd(Math.round((prev - 1) / 2), false, '...');
fAdd(Math.round((prev - 1) / 2), false, '');
}
if (pageCount - 2 === next) {
fAdd(pageCount - 1, true);
} else if (pageCount - 2 > next) {
fAdd(Math.round((pageCount + next) / 2), true, '...');
fAdd(Math.round((pageCount + next) / 2), true, '');
}
// first and last
@ -416,81 +121,253 @@ export function computedPaginatorHelper(koCurrentPage, koPageCount) {
return result;
};
}
},
/**
* @param {string} mailToUrl
* @returns {boolean}
*/
export function mailToHelper(mailToUrl) {
if (
mailToUrl &&
'mailto:' ===
mailToUrl
.toString()
.substr(0, 7)
.toLowerCase()
) {
mailToUrl = mailToUrl.toString().substr(7);
mailToHelper = mailToUrl => {
if (mailToUrl && 'mailto:' === mailToUrl.slice(0, 7).toLowerCase()) {
mailToUrl = mailToUrl.slice(7).split('?');
let to = [],
cc = null,
bcc = null,
params = {};
const email = mailToUrl.replace(/\?.+$/, ''),
query = mailToUrl.replace(/^[^?]*\?/, '');
query.split('&').forEach(temp => {
temp = temp.split('=');
params[decodeURIComponent(temp[0])] = decodeURIComponent(temp[1]);
});
if (undefined !== params.to) {
to = EmailModel.parseEmailLine(decodeURIComponent(email + ',' + params.to));
to = Object.values(
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);
}
if (undefined !== params.cc) {
cc = EmailModel.parseEmailLine(decodeURIComponent(params.cc));
}
if (undefined !== params.bcc) {
bcc = EmailModel.parseEmailLine(decodeURIComponent(params.bcc));
}
const
email = mailToUrl[0],
params = new URLSearchParams(mailToUrl[1]),
toEmailModel = value => null != value ? EmailModel.parseEmailLine(value) : null;
showMessageComposer([
ComposeType.Empty,
null,
to,
cc,
bcc,
null == params.subject ? null : decodeURIComponent(params.subject),
null == params.body ? null : plainToHtml(decodeURIComponent(params.body))
params.get('to')
? Object.values(
toEmailModel(email + ',' + params.get('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;
}, {})
)
: EmailModel.parseEmailLine(email),
toEmailModel(params.get('cc')),
toEmailModel(params.get('bcc')),
params.get('subject'),
plainToHtml(params.get('body') || '')
]);
return true;
}
return false;
}
},
export function showMessageComposer(params = [])
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,27 +1,15 @@
import ko from 'ko';
import { AbstractComponent } from 'Component/Abstract';
class AbstractCheckbox extends AbstractComponent {
export class AbstractCheckbox {
/**
* @param {Object} params = {}
*/
constructor(params = {}) {
super();
this.value = ko.isObservable(params.value) ? params.value
: ko.observable(!!params.value);
this.value = params.value;
if (undefined === this.value || !this.value.subscribe) {
this.value = ko.observable(!!this.value);
}
this.enable = params.enable;
if (undefined === this.enable || !this.enable.subscribe) {
this.enable = ko.observable(undefined === this.enable || !!this.enable);
}
this.disable = params.disable;
if (undefined === this.disable || !this.disable.subscribe) {
this.disable = ko.observable(!!this.disable);
}
this.enable = ko.isObservable(params.enable) ? params.enable
: ko.observable(undefined === params.enable || !!params.enable);
this.label = params.label || '';
this.inline = !!params.inline;
@ -30,10 +18,6 @@ class AbstractCheckbox extends AbstractComponent {
}
click() {
if (this.enable() && !this.disable()) {
this.value(!this.value());
}
this.enable() && this.value(!this.value());
}
}
export { AbstractCheckbox };

View file

@ -1,61 +1,57 @@
import ko from 'ko';
import { pInt } from 'Common/Utils';
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
*/
constructor(params) {
super();
this.value = params.value || '';
this.size = params.size || 0;
this.label = params.label || '';
this.preLabel = params.preLabel || '';
this.enable = undefined === params.enable ? true : params.enable;
this.enable = null == params.enable ? true : params.enable;
this.trigger = params.trigger && params.trigger.subscribe ? params.trigger : null;
this.placeholder = params.placeholder || '';
this.labeled = undefined !== params.label;
this.preLabeled = undefined !== params.preLabel;
this.triggered = undefined !== params.trigger && !!this.trigger;
this.classForTrigger = ko.observable('');
this.className = ko.computed(() => {
const size = ko.unwrap(this.size),
suffixValue = this.trigger ? ' ' + ('settings-saved-trigger-input ' + this.classForTrigger()).trim() : '';
return (0 < size ? 'span' + size : '') + suffixValue;
});
if (undefined !== params.width && params.element) {
params.element.querySelectorAll('input,select,textarea').forEach(node => node.style.width = params.width);
}
this.disposable.push(this.className);
this.labeled = null != params.label;
let size = 0 < params.size ? 'span' + params.size : '';
if (this.trigger) {
this.setTriggerState(this.trigger());
const
classForTrigger = ko.observable(''),
setTriggerState = value => {
switch (pInt(value)) {
case SaveSettingsStep.TrueResult:
classForTrigger('success');
break;
case SaveSettingsStep.FalseResult:
classForTrigger('error');
break;
default:
classForTrigger('');
break;
}
};
this.disposable.push(this.trigger.subscribe(this.setTriggerState, this));
setTriggerState(this.trigger());
this.className = koComputable(() =>
(size + ' settings-saved-trigger-input ' + classForTrigger()).trim()
);
this.disposables = [
this.trigger.subscribe(setTriggerState, this),
this.className
];
} else {
this.className = size;
this.disposables = [];
}
}
setTriggerState(value) {
switch (pInt(value)) {
case SaveSettingsStep.TrueResult:
this.classForTrigger('success');
break;
case SaveSettingsStep.FalseResult:
this.classForTrigger('error');
break;
default:
this.classForTrigger('');
break;
}
dispose() {
this.disposables.forEach(dispose);
}
}
export { AbstractInput };

View file

@ -1,5 +1,4 @@
import { doc, createElement } from 'Common/Globals';
import { isArray } from 'Common/Utils';
import { doc, createElement, addEventsListeners } from 'Common/Globals';
import { EmailModel } from 'Model/Email';
const contentType = 'snappymail/emailaddress',
@ -17,7 +16,7 @@ export class EmailAddressesComponent {
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 Mobile dataTransfer.types.includes(contentType) fails, only text/plain is set
validDropzone = () => dragAddress && dragAddress.li.parentNode !== self.ul,
@ -42,49 +41,55 @@ export class EmailAddressesComponent {
// Create the elements
self.ul = createElement('ul',{class:"emailaddresses"});
self.ul.addEventListener('click', e => self._focus(e));
self.ul.addEventListener('dblclick', e => self._editTag(e));
self.ul.addEventListener("dragenter", fnDrag);
self.ul.addEventListener("dragover", fnDrag);
self.ul.addEventListener("drop", e => {
if (validDropzone() && dragAddress.value) {
e.preventDefault();
dragAddress.source._removeDraggedTag(dragAddress.li);
self._parseValue(dragAddress.value);
addEventsListeners(self.ul, {
click: e => self._focus(e),
dblclick: e => self._editTag(e),
dragenter: fnDrag,
dragover: fnDrag,
drop: e => {
if (validDropzone() && dragAddress.value) {
e.preventDefault();
dragAddress.source._removeDraggedTag(dragAddress.li);
self._parseValue(dragAddress.value);
}
}
});
self.input = createElement('input',{type:"text", list:datalist.id,
autocomplete:"off", autocorrect:"off", autocapitalize:"off", spellcheck:"false"});
self.input.addEventListener('focus', () => self._focusTrigger(true));
self.input.addEventListener('blur', () => {
// prevent autoComplete menu click from causing a false 'blur'
self._parseInput(true);
self._focusTrigger(false);
});
self.input.addEventListener('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();
addEventsListeners(self.input, {
focus: () => {
self._focusTrigger(true);
self.input.value || self._resetDatalist();
},
blur: () => {
// prevent autoComplete menu click from causing a false 'blur'
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
if (element.placeholder) {
@ -116,7 +121,7 @@ export class EmailAddressesComponent {
)
}
}).throttle(500)
: () => {};
: () => 0;
}
_focusTrigger(bValue) {
@ -138,20 +143,56 @@ export class EmailAddressesComponent {
_parseValue(val) {
if (val) {
var self = this,
values = [];
const v = val.trim(),
hook = (v && [',', ';', '\n'].includes(v.substr(-1)))
? EmailModel.splitEmailLine(val)
: null;
values = (hook || [val]).map(value => EmailModel.parseEmailLine(value))
.flat(Infinity)
.map(item => (item.toLine ? [item.toLine(false), item] : [item, null]));
const self = this,
v = val.trim(),
hook = (v && [',', ';', '\n'].includes(v.slice(-1))) ? EmailModel.splitEmailLine(val) : null,
values = (hook || [val]).map(value => EmailModel.parseEmailLine(value))
.flat(Infinity)
.map(item => (item.toLine ? [item.toLine(false), item] : [item, null]));
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;
}
}
@ -202,56 +243,6 @@ export class EmailAddressesComponent {
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() {
return this._chosenValues.map(v => v.value).join(',');
}
@ -276,66 +267,70 @@ export class EmailAddressesComponent {
el = createElement('a',{href:'#', class:'ficon'});
el.append('✖');
el.addEventListener('click', e => self._removeTag(e, li));
el.addEventListener('focus', () => li.className = 'emailaddresses-selected');
el.addEventListener('blur', () => li.className = null);
el.addEventListener('keydown', e => {
switch (e.key) {
case 'Delete':
case 'Backspace':
self._removeTag(e, li);
break;
addEventsListeners(el, {
click: e => self._removeTag(e, li),
focus: () => li.className = 'emailaddresses-selected',
blur: () => li.className = null,
keydown: e => {
switch (e.key) {
case 'Delete':
case 'Backspace':
self._removeTag(e, li);
break;
// 'e' - edit tag (removes tag and places value into visible input
case 'e':
case 'Enter':
self._editTag(e);
break;
// 'e' - edit tag (removes tag and places value into visible input
case 'e':
case 'Enter':
self._editTag(e);
break;
case 'ArrowLeft':
// select the previous tag or input if no more tags exist
var previous = el.closest('li').previousElementSibling;
if (previous.matches('li')) {
previous.querySelector('a').focus();
} else {
self.focus();
}
break;
case 'ArrowLeft':
// select the previous tag or input if no more tags exist
var previous = el.closest('li').previousElementSibling;
if (previous.matches('li')) {
previous.querySelector('a').focus();
} else {
self.focus();
}
break;
case 'ArrowRight':
// select the next tag or input if no more tags exist
var next = el.closest('li').nextElementSibling;
if (next !== this.inputCont) {
next.querySelector('a').focus();
} else {
this.focus();
}
break;
case 'ArrowRight':
// select the next tag or input if no more tags exist
var next = el.closest('li').nextElementSibling;
if (next !== this.inputCont) {
next.querySelector('a').focus();
} else {
this.focus();
}
break;
case 'ArrowDown':
self._focus(e);
break;
case 'ArrowDown':
self._focus(e);
break;
}
}
});
li.append(el);
li.emailaddress = v;
li.addEventListener("dragstart", e => {
dragAddress = {
source: self,
li: li,
value: li.emailaddress.obj.toLine()
};
// e.dataTransfer.setData(contentType, li.emailaddress.obj.toLine());
e.dataTransfer.setData('text/plain', contentType);
// e.dataTransfer.setDragImage(li, 0, 0);
e.dataTransfer.effectAllowed = 'move';
li.style.opacity = 0.25;
});
li.addEventListener("dragend", () => {
dragAddress = null;
li.style.cssText = '';
addEventsListeners(li, {
dragstart: e => {
dragAddress = {
source: self,
li: li,
value: li.emailaddress.obj.toLine()
};
// e.dataTransfer.setData(contentType, li.emailaddress.obj.toLine());
e.dataTransfer.setData('text/plain', contentType);
// e.dataTransfer.setDragImage(li, 0, 0);
e.dataTransfer.effectAllowed = 'move';
li.style.opacity = 0.25;
},
dragend: () => {
dragAddress = null;
li.style.cssText = '';
}
});
self.inputCont.before(li);

View file

@ -1,41 +1,3 @@
import ko from 'ko';
import { AbstractCheckbox } from 'Component/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.animationBoxSetTrue = this.animationBoxSetTrue.bind(this);
this.animationCheckmarkSetTrue = this.animationCheckmarkSetTrue.bind(this);
this.disposable.push(
this.value.subscribe((value) => {
this.triggerAnimation(value);
}, this)
);
}
animationBoxSetTrue() {
this.animationBox(true);
}
animationCheckmarkSetTrue() {
this.animationCheckmark(true);
}
triggerAnimation(box) {
if (box) {
this.animationBoxSetTrue();
setTimeout(this.animationCheckmarkSetTrue, 200);
} else {
this.animationCheckmarkSetTrue();
setTimeout(this.animationBoxSetTrue, 200);
}
}
}
export class CheckboxMaterialDesignComponent extends AbstractCheckbox {}

View file

@ -13,11 +13,7 @@ export class SelectComponent extends AbstractInput {
this.optionsText = params.optionsText || null;
this.optionsValue = params.optionsValue || null;
this.optionsCaption = params.optionsCaption || null;
if (this.optionsCaption) {
this.optionsCaption = i18n(this.optionsCaption);
}
this.optionsCaption = i18n(params.optionsCaption || null);
this.defaultOptionsAfterRender = defaultOptionsAfterRender;
}

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

@ -9,13 +9,16 @@ const
i18n = (str, def) => rl.i18n(str) || def,
ctrlKey = shortcuts.getMetaKey().replace('meta','⌘') + ' + ',
ctrlKey = shortcuts.getMetaKey() + ' + ',
tpl = doc.createElement('template'),
clr = doc.createElement('input'),
createElement = name => doc.createElement(name),
tpl = createElement('template'),
clr = createElement('input'),
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 => {
let frag = doc.createDocumentFragment();
@ -33,13 +36,12 @@ const
addLinks: true // allow_smart_html_links
*/
sanitizeToDOMFragment: (html, isPaste/*, squire*/) => {
tpl.innerHTML = html
tpl.innerHTML = (html||'')
.replace(/<\/?(BODY|HTML)[^>]*>/gi,'')
.replace(/<!--[^>]+-->/g,'')
.replace(/<span[^>]*>\s*<\/span>/gi,'')
.trim();
tpl.querySelectorAll('a:empty,span:empty').forEach(el => el.remove());
tpl.querySelectorAll('[data-x-div-type]').forEach(el => el.replaceWith(getFragmentOfChildren(el)));
if (isPaste) {
tpl.querySelectorAll(removeElements).forEach(el => el.remove());
tpl.querySelectorAll('*').forEach(el => {
@ -57,66 +59,6 @@ const
}
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";
@ -127,9 +69,9 @@ class SquireUI
{
constructor(container) {
const
doClr = fn => () => {
doClr = name => () => {
clr.value = '';
clr.onchange = () => squire[fn](clr.value);
clr.onchange = () => squire.setStyle({[name]:clr.value});
clr.click();
},
@ -171,29 +113,15 @@ class SquireUI
colors: {
textColor: {
html: 'A<sub>▾</sub>',
cmd: doClr('setTextColor'),
cmd: doClr('color'),
hint: 'Text color'
},
backgroundColor: {
html: '🎨', /* ▧ */
cmd: doClr('setBackgroundColor'),
cmd: doClr('backgroundColor'),
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: {
bold: {
html: 'B',
@ -318,10 +246,10 @@ class SquireUI
}
},
plain = doc.createElement('textarea'),
wysiwyg = doc.createElement('div'),
toolbar = doc.createElement('div'),
browseImage = doc.createElement('input'),
plain = createElement('textarea'),
wysiwyg = createElement('div'),
toolbar = createElement('div'),
browseImage = createElement('input'),
squire = new Squire(wysiwyg, SquireDefaultConfig);
browseImage.type = 'file';
@ -336,7 +264,7 @@ class SquireUI
}
plain.className = 'squire-plain';
wysiwyg.className = 'squire-wysiwyg cke_editable';
wysiwyg.className = 'squire-wysiwyg';
this.mode = ''; // 'plain' | 'wysiwyg'
this.__plain = {
getRawData: () => this.plain.value,
@ -349,25 +277,24 @@ class SquireUI
this.wysiwyg = wysiwyg;
toolbar.className = 'squire-toolbar btn-toolbar';
let touchTap;
for (let group in actions) {
let group, action, touchTap;
for (group in actions) {
/*
if ('bidi' == group && !rl.settings.app('allowHtmlEditorBitiButtons')) {
continue;
}
let toolgroup = doc.createElement('div');
*/
let toolgroup = createElement('div');
toolgroup.className = 'btn-group';
toolgroup.id = 'squire-toolgroup-'+group;
for (let action in actions[group]) {
if ('source' == action && !rl.settings.app('allowHtmlEditorSourceButton')) {
continue;
}
for (action in actions[group]) {
let cfg = actions[group][action], input, ev = 'click';
if (cfg.input) {
input = doc.createElement('input');
input = createElement('input');
input.type = cfg.input;
ev = 'change';
} else if (cfg.select) {
input = doc.createElement('select');
input = createElement('select');
input.className = 'btn';
if (Array.isArray(cfg.select)) {
cfg.select.forEach(value => {
@ -377,7 +304,7 @@ class SquireUI
});
} else {
Object.entries(cfg.select).forEach(([label, options]) => {
let group = doc.createElement('optgroup');
let group = createElement('optgroup');
group.label = label;
Object.entries(options).forEach(([text, value]) => {
var option = new Option(text, value);
@ -389,7 +316,7 @@ class SquireUI
}
ev = 'input';
} else {
input = doc.createElement('button');
input = createElement('button');
input.type = 'button';
input.className = 'btn';
input.innerHTML = cfg.html;
@ -412,6 +339,7 @@ class SquireUI
input.title = ctrlKey + cfg.key;
}
input.dataset.action = action;
input.tabIndex = -1;
cfg.input = input;
toolgroup.append(input);
}
@ -425,8 +353,8 @@ class SquireUI
changes.redo.input.disabled = !state.canRedo;
});
squire.addEventListener('focus', () => shortcuts.off());
squire.addEventListener('blur', () => shortcuts.on());
// squire.addEventListener('focus', () => shortcuts.off());
// squire.addEventListener('blur', () => shortcuts.on());
container.append(toolbar, wysiwyg, plain);
@ -477,9 +405,9 @@ class SquireUI
let cl = this.container.classList;
cl.remove('squire-mode-'+this.mode);
if ('plain' == mode) {
this.plain.value = rl.Utils.htmlToPlain(this.squire.getHTML(), true).trim();
this.plain.value = htmlToPlain(this.squire.getHTML(), true);
} else {
this.setData(rl.Utils.plainToHtml(this.plain.value, true));
this.setData(plainToHtml(this.plain.value, true));
mode = 'wysiwyg';
}
this.mode = mode; // 'wysiwyg' or 'plain'
@ -509,19 +437,32 @@ class SquireUI
}, cfg);
if (cfg.clearCache) {
this.__previous_signature = null;
this._prev_txt_sig = null;
} else try {
const signature = cfg.isHtml ? htmlToPlain(cfg.signature) : cfg.signature;
if ('plain' === this.mode) {
if (cfg.isHtml) {
cfg.signature = rl.Utils.htmlToPlain(cfg.signature);
let
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 {
if (!cfg.isHtml) {
cfg.signature = rl.Utils.plainToHtml(cfg.signature);
}
this.setData(rl_signature_replacer(this, this.getData(), cfg.signature, true, cfg.insertBefore));
const squire = this.squire,
root = squire.getRoot(),
range = squire.getSelection(),
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) {
console.error(e);
}
@ -540,12 +481,6 @@ class SquireUI
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;

View file

@ -1,11 +1,14 @@
import 'External/ko';
import ko from 'ko';
import { HtmlEditor } from 'Common/Html';
import { timeToNode } from 'Common/Momentor';
import { elementById } from 'Common/Globals';
import { timeToNode } from 'Common/Translator';
import { elementById, addEventsListeners, dropdowns } from 'Common/Globals';
import { isArray } from 'Common/Utils';
import { dropdownsDetectVisibility } from 'Common/UtilsUser';
import { EmailAddressesComponent } from 'Component/EmailAddresses';
import { ThemeStore } from 'Stores/Theme';
import { moveMessagesToFolder } from 'Common/Folders';
import { setExpandedFolder } from 'Model/FolderCollection';
const rlContentType = 'snappymail/action',
@ -27,233 +30,223 @@ const rlContentType = 'snappymail/action',
id: 0,
stop: () => clearTimeout(dragTimer.id),
start: fn => dragTimer.id = setTimeout(fn, 500)
};
},
ttn = (element, fValueAccessor) => timeToNode(element, ko.unwrap(fValueAccessor()));
let dragImage,
dragData;
ko.bindingHandlers.editor = {
init: (element, fValueAccessor) => {
let editor = null;
Object.assign(ko.bindingHandlers, {
const fValue = fValueAccessor(),
fUpdateEditorValue = () => fValue && fValue.__editor && fValue.__editor.setHtmlOrPlain(fValue()),
fUpdateKoValue = () => fValue && fValue.__editor && fValue(fValue.__editor.getDataWithHtmlMark()),
fOnReady = () => {
fValue.__editor = editor;
fUpdateEditorValue();
};
editor: {
init: (element, fValueAccessor) => {
let editor = null;
if (ko.isObservable(fValue) && HtmlEditor) {
editor = new HtmlEditor(element, fUpdateKoValue, fOnReady, fUpdateKoValue);
const fValue = fValueAccessor(),
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.bindingHandlers.moment = {
init: ttn,
update: ttn
};
ko.bindingHandlers.emailsTags = {
init: (element, fValueAccessor, fAllBindingsAccessor) => {
const fValue = fValueAccessor(),
fAllBindings = fAllBindingsAccessor();
element.addresses = new EmailAddressesComponent(element, {
focusCallback: value => fValue.focused && fValue.focused(!!value),
autoCompleteSource: fAllBindings.autoCompleteSource || null,
onChange: value => fValue(value)
});
if (fValue.focused && fValue.focused.subscribe) {
fValue.focused.subscribe(value =>
element.addresses[value ? 'focus' : 'blur']()
);
// ko.utils.domNodeDisposal.addDisposeCallback(element, () => {
// });
}
}
},
update: (element, fValueAccessor) => {
element.addresses.value = ko.unwrap(fValueAccessor());
}
};
// Start dragging selected messages
ko.bindingHandlers.dragmessages = {
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);
moment: {
init: ttn,
update: ttn
},
// Else Chrome doesn't show it
dragImage.style.left = e.clientX + 'px';
dragImage.style.top = e.clientY + 'px';
dragImage.style.right = 'auto';
emailsTags: {
init: (element, fValueAccessor, fAllBindings) => {
const fValue = fValueAccessor();
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
dragImage.style.cssText = '';
} else {
e.preventDefault();
if (fValue.focused && fValue.focused.subscribe) {
fValue.focused.subscribe(value =>
element.addresses[value ? 'focus' : 'blur']()
);
}
},
update: (element, fValueAccessor) => {
element.addresses.value = ko.unwrap(fValueAccessor());
}
},
}, false);
element.addEventListener("dragend", () => dragData = null);
element.setAttribute('draggable', true);
}
};
// Start dragging selected messages
dragmessages: {
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
ko.bindingHandlers.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);
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);
}
}
});
}
};
// Else Chrome doesn't show it
dragImage.style.left = e.clientX + 'px';
dragImage.style.top = e.clientY + 'px';
dragImage.style.right = 'auto';
ko.bindingHandlers.sortableItem = {
init: (element, fValueAccessor) => {
let options = ko.unwrap(fValueAccessor()) || {},
parent = element.parentNode,
fnHover = e => {
if ('sortable' === getDragAction(e)) {
setDragAction(e, 'messages', e.ctrlKey ? 'copy' : 'move', data, dragImage);
// Remove the Chrome visibility
dragImage.style.cssText = '';
} else {
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();
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);
}
}, false);
element.addEventListener("dragend", () => dragData = null);
element.setAttribute('draggable', true);
}
},
// 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 = {
init: (element, fValueAccessor) => fValueAccessor()(element)
};
ko.bindingHandlers.onEsc = {
init: (element, fValueAccessor, fAllBindingsAccessor, viewModel) => {
let fn = event => {
if ('Escape' == event.key) {
element.dispatchEvent(new Event('change'));
fValueAccessor().call(viewModel);
sortableItem: {
init: (element, fValueAccessor) => {
let options = ko.unwrap(fValueAccessor()) || {},
parent = element.parentNode,
fnHover = e => {
if ('sortable' === getDragAction(e)) {
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();
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 = {
init: element => {
rl.Dropdowns.register(element);
element.ddBtn = new BSN.Dropdown(element.querySelector('[data-toggle="dropdown"]'));
}
};
initDom: {
init: (element, fValueAccessor) => fValueAccessor()(element)
},
ko.bindingHandlers.openDropdownTrigger = {
update: (element, fValueAccessor) => {
if (ko.unwrap(fValueAccessor())) {
const el = element.ddBtn;
el.open || el.toggle();
// el.focus();
registerBootstrapDropdown: {
init: element => {
dropdowns.push(element);
element.ddBtn = new BSN.Dropdown(element.querySelector('.dropdown-toggle'));
}
},
rl.Dropdowns.detectVisibility();
fValueAccessor()(false);
openDropdownTrigger: {
update: (element, fValueAccessor) => {
if (ko.unwrap(fValueAccessor())) {
const el = element.ddBtn;
el.open || el.toggle();
// el.focus();
dropdownsDetectVisibility();
fValueAccessor()(false);
}
}
}
};
ko.bindingHandlers.dropdownCloser = {
init: element => element.closest('.dropdown').addEventListener('click', event =>
event.target.closestWithin('.e-item', element) && element.ddBtn.toggle()
)
};
});

View file

@ -1,37 +1,36 @@
(doc => {
let visible = "visible",
let idle = 'idle',
visible = 'visible',
status = visible,
timer = 0,
wakeUp = () => {
clearTimeout(timer);
if (status !== visible) {
status = visible;
}
status = visible;
timer = setTimeout(() => {
if (status === visible) {
status = "idle";
dispatchEvent(new CustomEvent("idle"));
status = idle;
dispatchEvent(new CustomEvent(idle));
}
}, 10000);
},
init = () => {
init = ()=>{};
init = () => 0;
// Safari
addEventListener('pagehide', () => status = "hidden");
addEventListener('pagehide', () => status = 'hidden');
// Else
doc.addEventListener("visibilitychange", () => {
doc.addEventListener('visibilitychange', () => {
status = doc.visibilityState;
doc.hidden || wakeUp();
});
wakeUp();
["mousemove","keyup","touchstart"].forEach(t => doc.addEventListener(t, wakeUp));
["scroll","pageshow"].forEach(t => addEventListener(t, wakeUp));
['mousemove','keyup','touchstart'].forEach(t => doc.addEventListener(t, wakeUp));
['scroll','pageshow'].forEach(t => addEventListener(t, wakeUp));
};
this.ifvisible = {
idle: callback => {
init();
addEventListener("idle", callback);
addEventListener(idle, callback);
},
now: () => {
init();

280
dev/External/ko.js vendored
View file

@ -1,157 +1,171 @@
import ko from 'ko';
import { i18nToNodes } from 'Common/Translator';
import { doc, createElement } from 'Common/Globals';
import { SaveSettingsStep } from 'Common/Enums';
import { isNonEmptyArray, isFunction } from 'Common/Utils';
import { arrayLength, isFunction, forEachObjectEntry } from 'Common/Utils';
const
koValue = value => !ko.isObservable(value) && isFunction(value) ? value() : ko.unwrap(value);
export const
errorTip = (element, value) => value
? setTimeout(() => element.setAttribute('data-rainloopErrorTip', value), 100)
: element.removeAttribute('data-rainloopErrorTip'),
ko.bindingHandlers.tooltipErrorTip = {
init: element => {
doc.addEventListener('click', () => element.removeAttribute('data-rainloopErrorTip'));
},
update: (element, fValueAccessor) => {
const value = koValue(fValueAccessor());
if (value) {
setTimeout(() => element.setAttribute('data-rainloopErrorTip', value), 100);
} else {
element.removeAttribute('data-rainloopErrorTip');
/**
* The value of the pureComputed observable shouldnt vary based on the
* number of evaluations or other hidden information. Its value should be
* based solely on the values of other observables in the application
*/
koComputable = fn => ko.computed(fn, {'pure':true}),
addObservablesTo = (target, observables) =>
forEachObjectEntry(observables, (key, value) =>
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 = {
init: (element, fValueAccessor, fAllBindingsAccessor, viewModel) => {
let fn = event => {
if ('Enter' == event.key) {
element.dispatchEvent(new Event('change'));
fValueAccessor().call(viewModel);
onEnter: {
init: (element, fValueAccessor, fAllBindings, viewModel) => {
let fn = event => {
if ('Enter' == event.key) {
element.dispatchEvent(new Event('change'));
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 = {
init: (element, fValueAccessor, fAllBindingsAccessor, viewModel) => {
let fn = event => {
if (' ' == event.key) {
fValueAccessor().call(viewModel, event);
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.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, fAllBindingsAccessor, 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,
fAllBindingsAccessor,
viewModel,
bindingContext
);
},
update: (element, fValueAccessor) => {
const cl = element.classList,
command = fValueAccessor();
let disabled = !command.enabled();
disabled = disabled || !command.canExecute();
cl.toggle('disabled', disabled);
if (element.matches('INPUT,TEXTAREA,BUTTON')) {
element.disabled = disabled;
}
}
};
ko.bindingHandlers.saveTrigger = {
init: (element) => {
let icon = element;
if (element.matches('input,select,textarea')) {
element.classList.add('settings-saved-trigger-input');
element.after(element.saveTriggerIcon = icon = createElement('span'));
}
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);
saveTrigger: {
init: (element) => {
let icon = element;
if (element.matches('input,select,textarea')) {
element.classList.add('settings-saved-trigger-input');
element.after(element.saveTriggerIcon = icon = createElement('span'));
}
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('error', value === SaveSettingsStep.FalseResult);
}
cl = element.classList;
cl.toggle('success', value === SaveSettingsStep.TrueResult);
cl.toggle('error', value === SaveSettingsStep.FalseResult);
}
cl = element.classList;
cl.toggle('success', value === SaveSettingsStep.TrueResult);
cl.toggle('error', value === SaveSettingsStep.FalseResult);
}
};
});
// extenders
ko.extenders.limitedList = (target, limitedList) => {
const result = ko
.computed({
read: target,
write: (newValue) => {
const currentValue = ko.unwrap(target),
list = ko.unwrap(limitedList);
if (isNonEmptyArray(list)) {
if (list.includes(newValue)) {
target(newValue);
} else if (list.includes(currentValue, list)) {
target(currentValue + ' ');
target(currentValue);
} else {
target(list[0] + ' ');
target(list[0]);
}
} else {
target('');
}
.computed({
read: target,
write: newValue => {
let currentValue = target(),
list = ko.unwrap(limitedList);
list = arrayLength(list) ? list : [''];
if (!list.includes(newValue)) {
newValue = list.includes(currentValue, list) ? currentValue : list[0];
target(newValue + ' ');
}
})
.extend({ notify: 'always' });
target(newValue);
}
})
.extend({ notify: 'always' });
result(target());
@ -184,6 +198,6 @@ ko.extenders.falseTimeout = (target, option) => {
// functions
ko.observable.fn.deleteAccessHelper = function() {
return this.extend({ falseTimeout: 3000, toggleSubscribeProperty: [this, 'deleteAccess'] });
ko.observable.fn.askDeleteHelper = function() {
return this.extend({ falseTimeout: 3000, toggleSubscribeProperty: [this, 'askDelete'] });
};

View file

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

View file

@ -1,23 +1,20 @@
import { isArray, isFunction, addObservablesTo, addComputablesTo } from 'Common/Utils';
function dispose(disposable) {
if (disposable && isFunction(disposable.dispose)) {
disposable.dispose();
}
}
import { isArray, forEachObjectValue, forEachObjectEntry } from 'Common/Utils';
import { dispose, addObservablesTo, addComputablesTo } from 'External/ko';
function typeCast(curValue, newValue) {
switch (typeof curValue)
{
case 'boolean': return 0 != newValue && !!newValue;
case 'number': return isFinite(newValue) ? parseFloat(newValue) : 0;
case 'string': return null != newValue ? '' + newValue : '';
case 'object':
if (curValue.constructor.reviveFromJson) {
return curValue.constructor.reviveFromJson(newValue);
if (null != curValue) {
switch (typeof curValue)
{
case 'boolean': return 0 != newValue && !!newValue;
case 'number': return isFinite(newValue) ? parseFloat(newValue) : 0;
case 'string': return null != newValue ? '' + newValue : '';
case 'object':
if (curValue.constructor.reviveFromJson) {
return curValue.constructor.reviveFromJson(newValue);
}
if (isArray(curValue) && !isArray(newValue))
return [];
}
if (isArray(curValue) && !isArray(newValue))
return [];
}
return newValue;
}
@ -29,7 +26,7 @@ export class AbstractModel {
throw new Error("Can't instantiate AbstractModel!");
}
*/
this.subscribables = [];
this.disposables = [];
}
addObservables(observables) {
@ -41,25 +38,26 @@ export class AbstractModel {
}
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 */
onDestroy() {
/** dispose ko subscribables */
this.subscribables.forEach(dispose);
this.disposables.forEach(dispose);
/** clear object entries */
// Object.entries(this).forEach(([key, value]) => {
Object.values(this).forEach(value => {
// forEachObjectEntry(this, (key, value) => {
forEachObjectValue(this, value => {
/** clear CollectionModel */
let arr = ko.isObservableArray(value) ? value() : value;
arr && arr.onDestroy && value.onDestroy();
arr && arr.onDestroy && arr.onDestroy();
/** destroy ko.observable/ko.computed? */
dispose(value);
// dispose(value);
/** clear object value */
// this[key] = null; // TODO: issue with Contacts view
});
// this.subscribables = [];
// this.disposables = [];
}
/**
@ -84,39 +82,37 @@ export class AbstractModel {
revivePropertiesFromJson(json) {
let model = this.constructor;
try {
if (!model.validJson(json)) {
return false;
}
Object.entries(json).forEach(([key, value]) => {
if ('@' !== key[0]) {
key = key[0].toLowerCase() + key.substr(1);
switch (typeof this[key])
{
case 'function':
if (ko.isObservable(this[key])) {
this[key](typeCast(this[key](), value));
// console.log('Observable ' + (typeof this[key]()) + ' ' + (model.name) + '.' + key + ' revived');
}
// else console.log(model.name + '.' + key + ' is a function');
break;
case 'boolean':
case 'number':
case 'object':
case 'string':
this[key] = typeCast(this[key], value);
break;
// fall through
case 'undefined':
default:
// console.log((typeof this[key])+' '+(model.name)+'.'+key+' not revived');
}
}
});
} catch (e) {
console.log(model.name);
console.error(e);
if (!model.validJson(json)) {
return false;
}
forEachObjectEntry(json, (key, value) => {
if ('@' !== key[0]) try {
key = key[0].toLowerCase() + key.slice(1);
switch (typeof this[key])
{
case 'function':
if (ko.isObservable(this[key])) {
this[key](typeCast(this[key](), value));
// console.log('Observable ' + (typeof this[key]()) + ' ' + (model.name) + '.' + key + ' revived');
}
// else console.log(model.name + '.' + key + ' is a function');
break;
case 'boolean':
case 'number':
case 'object':
case 'string':
this[key] = typeCast(this[key], value);
break;
// fall through
case 'undefined':
default:
// console.log((typeof this[key])+' '+(model.name)+'.'+key+' not revived');
}
} catch (e) {
console.log(model.name + '.' + key);
console.error(e);
}
});
return true;
}

View file

@ -1,24 +1,10 @@
import { isArray, isNonEmptyArray } from 'Common/Utils';
import { isArray, arrayLength } from 'Common/Utils';
export class AbstractScreen {
constructor(screenName, viewModels = []) {
this.oCross = null;
this.sScreenName = screenName;
this.aViewModels = isArray(viewModels) ? viewModels : [];
}
/**
* @returns {Array}
*/
get viewModels() {
return this.aViewModels;
}
/**
* @returns {string}
*/
screenName() {
return this.sScreenName;
this.__cross = null;
this.screenName = screenName;
this.viewModels = isArray(viewModels) ? viewModels : [];
}
/**
@ -28,28 +14,26 @@ export class AbstractScreen {
return null;
}
/**
* @returns {?Object}
*/
get __cross() {
return this.oCross;
}
/*
onBuild(viewModelDom) {}
onShow() {}
onHide() {}
__started
__builded
*/
/**
* @returns {void}
*/
onStart() {
if (!this.__started) {
this.__started = true;
const routes = this.routes();
if (isNonEmptyArray(routes)) {
let route = new Crossroads(),
fMatcher = (this.onRoute || (()=>{})).bind(this);
const routes = this.routes();
if (arrayLength(routes)) {
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,45 +1,38 @@
import ko from 'ko';
import { inFocus, addObservablesTo, addComputablesTo, addSubscribablesTo } from 'Common/Utils';
import { Scope } from 'Common/Enums';
import { keyScope } from 'Common/Globals';
import { ViewType } from 'Knoin/Knoin';
import { addObservablesTo, addComputablesTo, addSubscribablesTo } from 'External/ko';
import { keyScope, SettingsGet, leftPanelDisabled } from 'Common/Globals';
import { ViewTypePopup, showScreenPopup } from 'Knoin/Knoin';
import { SaveSettingsStep } from 'Common/Enums';
class AbstractView {
constructor(name, templateID, type)
constructor(templateID, type)
{
this.viewModelName = 'View/' + name;
this.viewModelTemplateID = templateID;
this.viewModelPosition = 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 = '';
// Object.defineProperty(this, 'viewModelTemplateID', { value: templateID });
this.viewModelTemplateID = templateID || this.constructor.name.replace('UserView', '');
this.viewType = type;
this.viewModelDom = null;
this.keyScope = {
scope: 'none',
previous: 'none',
set: function() {
this.previous = keyScope();
keyScope(this.scope);
},
unset: function() {
keyScope(this.previous);
}
};
}
/**
* @returns {void}
*/
storeAndSetScope() {
this.sCurrentScope = keyScope();
keyScope(this.sDefaultScope);
}
/**
* @returns {void}
*/
restoreScope() {
keyScope(this.sCurrentScope);
}
cancelCommand() {}
closeCommand() {}
/*
onBuild() {}
beforeShow() {} // Happens before: hidden = false
onShow() {} // Happens after: hidden = false
onHide() {}
*/
querySelector(selectors) {
return this.viewModelDom.querySelector(selectors);
@ -63,51 +56,115 @@ export class AbstractViewPopup extends AbstractView
{
constructor(name)
{
super('Popup/' + name, 'Popups' + name, ViewType.Popup);
if (name in Scope) {
this.sDefaultScope = Scope[name];
}
}
/**
* @returns {void}
*/
registerPopupKeyDown() {
addEventListener('keydown', event => {
if (event && this.modalVisibility && this.modalVisibility()) {
if (!this.bDisabeCloseOnEsc && 'Escape' == event.key) {
this.cancelCommand && this.cancelCommand();
return false;
} else if ('Backspace' == event.key && !inFocus()) {
return false;
}
super('Popups' + name, ViewTypePopup);
this.keyScope.scope = name;
this.modalVisible = ko.observable(false).extend({ rateLimit: 0 });
shortcuts.add('escape,close', '', name, () => {
if (this.modalVisible() && false !== this.onClose()) {
this.close();
return false;
}
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
{
constructor(name, templateID)
{
super(name, templateID, ViewType.Center);
}
AbstractViewPopup.showModal = function(params = []) {
showScreenPopup(this, params);
}
AbstractViewPopup.hidden = function() {
return !this.__vm || !this.__vm.modalVisible();
}
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
{
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,370 +1,310 @@
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';
import { runHook } from 'Common/Plugins';
import { isNonEmptyArray, isFunction } from 'Common/Utils';
let currentScreen = null,
let
SCREENS = {},
currentScreen = null,
defaultScreenName = '';
const SCREENS = {},
const
autofocus = dom => {
const af = dom.querySelector('[autofocus]');
af && af.focus();
};
},
export const popupVisibilityNames = ko.observableArray([]);
visiblePopups = new Set,
export const ViewType = {
Popup: 'Popups',
Left: 'Left',
Right: 'Right',
Center: 'Center'
};
/**
* @param {string} screenName
* @returns {?Object}
*/
screen = screenName => screenName && null != SCREENS[screenName] ? SCREENS[screenName] : null,
/**
* @param {Function} fExecute
* @param {(Function|boolean|null)=} fCanExecute = true
* @returns {Function}
*/
export function createCommand(fExecute, fCanExecute = true) {
let fResult = null;
/**
* @param {Function} ViewModelClass
* @param {Object=} vmScreen
* @returns {*}
*/
buildViewModel = (ViewModelClass, vmScreen) => {
if (ViewModelClass && !ViewModelClass.__builded) {
let vmDom = null;
const
vm = new ViewModelClass(vmScreen),
id = vm.viewModelTemplateID,
position = vm.viewType || '',
dialog = ViewTypePopup === position,
vmPlace = position ? doc.getElementById('rl-' + position.toLowerCase()) : null;
fResult = fExecute
? (...args) => {
if (fResult && fResult.canExecute && fResult.canExecute()) {
fExecute.apply(null, args);
}
return false;
} : ()=>{};
fResult.enabled = ko.observable(true);
fResult.isCommand = true;
ViewModelClass.__builded = true;
ViewModelClass.__vm = vm;
if (isFunction(fCanExecute)) {
fResult.canExecute = ko.computed(() => fResult && fResult.enabled() && fCanExecute.call(null));
} else {
fResult.canExecute = ko.computed(() => fResult && fResult.enabled() && !!fCanExecute);
}
if (vmPlace) {
vmDom = Element.fromHTML(dialog
? '<dialog id="V-'+ id + '"></dialog>'
: '<div id="V-'+ id + '" hidden=""></div>');
vmPlace.append(vmDom);
return fResult;
}
vm.viewModelDom = vmDom;
ViewModelClass.__dom = vmDom;
/**
* @param {string} screenName
* @returns {?Object}
*/
function screen(screenName) {
return screenName && null != SCREENS[screenName] ? SCREENS[screenName] : null;
}
if (ViewTypePopup === position) {
vm.close = () => hideScreenPopup(ViewModelClass);
/**
* @param {Function} ViewModelClassToHide
* @returns {void}
*/
export function hideScreenPopup(ViewModelClassToHide) {
if (ViewModelClassToHide && ViewModelClassToHide.__vm && ViewModelClassToHide.__dom) {
ViewModelClassToHide.__vm.modalVisibility(false);
}
}
// 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;
};
}
/**
* @param {Function} ViewModelClass
* @param {Object=} vmScreen
* @returns {*}
*/
function buildViewModel(ViewModelClass, vmScreen) {
if (ViewModelClass && !ViewModelClass.__builded) {
let vmDom = null;
const vm = new ViewModelClass(vmScreen),
position = vm.viewModelPosition || '',
vmPlace = position ? doc.querySelector('#rl-content #rl-' + position.toLowerCase()) : null;
ViewModelClass.__builded = true;
ViewModelClass.__vm = vm;
if (vmPlace) {
vmDom = Element.fromHTML('<div class="rl-view-model RL-' + vm.viewModelTemplateID + '" hidden=""></div>');
vmPlace.append(vmDom);
vm.viewModelDom = vmDom;
ViewModelClass.__dom = vmDom;
if (ViewType.Popup === position) {
vm.cancelCommand = vm.closeCommand = createCommand(() => {
hideScreenPopup(ViewModelClass);
});
// show/hide popup/modal
const endShowHide = e => {
if (e.target === vmDom) {
if (vmDom.classList.contains('show')) {
autofocus(vmDom);
vm.onShowWithDelay && vm.onShowWithDelay();
} else {
vmDom.hidden = true;
vm.onHideWithDelay && vm.onHideWithDelay();
// show/hide popup/modal
const endShowHide = e => {
if (e.target === vmDom) {
if (vmDom.classList.contains('animate')) {
autofocus(vmDom);
vm.afterShow && vm.afterShow();
} else {
vmDom.close();
vm.afterHide && vm.afterHide();
}
}
}
};
};
vm.modalVisibility.subscribe(value => {
if (value) {
vmDom.style.zIndex = 3000 + popupVisibilityNames().length + 10;
vmDom.hidden = false;
vm.storeAndSetScope();
popupVisibilityNames.push(vm.viewModelName);
requestAnimationFrame(() => { // wait just before the next paint
vmDom.offsetHeight; // force a reflow
vmDom.classList.add('show'); // trigger the transitions
});
} else {
vm.onHide && vm.onHide();
vmDom.classList.remove('show');
vm.restoreScope();
popupVisibilityNames(popupVisibilityNames.filter(v=>v!==vm.viewModelName));
}
vmDom.setAttribute('aria-hidden', !value);
});
if ('ontransitionend' in vmDom) {
vm.modalVisible.subscribe(value => {
if (value) {
visiblePopups.add(vm);
vmDom.style.zIndex = 3000 + (visiblePopups.size * 2);
vmDom.showModal();
if (vmDom.backdrop) {
vmDom.backdrop.style.zIndex = 3000 + visiblePopups.size;
}
vm.keyScope.set();
requestAnimationFrame(() => { // wait just before the next paint
vmDom.offsetHeight; // force a reflow
vmDom.classList.add('animate'); // trigger the transitions
});
} else {
visiblePopups.delete(vm);
vm.onHide && vm.onHide();
vm.keyScope.unset();
vmDom.classList.remove('animate'); // trigger the transitions
}
arePopupsVisible(0 < visiblePopups.size);
});
vmDom.addEventListener('transitionend', endShowHide);
} else {
// For Edge < 79 and mobile browsers
vm.modalVisibility.subscribe(() => ()=>setTimeout(endShowHide({target:vmDom}), 500));
}
}
ko.applyBindingAccessorsToNode(
vmDom,
{
i18nInit: true,
template: () => ({ name: vm.viewModelTemplateID })
},
vm
);
vm.onBuild && vm.onBuild(vmDom);
if (vm && ViewType.Popup === position) {
vm.registerPopupKeyDown();
}
} else {
console.log('Cannot find view model position: ' + position);
}
}
return ViewModelClass && ViewModelClass.__vm;
}
function getScreenPopupViewModel(ViewModelClassToShow) {
return (buildViewModel(ViewModelClassToShow) && ViewModelClassToShow.__dom) && ViewModelClassToShow.__vm;
}
/**
* @param {Function} ViewModelClassToShow
* @param {Array=} params
* @returns {void}
*/
export function showScreenPopup(ViewModelClassToShow, params = []) {
const vm = getScreenPopupViewModel(ViewModelClassToShow);
if (vm) {
params = params || [];
vm.onBeforeShow && vm.onBeforeShow(...params);
vm.modalVisibility(true);
vm.onShow && vm.onShow(...params);
runHook('view-model-on-show', [vm.name, vm.__vm, params]);
}
}
/**
* @param {Function} ViewModelClassToShow
* @returns {void}
*/
export function warmUpScreenPopup(ViewModelClassToShow) {
const vm = getScreenPopupViewModel(ViewModelClassToShow);
vm && vm.onWarmUp && vm.onWarmUp();
}
/**
* @param {Function} ViewModelClassToShow
* @returns {boolean}
*/
export function isPopupVisible(ViewModelClassToShow) {
return ViewModelClassToShow && ViewModelClassToShow.__vm && ViewModelClassToShow.__vm.modalVisibility();
}
/**
* @param {string} screenName
* @param {string} subPart
* @returns {void}
*/
function screenOnRoute(screenName, subPart) {
let vmScreen = null,
isSameScreen = false;
if (null == screenName || '' == screenName) {
screenName = defaultScreenName;
}
if (screenName) {
vmScreen = screen(screenName);
if (!vmScreen) {
vmScreen = screen(defaultScreenName);
if (vmScreen) {
subPart = screenName + '/' + subPart;
screenName = defaultScreenName;
}
}
if (vmScreen && vmScreen.__started) {
isSameScreen = currentScreen && vmScreen === currentScreen;
if (!vmScreen.__builded) {
vmScreen.__builded = true;
vmScreen.viewModels.forEach(ViewModelClass =>
buildViewModel(ViewModelClass, vmScreen)
ko.applyBindingAccessorsToNode(
vmDom,
{
i18nInit: true,
template: () => ({ name: id })
},
vm
);
vmScreen.onBuild && vmScreen.onBuild();
vm.onBuild && vm.onBuild(vmDom);
fireEvent('rl-view-model', vm);
} else {
console.log('Cannot find view model position: ' + position);
}
}
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} subPart
* @returns {void}
*/
screenOnRoute = (screenName, subPart) => {
let vmScreen = null,
isSameScreen = false;
if (null == screenName || '' == screenName) {
screenName = defaultScreenName;
}
// Close all popups
for (let vm of visiblePopups) {
false === vm.onClose() || vm.close();
}
if (screenName) {
vmScreen = screen(screenName);
if (!vmScreen) {
vmScreen = screen(defaultScreenName);
if (vmScreen) {
subPart = screenName + '/' + subPart;
screenName = defaultScreenName;
}
}
setTimeout(() => {
// hide screen
if (currentScreen && !isSameScreen) {
currentScreen.onHide && currentScreen.onHide();
currentScreen.onHideWithDelay && setTimeout(()=>currentScreen.onHideWithDelay(), 500);
if (vmScreen && vmScreen.__started) {
isSameScreen = currentScreen && vmScreen === currentScreen;
if (isNonEmptyArray(currentScreen.viewModels)) {
currentScreen.viewModels.forEach(ViewModelClass => {
if (
ViewModelClass.__vm &&
ViewModelClass.__dom &&
ViewType.Popup !== ViewModelClass.__vm.viewModelPosition
) {
ViewModelClass.__dom.hidden = true;
ViewModelClass.__vm.viewModelVisible = false;
if (!vmScreen.__builded) {
vmScreen.__builded = true;
ViewModelClass.__vm.onHide && ViewModelClass.__vm.onHide();
ViewModelClass.__vm.onHideWithDelay && setTimeout(()=>ViewModelClass.__vm.onHideWithDelay(), 500);
}
vmScreen.viewModels.forEach(ViewModelClass =>
buildViewModel(ViewModelClass, vmScreen)
);
vmScreen.onBuild && vmScreen.onBuild();
}
setTimeout(() => {
// hide screen
if (currentScreen && !isSameScreen) {
hideScreen(currentScreen);
}
// --
currentScreen = vmScreen;
// show screen
if (!isSameScreen) {
vmScreen.onShow && vmScreen.onShow();
forEachViewModel(vmScreen, (vm, dom) => {
vm.beforeShow && vm.beforeShow();
dom.hidden = false;
vm.onShow && vm.onShow();
autofocus(dom);
});
}
}
// --
// --
currentScreen = vmScreen;
// show screen
if (currentScreen && !isSameScreen) {
currentScreen.onShow && currentScreen.onShow();
if (isNonEmptyArray(currentScreen.viewModels)) {
currentScreen.viewModels.forEach(ViewModelClass => {
if (
ViewModelClass.__vm &&
ViewModelClass.__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);
runHook('view-model-on-show', [ViewModelClass.name, ViewModelClass.__vm, params]);
}
});
}
}
// --
vmScreen && vmScreen.__cross && vmScreen.__cross.parse(subPart);
}, 1);
vmScreen.__cross && vmScreen.__cross.parse(subPart);
}, 1);
}
}
}
}
};
/**
* @param {Array} screensClasses
* @returns {void}
*/
export function startScreens(screensClasses) {
screensClasses.forEach(CScreen => {
if (CScreen) {
const vmScreen = new CScreen(),
screenName = vmScreen && vmScreen.screenName();
if (screenName) {
export const
ViewTypePopup = 'Popups',
/**
* @param {Function} ViewModelClassToShow
* @param {Array=} params
* @returns {void}
*/
showScreenPopup = (ViewModelClassToShow, params = []) => {
const vm = buildViewModel(ViewModelClassToShow) && ViewModelClassToShow.__dom && ViewModelClassToShow.__vm;
if (vm) {
params = params || [];
vm.beforeShow && vm.beforeShow(...params);
vm.modalVisible(true);
vm.onShow && vm.onShow(...params);
}
},
arePopupsVisible = ko.observable(false),
/**
* @param {Array} screensClasses
* @returns {void}
*/
startScreens = screensClasses => {
hasher.clear();
forEachObjectValue(SCREENS, screen => hideScreen(screen, 1));
SCREENS = {};
currentScreen = null,
defaultScreenName = '';
screensClasses.forEach(CScreen => {
if (CScreen) {
const vmScreen = new CScreen(),
screenName = vmScreen.screenName;
defaultScreenName || (defaultScreenName = screenName);
SCREENS[screenName] = vmScreen;
}
}
});
});
Object.values(SCREENS).forEach(vmScreen =>
vmScreen && vmScreen.onStart && vmScreen.onStart()
);
const cross = new Crossroads();
cross.addRoute(/^([a-zA-Z0-9-]*)\/?(.*)$/, screenOnRoute);
hasher.changed.add(cross.parse.bind(cross));
hasher.init();
setTimeout(() => $htmlCL.remove('rl-started-trigger'), 100);
setTimeout(() => $htmlCL.add('rl-started-delay'), 200);
}
function decorateKoCommands(thisArg, commands) {
Object.entries(commands).forEach(([key, canExecute]) => {
let command = thisArg[key],
fn = (...args) => fn.enabled() && fn.canExecute() && command.apply(thisArg, args);
// fn.__realCanExecute = canExecute;
// 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;
});
}
ko.decorateCommands = decorateKoCommands;
/**
* @param {miced} $items
* @returns {Function}
*/
function settingsMenuKeysHandler(items) {
return ((event, handler)=>{
let index = items.length;
if (event && index) {
while (index-- && !items[index].matches('.selected'));
if (handler && 'arrowup' === handler.shortcut) {
index && --index;
} else if (index < items.length - 1) {
++index;
forEachObjectValue(SCREENS, vmScreen => {
if (!vmScreen.__started) {
vmScreen.onStart();
vmScreen.__started = true;
}
});
const resultHash = items[index].href;
resultHash && rl.route.setHash(resultHash, false, true);
}
}).throttle(200);
}
const cross = new Crossroads();
cross.addRoute(/^([a-zA-Z0-9-]*)\/?(.*)$/, screenOnRoute);
export {
decorateKoCommands,
settingsMenuKeysHandler
};
hasher.add(cross.parse.bind(cross));
hasher.init();
setTimeout(() => $htmlCL.remove('rl-started-trigger'), 100);
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) =>
forEachObjectEntry(commands, (key, canExecute) => {
let command = thisArg[key],
fn = (...args) => fn.canExecute() && command.apply(thisArg, args);
fn.canExecute = koComputable(() => canExecute.call(thisArg, thisArg));
thisArg[key] = fn;
});
ko.decorateCommands = decorateKoCommands;

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

View file

@ -1,5 +1,3 @@
import { change } from 'Common/Links';
import { AbstractModel } from 'Knoin/AbstractModel';
export class AccountModel extends AbstractModel {
@ -8,23 +6,16 @@ export class AccountModel extends AbstractModel {
* @param {boolean=} canBeDelete = true
* @param {number=} count = 0
*/
constructor(email, canBeDelete = true, count = 0) {
constructor(email/*, count = 0*/, isAdditional = true) {
super();
this.email = email;
this.addObservables({
count: count,
deleteAccess: false,
canBeDeleted: !!canBeDelete
// count: count || 0,
askDelete: false,
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.fileType = FileType.Unknown;
this.friendlySize = '';
this.isInline = false;
this.isLinked = false;
this.isThumbnail = false;
this.cid = '';
this.cidWithoutTags = '';
this.contentLocation = '';
this.download = '';
this.folder = '';
this.uid = '';
this.url = '';
this.mimeIndex = '';
this.framed = false;
this.addObservables({
isInline: false,
isLinked: false
});
}
/**
@ -43,7 +46,6 @@ export class AttachmentModel extends AbstractModel {
const attachment = super.reviveFromJson(json);
if (attachment) {
attachment.friendlySize = FileInfo.friendlySize(json.EstimatedSize);
attachment.cidWithoutTags = attachment.cid.replace(/^<+/, '').replace(/>+$/, '');
attachment.fileNameExt = FileInfo.getExtension(attachment.fileName);
attachment.fileType = FileInfo.getType(attachment.fileNameExt, attachment.mimeType);
@ -51,6 +53,10 @@ export class AttachmentModel extends AbstractModel {
return attachment;
}
contentId() {
return this.cid.replace(/^<+|>+$/g, '');
}
/**
* @returns {boolean}
*/
@ -122,29 +128,21 @@ export class AttachmentModel extends AbstractModel {
* @returns {string}
*/
linkDownload() {
return attachmentDownload(this.download);
return this.url || attachmentDownload(this.download);
}
/**
* @returns {string}
*/
linkPreview() {
return serverRequestRaw('View', this.download);
}
/**
* @returns {string}
*/
linkThumbnail() {
return this.hasThumbnail() ? serverRequestRaw('ViewThumbnail', this.download) : '';
return this.url || serverRequestRaw('View', this.download);
}
/**
* @returns {string}
*/
linkThumbnailPreviewStyle() {
const link = this.linkThumbnail();
return link ? 'background:url(' + link + ')' : '';
return this.hasThumbnail() ? 'background:url(' + serverRequestRaw('ViewThumbnail', this.download) + ')' : '';
}
/**
@ -175,7 +173,7 @@ export class AttachmentModel extends AbstractModel {
const localEvent = event.originalEvent || event;
if (attachment && localEvent && localEvent.dataTransfer && localEvent.dataTransfer.setData) {
let link = this.linkDownload();
if ('http' !== link.substr(0, 4)) {
if ('http' !== link.slice(0, 4)) {
link = location.protocol + '//' + location.host + location.pathname + link;
}
localEvent.dataTransfer.setData('DownloadURL', this.mimeType + ':' + this.fileName + ':' + link);

View file

@ -11,13 +11,20 @@ export class AttachmentCollectionModel extends AbstractCollectionModel
*/
static reviveFromJson(items) {
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}
*/
hasVisible() {
return !!this.find(item => !item.isLinked);
return !!this.filter(item => !item.isLinked()).length;
}
/**
@ -25,7 +32,7 @@ export class AttachmentCollectionModel extends AbstractCollectionModel
* @returns {*}
*/
findByCid(cid) {
cid = cid.replace(/^<+|>+$/, '');
return this.find(item => cid === item.cidWithoutTags);
cid = cid.replace(/^<+|>+$/g, '');
return this.find(item => cid === item.contentId());
}
}

View file

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

View file

@ -1,4 +1,4 @@
import { isNonEmptyArray } from 'Common/Utils';
import { arrayLength } from 'Common/Utils';
import { ContactPropertyModel, ContactPropertyType } from 'Model/ContactProperty';
import { AbstractModel } from 'Knoin/AbstractModel';
@ -26,7 +26,7 @@ export class ContactModel extends AbstractModel {
let name = '',
email = '';
if (isNonEmptyArray(this.properties)) {
if (arrayLength(this.properties)) {
this.properties.forEach(property => {
if (property) {
if (ContactPropertyType.FirstName === property.type()) {
@ -52,7 +52,7 @@ export class ContactModel extends AbstractModel {
const contact = super.reviveFromJson(json);
if (contact) {
let list = [];
if (isNonEmptyArray(json.properties)) {
if (arrayLength(json.properties)) {
json.properties.forEach(property => {
property = ContactPropertyModel.reviveFromJson(property);
property && list.push(property);

View file

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

View file

@ -1,42 +1,112 @@
import { AbstractCollectionModel } from 'Model/AbstractCollection';
import { UNUSED_OPTION_VALUE } from 'Common/Consts';
import { isArray, pInt } from 'Common/Utils';
import { ClientSideKeyName, FolderType } from 'Common/EnumsUser';
import * as Cache from 'Common/Cache';
import { Settings, SettingsGet } from 'Common/Globals';
import { isArray, getKeyByValue, forEachObjectEntry, b64EncodeJSONSafe } from 'Common/Utils';
import { ClientSideKeyNameExpandedFolders, FolderType, FolderMetadataKeys } from 'Common/EnumsUser';
import { getFolderFromCacheList, setFolder, setFolderInboxName, setFolderHash } from 'Common/Cache';
import { Settings, SettingsGet, fireEvent } from 'Common/Globals';
import * as Local from 'Storage/Client';
import { AppUserStore } from 'Stores/User/App';
import { FolderUserStore } from 'Stores/User/Folder';
import { MessagelistUserStore } from 'Stores/User/Messagelist';
import { SettingsUserStore } from 'Stores/User/Settings';
import ko from 'ko';
import { isPosNumeric } from 'Common/UtilsUser';
import { sortFolders } from 'Common/Folders';
import { i18n, trigger as translatorTrigger } from 'Common/Translator';
import { AbstractModel } from 'Knoin/AbstractModel';
import { koComputable } from 'External/ko';
//import { mailBox } from 'Common/Links';
import Remote from 'Remote/User/Fetch';
const
ServerFolderType = {
USER: 0,
INBOX: 1,
SENT: 2,
DRAFTS: 3,
JUNK: 4,
TRASH: 5,
IMPORTANT: 10,
FLAGGED: 11,
ALL: 12
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
},
normalizeFolder = sFolderFullNameRaw => ('' === sFolderFullNameRaw
|| UNUSED_OPTION_VALUE === sFolderFullNameRaw
|| null !== Cache.getFolderFromCacheList(sFolderFullNameRaw))
? sFolderFullNameRaw
: '';
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
{
@ -44,11 +114,11 @@ export class FolderCollectionModel extends AbstractCollectionModel
constructor() {
super();
this.CountRec
this.FoldersHash
this.IsThreadsSupported
this.Namespace;
this.Optimized
this.SystemFolders
this.Capabilities
}
*/
@ -57,164 +127,162 @@ export class FolderCollectionModel extends AbstractCollectionModel
* @returns {FolderCollectionModel}
*/
static reviveFromJson(object) {
const expandedFolders = Local.get(ClientSideKeyName.ExpandedFolders);
return super.reviveFromJson(object, (oFolder, self) => {
let oCacheFolder = Cache.getFolderFromCacheList(oFolder.FullNameRaw);
/*
if (oCacheFolder) {
oFolder.SubFolders = FolderCollectionModel.reviveFromJson(oFolder.SubFolders);
oFolder.SubFolders && oCacheFolder.subFolders(oFolder.SubFolders);
}
*/
if (!oCacheFolder && (oCacheFolder = FolderModel.reviveFromJson(oFolder))) {
if (oFolder.FullNameRaw == self.SystemFolders[ServerFolderType.INBOX]) {
const expandedFolders = Local.get(ClientSideKeyNameExpandedFolders);
if (object && object.SystemFolders) {
forEachObjectEntry(SystemFolders, key =>
SystemFolders[key] = SettingsGet(key+'Folder') || object.SystemFolders[FolderType[key]]
);
}
const result = super.reviveFromJson(object, oFolder => {
let oCacheFolder = getFolderFromCacheList(oFolder.FullName),
type = FolderType[getKeyByValue(SystemFolders, oFolder.FullName)];
if (!oCacheFolder) {
oCacheFolder = FolderModel.reviveFromJson(oFolder);
if (!oCacheFolder)
return null;
if (1 == type) {
oCacheFolder.type(FolderType.Inbox);
Cache.setFolderInboxName(oFolder.FullNameRaw);
setFolderInboxName(oFolder.FullName);
}
Cache.setFolder(oCacheFolder.fullNameHash, oFolder.FullNameRaw, oCacheFolder);
setFolder(oCacheFolder);
}
if (oCacheFolder) {
oCacheFolder.collapsed(!expandedFolders
|| !isArray(expandedFolders)
|| !expandedFolders.includes(oCacheFolder.fullNameHash));
if (1 < type) {
oCacheFolder.type(type);
}
if (oFolder.Extended) {
if (oFolder.Extended.Hash) {
Cache.setFolderHash(oCacheFolder.fullNameRaw, oFolder.Extended.Hash);
}
oCacheFolder.collapsed(!expandedFolders
|| !isArray(expandedFolders)
|| !expandedFolders.includes(oCacheFolder.fullName));
if (null != oFolder.Extended.MessageCount) {
oCacheFolder.messageCountAll(oFolder.Extended.MessageCount);
}
if (oFolder.Extended) {
if (oFolder.Extended.Hash) {
setFolderHash(oCacheFolder.fullName, oFolder.Extended.Hash);
}
if (null != oFolder.Extended.MessageUnseenCount) {
oCacheFolder.messageCountUnread(oFolder.Extended.MessageUnseenCount);
}
if (null != oFolder.Extended.MessageCount) {
oCacheFolder.messageCountAll(oFolder.Extended.MessageCount);
}
if (null != oFolder.Extended.MessageUnseenCount) {
oCacheFolder.messageCountUnread(oFolder.Extended.MessageUnseenCount);
}
}
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() {
const cnt = pInt(this.CountRec);
FolderUserStore.displaySpecSetting(Settings.app('folderSpecLimit') < this.CountRec);
let limit = pInt(Settings.app('folderSpecLimit'));
limit = 100 < limit ? 100 : 10 > limit ? 10 : limit;
FolderUserStore.displaySpecSetting(0 >= cnt || limit < cnt);
if (!(
SettingsGet('SentFolder') +
SettingsGet('DraftsFolder') +
SettingsGet('SpamFolder') +
SettingsGet('TrashFolder') +
SettingsGet('ArchiveFolder')
)
) {
FolderUserStore.saveSystemFolders(SystemFolders);
}
FolderUserStore.folderList(this);
if (undefined !== this.Namespace) {
FolderUserStore.namespace = this.Namespace;
}
FolderUserStore.namespace = this.Namespace;
AppUserStore.threadsAllowed(!!(Settings.app('useImapThread') && this.IsThreadsSupported));
FolderUserStore.folderListOptimized(!!this.Optimized);
FolderUserStore.sortSupported(!!this.IsSortSupported);
FolderUserStore.quotaUsage(this.quotaUsage);
FolderUserStore.quotaLimit(this.quotaLimit);
FolderUserStore.capabilities(this.Capabilities);
let update = false;
FolderUserStore.sentFolder(normalizeFolder(SystemFolders.Sent));
FolderUserStore.draftsFolder(normalizeFolder(SystemFolders.Drafts));
FolderUserStore.spamFolder(normalizeFolder(SystemFolders.Spam));
FolderUserStore.trashFolder(normalizeFolder(SystemFolders.Trash));
FolderUserStore.archiveFolder(normalizeFolder(SystemFolders.Archive));
if (
this.SystemFolders &&
!('' +
SettingsGet('SentFolder') +
SettingsGet('DraftFolder') +
SettingsGet('SpamFolder') +
SettingsGet('TrashFolder') +
SettingsGet('ArchiveFolder'))
) {
Settings.set('SentFolder', this.SystemFolders[ServerFolderType.SENT] || null);
Settings.set('DraftFolder', this.SystemFolders[ServerFolderType.DRAFTS] || null);
Settings.set('SpamFolder', this.SystemFolders[ServerFolderType.JUNK] || null);
Settings.set('TrashFolder', this.SystemFolders[ServerFolderType.TRASH] || null);
Settings.set('ArchiveFolder', this.SystemFolders[ServerFolderType.ALL] || null);
update = true;
}
FolderUserStore.sentFolder(normalizeFolder(SettingsGet('SentFolder')));
FolderUserStore.draftFolder(normalizeFolder(SettingsGet('DraftFolder')));
FolderUserStore.spamFolder(normalizeFolder(SettingsGet('SpamFolder')));
FolderUserStore.trashFolder(normalizeFolder(SettingsGet('TrashFolder')));
FolderUserStore.archiveFolder(normalizeFolder(SettingsGet('ArchiveFolder')));
if (update) {
rl.app.Remote.saveSystemFolders(()=>{}, {
SentFolder: FolderUserStore.sentFolder(),
DraftFolder: FolderUserStore.draftFolder(),
SpamFolder: FolderUserStore.spamFolder(),
TrashFolder: FolderUserStore.trashFolder(),
ArchiveFolder: FolderUserStore.archiveFolder()
});
}
Local.set(ClientSideKeyName.FoldersLashHash, this.FoldersHash);
// FolderUserStore.folderList.valueHasMutated();
}
}
function getSystemFolderName(type, def)
{
switch (type) {
case FolderType.Inbox:
return i18n('FOLDER_LIST/INBOX_NAME');
case FolderType.SentItems:
return i18n('FOLDER_LIST/SENT_NAME');
case FolderType.Draft:
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 {
constructor() {
super();
this.fullName = '';
this.fullNameRaw = '';
this.fullNameHash = '';
this.delimiter = '';
this.namespace = '';
this.deep = 0;
this.expires = 0;
this.metadata = {};
this.selectable = false;
this.exists = true;
// this.hash = '';
// this.uidNext = 0;
this.addObservables({
name: '',
type: FolderType.User,
selectable: false,
focused: false,
selected: false,
edited: false,
subscribed: true,
checkable: false,
deleteAccess: false,
checkable: false, // Check for new messages
askDelete: false,
nameForEdit: '',
errorMsg: '',
privateMessageCountAll: 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.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
* @param {FetchJsonFolder} json
@ -223,12 +291,22 @@ export class FolderModel extends AbstractModel {
static reviveFromJson(json) {
const folder = super.reviveFromJson(json);
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,
write: (iValue) => {
if (isPosNumeric(iValue, true)) {
if (isPosNumeric(iValue)) {
folder.privateMessageCountAll(iValue);
} else {
folder.privateMessageCountAll.valueHasMutated();
@ -237,10 +315,10 @@ export class FolderModel extends AbstractModel {
})
.extend({ notify: 'always' });
folder.messageCountUnread = ko.computed({
folder.messageCountUnread = koComputable({
read: folder.privateMessageCountUnread,
write: (value) => {
if (isPosNumeric(value, true)) {
if (isPosNumeric(value)) {
folder.privateMessageCountUnread(value);
} else {
folder.privateMessageCountUnread.valueHasMutated();
@ -253,61 +331,68 @@ export class FolderModel extends AbstractModel {
isInbox: () => FolderType.Inbox === folder.type(),
hasSubscribedSubfolders:
() =>
!!folder.subFolders.find(
oFolder => (oFolder.subscribed() || oFolder.hasSubscribedSubfolders()) && !oFolder.isSystemFolder()
),
isFlagged: () => FolderUserStore.currentFolder() === folder
&& MessagelistUserStore.listSearch().includes('flagged'),
canBeEdited: () => FolderType.User === folder.type() && folder.exists && folder.selectable,
hasVisibleSubfolders: () => !!folder.subFolders().find(folder => folder.visible()),
hasSubscriptions: () => folder.subscribed() | !!folder.subFolders().find(
oFolder => {
const subscribed = oFolder.hasSubscriptions();
return !oFolder.isSystemFolder() && subscribed;
}
),
canBeEdited: () => FolderType.User === folder.type() && folder.exists/* && folder.selectable()*/,
isSystemFolder: () => FolderType.User !== folder.type() | !!folder.kolabType(),
canBeSelected: () => folder.selectable() && !folder.isSystemFolder(),
canBeDeleted: () => folder.canBeSelected() && folder.exists,
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 isSubscribed = folder.subscribed(),
isSubFolders = folder.hasSubscribedSubfolders();
return isSubscribed || (isSubFolders && (!folder.exists || !folder.selectable));
const selectable = folder.canBeSelected(),
visible = (folder.subscribed() | !SettingsUserStore.hideUnsubscribed()) && selectable;
return folder.hasVisibleSubfolders() | visible;
},
isSystemFolder: () => FolderType.User !== folder.type(),
hidden: () => {
const isSystem = folder.isSystemFolder(),
isSubFolders = folder.hasSubscribedSubfolders();
return (isSystem && !isSubFolders) || (!folder.selectable && !isSubFolders);
},
hidden: () => !folder.selectable() && (folder.isSystemFolder() | !folder.hasVisibleSubfolders()),
printableUnreadCount: () => {
const count = folder.messageCountAll(),
unread = folder.messageCountUnread(),
type = folder.type();
if (0 < count) {
if (FolderType.Draft === type) {
return '' + count;
if (count) {
if (FolderType.Drafts === type) {
return count;
}
if (
0 < unread &&
unread &&
FolderType.Trash !== type &&
FolderType.Archive !== type &&
FolderType.SentItems !== type
FolderType.Sent !== type
) {
return '' + unread;
return unread;
}
}
return '';
return null;
},
canBeDeleted: () => !folder.isSystemFolder() && !folder.subFolders.length,
canBeSubscribed: () => !folder.isSystemFolder()
&& SettingsUserStore.hideUnsubscribed()
&& folder.selectable
&& Settings.app('useImapSubscribe'),
canBeSelected: () => !folder.isSystemFolder() && folder.selectable,
localName: () => {
let name = folder.name();
if (folder.isSystemFolder()) {
@ -320,28 +405,22 @@ export class FolderModel extends AbstractModel {
manageFolderSystemName: () => {
if (folder.isSystemFolder()) {
translatorTrigger();
let suffix = getSystemFolderName(folder.type(), '');
let suffix = getSystemFolderName(folder.type(), getKolabFolderName(folder.kolabType()));
if (folder.name() !== suffix && 'inbox' !== suffix.toLowerCase()) {
return '(' + suffix + ')';
}
}
return '';
},
collapsed: {
read: () => !folder.hidden() && folder.collapsedPrivate(),
write: (value) => {
folder.collapsedPrivate(value);
}
},
hasUnreadMessages: () => 0 < folder.messageCountUnread() && folder.printableUnreadCount(),
hasSubscribedUnreadMessagesSubfolders: () =>
!!folder.subFolders.find(
folder => folder.hasUnreadMessages() || folder.hasSubscribedUnreadMessagesSubfolders()
)
!!folder.subFolders().find(
folder => folder.hasUnreadMessages() | folder.hasSubscribedUnreadMessagesSubfolders()
)
// ,href: () => folder.canBeSelected() && mailBox(folder.fullNameHash)
});
folder.addSubscribables({
@ -351,7 +430,7 @@ export class FolderModel extends AbstractModel {
messageCountUnread: unread => {
if (FolderType.Inbox === folder.type()) {
dispatchEvent(new CustomEvent('mailbox.inbox-unread-count', {detail:unread}));
fireEvent('mailbox.inbox-unread-count', unread);
}
}
});
@ -363,16 +442,9 @@ export class FolderModel extends AbstractModel {
* @returns {string}
*/
collapsedCss() {
return 'e-collapsed-sign ' + (this.hasSubscribedSubfolders()
return 'e-collapsed-sign ' + (this.hasVisibleSubfolders()
? (this.collapsed() ? 'icon-right-mini' : 'icon-down-mini')
: 'icon-none'
);
}
/**
* @returns {string}
*/
printableFullName() {
return this.fullName.split(this.delimiter).join(' / ');
}
}

View file

@ -1,4 +1,4 @@
import ko from 'ko';
import { koComputable } from 'External/ko';
import { AbstractModel } from 'Knoin/AbstractModel';
@ -21,10 +21,10 @@ export class IdentityModel extends AbstractModel {
signature: '',
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(),
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 { i18n } from 'Common/Translator';
import { encodeHtml } from 'Common/Html';
import { isArray, isNonEmptyArray } from 'Common/Utils';
import { doc } from 'Common/Globals';
import { encodeHtml, plainToHtml, cleanHtml } from 'Common/Html';
import { isArray, arrayLength, forEachObjectEntry } from 'Common/Utils';
import { serverRequestRaw } from 'Common/Links';
import { FolderUserStore } from 'Stores/User/Folder';
import { SettingsUserStore } from 'Stores/User/Settings';
import { FileInfo } from 'Common/File';
import { AttachmentCollectionModel } from 'Model/AttachmentCollection';
@ -18,13 +19,17 @@ import { AbstractModel } from 'Knoin/AbstractModel';
import PreviewHTML from 'Html/PreviewMessage.html';
const
SignedVerifyStatus = {
UnknownPublicKeys: -4,
UnknownPrivateKey: -3,
Unverified: -2,
Error: -1,
None: 0,
Success: 1
// eslint-disable-next-line max-len
url = /(^|[\s\n]|\/?>)(https:\/\/[-A-Z0-9+\u0026\u2019#/%?=()~_|!:,.;]*[-A-Z0-9+\u0026#/%=~()_|])/gi,
// eslint-disable-next-line max-len
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,
hcont = Element.fromHTML('<div area="hidden" style="position:absolute;left:-5000px"></div>'),
getRealHeight = el => {
hcont.innerHTML = el.outerHTML;
const result = hcont.clientHeight;
hcont.innerHTML = '';
return result;
},
replyHelper = (emails, unic, localEmails) => {
@ -36,6 +41,8 @@ const
});
};
doc.body.append(hcont);
export class MessageModel extends AbstractModel {
constructor() {
super();
@ -44,38 +51,34 @@ export class MessageModel extends AbstractModel {
this.addObservables({
subject: '',
plain: '',
html: '',
size: 0,
spamScore: 0,
spamResult: '',
isSpam: false,
hasVirus: null, // or boolean when scanned
dateTimeStampInUTC: 0,
priority: MessagePriority.Normal,
senderEmailsString: '',
senderClearEmailsString: '',
newForAnimation: false,
deleted: false,
isDeleted: false,
isUnseen: false,
isFlagged: false,
isAnswered: false,
isForwarded: false,
isReadReceipt: false,
focused: false,
selected: false,
checked: false,
hasAttachments: false,
isHtml: false,
hasImages: false,
hasExternals: false,
isPgpSigned: false,
isPgpEncrypted: false,
pgpSignedVerifyStatus: SignedVerifyStatus.None,
pgpSignedVerifyUser: '',
pgpSigned: null,
pgpVerified: null,
pgpEncrypted: null,
pgpDecrypted: false,
readReceipt: '',
@ -84,22 +87,32 @@ export class MessageModel extends AbstractModel {
});
this.attachments = ko.observableArray(new AttachmentCollectionModel);
this.attachmentsSpecData = ko.observableArray();
this.threads = ko.observableArray();
this.unsubsribeLinks = ko.observableArray();
this.flags = ko.observableArray();
this.addComputables({
attachmentIconClass: () => FileInfo.getCombinedIconClass(this.hasAttachments() ? this.attachmentsSpecData() : []),
attachmentIconClass: () => FileInfo.getAttachmentsIconClass(this.attachments()),
threadsLen: () => this.threads().length,
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')
});
}
_reset() {
this.folder = '';
this.uid = '';
this.uid = 0;
this.hash = '';
this.requestHash = '';
this.externalProxy = false;
this.emails = [];
this.from = new EmailCollectionModel;
this.to = new EmailCollectionModel;
@ -107,7 +120,6 @@ export class MessageModel extends AbstractModel {
this.bcc = new EmailCollectionModel;
this.replyTo = new EmailCollectionModel;
this.deliveredTo = new EmailCollectionModel;
this.unsubsribeLinks = [];
this.body = null;
this.draftInfo = [];
this.messageId = '';
@ -118,49 +130,50 @@ export class MessageModel extends AbstractModel {
clear() {
this._reset();
this.subject('');
this.html('');
this.plain('');
this.size(0);
this.spamScore(0);
this.spamResult('');
this.isSpam(false);
this.hasVirus(null);
this.dateTimeStampInUTC(0);
this.priority(MessagePriority.Normal);
this.senderEmailsString('');
this.senderClearEmailsString('');
this.newForAnimation(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.checked(false);
this.hasAttachments(false);
this.attachmentsSpecData([]);
this.isHtml(false);
this.hasImages(false);
this.hasExternals(false);
this.attachments(new AttachmentCollectionModel);
this.isPgpSigned(false);
this.isPgpEncrypted(false);
this.pgpSignedVerifyStatus(SignedVerifyStatus.None);
this.pgpSignedVerifyUser('');
this.pgpSigned(null);
this.pgpVerified(null);
this.pgpEncrypted(null);
this.pgpDecrypted(false);
this.priority(MessagePriority.Normal);
this.readReceipt('');
this.threads([]);
this.unsubsribeLinks([]);
this.hasUnseenSubMessage(false);
this.hasFlaggedSubMessage(false);
}
spamStatus() {
let spam = this.spamResult();
return spam ? i18n(this.isSpam() ? 'GLOBAL/SPAM' : 'GLOBAL/NOT_SPAM') + ': ' + spam : '';
}
/**
* @param {Array} properties
* @returns {Array}
@ -179,7 +192,7 @@ export class MessageModel extends AbstractModel {
}
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.senderClearEmailsString(this[list].toStringClear());
}
@ -189,13 +202,12 @@ export class MessageModel extends AbstractModel {
* @returns {boolean}
*/
revivePropertiesFromJson(json) {
if ('Priority' in json) {
let p = parseInt(json.Priority, 10);
json.Priority = MessagePriority.High == p || MessagePriority.Low == p ? p : MessagePriority.Normal;
if ('Priority' in json && ![MessagePriority.High, MessagePriority.Low].includes(json.Priority)) {
json.Priority = MessagePriority.Normal;
}
if (super.revivePropertiesFromJson(json)) {
// this.foundedCIDs = isArray(json.FoundedCIDs) ? json.FoundedCIDs : [];
// this.attachments(AttachmentCollectionModel.reviveFromJson(json.Attachments, this.foundedCIDs));
// this.foundCIDs = isArray(json.FoundCIDs) ? json.FoundCIDs : [];
// this.attachments(AttachmentCollectionModel.reviveFromJson(json.Attachments, this.foundCIDs));
this.computeSenderEmail();
}
@ -205,14 +217,14 @@ export class MessageModel extends AbstractModel {
* @returns {boolean}
*/
hasUnsubsribeLinks() {
return this.unsubsribeLinks && this.unsubsribeLinks.length;
return this.unsubsribeLinks().length;
}
/**
* @returns {string}
*/
getFirstUnsubsribeLink() {
return this.unsubsribeLinks && this.unsubsribeLinks.length ? this.unsubsribeLinks[0] || '' : '';
return this.unsubsribeLinks()[0] || '';
}
/**
@ -229,7 +241,7 @@ export class MessageModel extends AbstractModel {
*/
fromDkimData() {
let result = ['none', ''];
if (isNonEmptyArray(this.from) && 1 === this.from.length && this.from[0] && this.from[0].dkimStatus) {
if (1 === arrayLength(this.from) && this.from[0] && this.from[0].dkimStatus) {
result = [this.from[0].dkimStatus, this.from[0].dkimValue || ''];
}
@ -277,7 +289,7 @@ export class MessageModel extends AbstractModel {
*/
lineAsCss() {
let classes = [];
Object.entries({
forEachObjectEntry({
deleted: this.deleted(),
'deleted-mark': this.isDeleted(),
selected: this.selected(),
@ -288,13 +300,12 @@ export class MessageModel extends AbstractModel {
forwarded: this.isForwarded(),
focused: this.focused(),
important: this.isImportant(),
withAttachments: this.hasAttachments(),
new: this.newForAnimation(),
withAttachments: !!this.attachments().length,
emptySubject: !this.subject(),
// hasChildrenMessage: 1 < this.threadsLen(),
hasUnseenSubMessage: this.hasUnseenSubMessage(),
hasFlaggedSubMessage: this.hasFlaggedSubMessage()
}).forEach(([key, value]) => value && classes.push(key));
}, (key, value) => value && classes.push(key));
return classes.join(' ');
}
@ -367,35 +378,138 @@ export class MessageModel extends AbstractModel {
return [toResult, ccResult];
}
/**
* @param {boolean=} print = false
*/
viewHtml() {
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) {
const timeStampInUTC = this.dateTimeStampInUTC() || 0,
ccLine = this.ccToLine(false),
m = 0 < timeStampInUTC ? new Date(timeStampInUTC * 1000) : null,
win = open(''),
doc = win.document;
doc.write(PreviewHTML
.replace(/{{subject}}/g, encodeHtml(this.subject()))
.replace('{{date}}', encodeHtml(m ? m.format('LLL') : ''))
.replace('{{fromCreds}}', encodeHtml(this.fromToLine(false)))
.replace('{{toCreds}}', encodeHtml(this.toToLine(false)))
.replace('{{toLabel}}', encodeHtml(i18n('GLOBAL/TO')))
.replace('{{ccHide}}', ccLine ? '' : 'hidden=""')
.replace('{{ccCreds}}', encodeHtml(ccLine))
.replace('{{ccLabel}}', encodeHtml(i18n('GLOBAL/CC')))
.replace('{{bodyClass}}', this.isHtml() ? 'html' : 'plain')
.replace('{{html}}', this.bodyAsHTML())
sdoc = win.document;
let subject = encodeHtml(this.subject()),
mode = this.isHtml() ? 'div' : 'pre',
cc = ccLine ? `<div>${encodeHtml(i18n('GLOBAL/CC'))}: ${encodeHtml(ccLine)}</div>` : '',
style = getComputedStyle(doc.querySelector('.messageView')),
prop = property => style.getPropertyValue(property);
sdoc.write(PreviewHTML
.replace('<title>', '<title>'+subject)
// eslint-disable-next-line max-len
.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}>`)
);
doc.close();
sdoc.close();
if (print) {
setTimeout(() => win.print(), 100);
}
}
/**
* @param {boolean=} print = false
*/
popupMessage() {
this.viewPopupMessage(false);
}
printMessage() {
this.viewPopupMessage(true);
}
@ -411,70 +525,61 @@ export class MessageModel extends AbstractModel {
* @param {MessageModel} message
* @returns {MessageModel}
*/
populateByMessageListItem(message) {
if (message) {
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.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 = '';
static fromMessageListItem(message) {
let self = new MessageModel();
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() {
if (this.body && this.body.rlHasImages) {
const body = this.body;
if (body && this.hasImages()) {
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 => {
if (node.matches('img')) {
node.loading = 'lazy';
@ -482,81 +587,28 @@ export class MessageModel extends AbstractModel {
node.src = node.getAttribute(attr);
});
attr = this.externalProxy ? 'data-x-additional-style-url' : 'data-x-style-url';
body.querySelectorAll('[' + attr + ']').forEach(node => {
node.setAttribute('style', ((node.getAttribute('style')||'')
+ ';' + node.getAttribute(attr))
.replace(/^[;\s]+/,''));
body.querySelectorAll('[data-x-style-url]').forEach(node => {
forEachObjectEntry(JSON.parse(node.dataset.xStyleUrl), (name, url) => node.style[name] = "url('" + url + "')");
});
}
}
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') || ''));
}
}
});
}
}
storeDataInDom() {
if (this.body) {
this.body.rlIsHtml = !!this.isHtml();
this.body.rlHasImages = !!this.hasImages();
}
}
fetchDataFromDom() {
if (this.body) {
this.isHtml(!!this.body.rlIsHtml);
this.hasImages(!!this.body.rlHasImages);
}
}
/**
* @returns {string}
*/
bodyAsHTML() {
if (this.body) {
let clone = this.body.cloneNode(true),
attr = 'data-html-editor-font-wrapper';
// if (this.body && !this.body.querySelector('iframe[src*=decrypt]')) {
if (this.body && !this.body.querySelector('iframe')) {
let clone = this.body.cloneNode(true);
clone.querySelectorAll('blockquote.rl-bq-switcher').forEach(
node => node.classList.remove('rl-bq-switcher','hidden-bq')
);
clone.querySelectorAll('.rlBlockquoteSwitcher').forEach(
node => node.remove()
);
clone.querySelectorAll('['+attr+']').forEach(
node => node.removeAttribute(attr)
);
return clone.innerHTML;
}
return '';
return this.html() || plainToHtml(this.plain());
}
/**
@ -573,4 +625,5 @@ export class MessageModel extends AbstractModel {
this.isReadReceipt()
].join(',');
}
}

View file

@ -39,7 +39,6 @@ export class MessageCollectionModel extends AbstractCollectionModel
if (message) {
if (hasNewMessageAndRemoveFromCache(message.folder, message.uid) && 5 >= newCount) {
++newCount;
message.newForAnimation(true);
}
message.deleted(false);

View file

@ -1,74 +0,0 @@
import ko from 'ko';
import { isNonEmptyArray } 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 = isNonEmptyArray(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,23 +0,0 @@
import ko from 'ko';
import { AbstractModel } from 'Knoin/AbstractModel';
export class TemplateModel extends AbstractModel {
/**
* @param {string} id
* @param {string} name
* @param {string} body
*/
constructor(id = '', name = '', body = '') {
super();
this.id = id;
this.name = name;
this.body = body;
this.populated = true;
this.deleteAccess = ko.observable(false);
}
// static reviveFromJson(json) {}
}

View file

@ -1,41 +1,30 @@
import { Notification } from 'Common/Enums';
import { Settings } from 'Common/Globals';
import { isArray, pInt, pString } from 'Common/Utils';
import { serverRequest } from 'Common/Links';
import { runHook } from 'Common/Plugins';
import { getNotification } from 'Common/Translator';
let iJsonErrorCount = 0,
iTokenErrorCount = 0;
let iJsonErrorCount = 0;
const getURL = (add = '') => serverRequest('Json') + add,
updateToken = data => {
if (data.UpdateToken) {
rl.hash.set();
Settings.set('AuthAccountHash', data.UpdateToken);
}
},
checkResponseError = data => {
const err = data ? data.ErrorCode : null;
if (Notification.InvalidToken === err && 10 < ++iTokenErrorCount) {
if (Notification.InvalidToken === err) {
alert(getNotification(err));
rl.logoutReload();
} else {
if ([
Notification.AuthError,
Notification.ConnectionError,
Notification.DomainNotAllowed,
Notification.AccountNotAllowed,
Notification.MailServerError,
Notification.UnknownNotification,
Notification.UnknownError
].includes(err)
) {
++iJsonErrorCount;
}
if (data.ClearAuth || data.Logout || 7 < iJsonErrorCount) {
rl.hash.clear();
data.ClearAuth || rl.logoutReload();
} else if ([
Notification.AuthError,
Notification.ConnectionError,
Notification.DomainNotAllowed,
Notification.AccountNotAllowed,
Notification.MailServerError,
Notification.UnknownNotification,
Notification.UnknownError
].includes(err)
) {
if (7 < ++iJsonErrorCount) {
rl.logoutReload();
}
}
},
@ -57,7 +46,11 @@ abort = (sAction, bClearOnly) => {
fetchJSON = (action, sGetAdd, params, timeout, jsonCallback) => {
sGetAdd = pString(sGetAdd);
params = params || {};
params.Action = action;
if (params instanceof FormData) {
params.set('Action', action);
} else {
params.Action = action;
}
let init = {};
if (window.AbortController) {
abort(action);
@ -69,6 +62,14 @@ fetchJSON = (action, sGetAdd, params, timeout, jsonCallback) => {
return rl.fetchJSON(getURL(sGetAdd), init, sGetAdd ? null : params).then(jsonCallback);
};
class FetchError extends Error
{
constructor(code, message) {
super(message);
this.code = code || Notification.JsonFalse;
}
}
export class AbstractFetchRemote
{
abort(sAction, bClearOnly) {
@ -76,6 +77,45 @@ export class AbstractFetchRemote
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 {string} sAction
@ -84,7 +124,7 @@ export class AbstractFetchRemote
* @param {string=} sGetAdd = ''
* @param {Array=} aAbortActions = []
*/
defaultRequest(fCallback, sAction, params, iTimeout, sGetAdd, abortActions) {
request(sAction, fCallback, params, iTimeout, sGetAdd, abortActions) {
params = params || {};
const start = Date.now();
@ -100,12 +140,8 @@ export class AbstractFetchRemote
undefined === iTimeout ? 30000 : pInt(iTimeout),
data => {
let cached = false;
if (data) {
if (data.Time) {
cached = pInt(data.Time) > Date.now() - start;
}
updateToken(data);
if (data && data.Time) {
cached = pInt(data.Time) > Date.now() - start;
}
let iError = 0;
@ -123,7 +159,7 @@ export class AbstractFetchRemote
}
*/
if (data.Result) {
iJsonErrorCount = iTokenErrorCount = 0;
iJsonErrorCount = 0;
} else {
checkResponseError(data);
iError = data.ErrorCode || Notification.UnknownError
@ -153,32 +189,11 @@ export class AbstractFetchRemote
});
}
/**
* @param {?Function} fCallback
*/
noop(fCallback) {
this.defaultRequest(fCallback, 'Noop');
}
/**
* @param {?Function} fCallback
*/
getPublicKey(fCallback) {
this.defaultRequest(fCallback, 'GetPublicKey');
}
/**
* @param {?Function} fCallback
* @param {string} sVersion
*/
jsVersion(fCallback, sVersion) {
this.defaultRequest(fCallback, 'Version', {
Version: sVersion
});
}
fastResolve(mData) {
return Promise.resolve(mData);
this.request('GetPublicKey', fCallback);
}
setTrigger(trigger, value) {
@ -190,17 +205,15 @@ export class AbstractFetchRemote
}
}
postRequest(action, fTrigger, params, timeOut) {
post(action, fTrigger, params, timeOut) {
this.setTrigger(fTrigger, true);
return fetchJSON(action, '', params, pInt(timeOut, 30000),
data => {
abort(action, true);
if (!data) {
return Promise.reject(Notification.JsonParse);
return Promise.reject(new FetchError(Notification.JsonParse));
}
updateToken(data);
/*
let isCached = false, type = '';
if (data && data.Time) {
@ -223,8 +236,10 @@ export class AbstractFetchRemote
if (!data.Result || action !== data.Action) {
checkResponseError(data);
const err = data ? data.ErrorCode : 0;
return Promise.reject(err || Notification.JsonFalse);
return Promise.reject(new FetchError(
data ? data.ErrorCode : 0,
data ? (data.ErrorMessageAdditional || data.ErrorMessage) : ''
));
}
return data;

View file

@ -1,212 +1,14 @@
import { AbstractFetchRemote } from 'Remote/AbstractFetch';
class RemoteAdminFetch extends AbstractFetchRemote {
/**
* @param {?Function} fCallback
* @param {string} sLogin
* @param {string} sPassword
*/
adminLogin(fCallback, sLogin, sPassword) {
this.defaultRequest(fCallback, 'AdminLogin', {
Login: sLogin,
Password: sPassword
});
}
/**
* @param {string} key
* @param {?scalar} value
* @param {?Function} fCallback
*/
adminLogout(fCallback) {
this.defaultRequest(fCallback, 'AdminLogout');
}
/**
* @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
*/
pluginList(fCallback) {
this.defaultRequest(fCallback, 'AdminPluginList');
}
/**
* @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} sName
*/
plugin(fCallback, sName) {
this.defaultRequest(fCallback, 'AdminPluginLoad', {
Name: sName
});
}
/**
* @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} sName
* @param {boolean} bDisabled
*/
pluginDisable(fCallback, sName, bDisabled) {
this.defaultRequest(fCallback, 'AdminPluginDisable', {
Name: sName,
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);
saveSetting(key, value, fCallback) {
this.request('AdminSettingsUpdate', fCallback, {[key]: value});
}
}

View file

@ -1,11 +1,9 @@
import { isArray, isNonEmptyArray, pString, pInt } from 'Common/Utils';
import { pString, pInt, b64EncodeJSONSafe } from 'Common/Utils';
import {
getFolderHash,
getFolderInboxName,
getFolderUidNext,
getFolderFromCacheList,
MessageFlagsCache
getFolderFromCacheList
} from 'Common/Cache';
import { SettingsGet } from 'Common/Globals';
@ -17,224 +15,7 @@ import { FolderUserStore } from 'Stores/User/Folder';
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 {
/**
* @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 {string} sEmail
* @param {string} sLogin
* @param {string} sPassword
* @param {boolean} bSignMe
* @param {string=} sLanguage
*/
login(fCallback, sEmail, sPassword, bSignMe, sLanguage) {
this.defaultRequest(fCallback, 'Login', {
Email: sEmail,
Login: '',
Password: sPassword,
Language: sLanguage || '',
SignMe: bSignMe ? 1 : 0
});
}
/**
* @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
*/
templates(fCallback) {
this.defaultRequest(fCallback, 'Templates', {});
}
/**
* @param {Function} fCallback
* @param {string} sID
*/
templateGetById(fCallback, sID) {
this.defaultRequest(fCallback, 'TemplateGetByID', {
ID: sID
});
}
/**
* @param {Function} fCallback
* @param {string} sID
*/
templateDelete(fCallback, sID) {
this.defaultRequest(fCallback, 'TemplateDelete', {
IdToDelete: sID
});
}
/**
* @param {Function} fCallback
* @param {string} sID
* @param {string} sName
* @param {string} sBody
*/
templateSetup(fCallback, sID, sName, sBody) {
this.defaultRequest(fCallback, 'TemplateSetup', {
ID: sID,
Name: sName,
Body: sBody
});
}
/**
* @param {Function} fCallback
@ -243,23 +24,23 @@ class RemoteUserFetch extends AbstractFetchRemote {
*/
messageList(fCallback, params, bSilent = false) {
const
sFolderFullNameRaw = pString(params.Folder),
folderHash = getFolderHash(sFolderFullNameRaw),
useThreads = AppUserStore.threadsAllowed() && SettingsUserStore.useThreads() ? 1 : 0,
inboxUidNext = getFolderInboxName() === sFolderFullNameRaw ? getFolderUidNext(sFolderFullNameRaw) : '';
sFolderFullName = pString(params.Folder),
folderHash = getFolderHash(sFolderFullName);
params.Folder = sFolderFullNameRaw;
params.ThreadUid = useThreads ? params.ThreadUid : '';
params = Object.assign({
Folder: '',
Offset: 0,
Limit: SettingsUserStore.messagesPerPage(),
Search: '',
UidNext: inboxUidNext,
UseThreads: useThreads,
ThreadUid: '',
Sort: FolderUserStore.sortMode()
UidNext: getFolderUidNext(sFolderFullName), // Used to check for new messages
Sort: FolderUserStore.sortMode(),
Hash: folderHash + SettingsGet('AccountHash')
}, params);
params.Folder = sFolderFullName;
if (AppUserStore.threadsAllowed() && SettingsUserStore.useThreads()) {
params.UseThreads = 1;
} else {
params.ThreadUid = 0;
}
let sGetAdd = '';
@ -267,13 +48,12 @@ class RemoteUserFetch extends AbstractFetchRemote {
sGetAdd = 'MessageList/' +
SUB_QUERY_PREFIX +
'/' +
urlsafeArray([SettingsGet('ProjectHash'),folderHash].concat(Object.values(params)));
b64EncodeJSONSafe(params);
params = {};
}
this.defaultRequest(
this.request('MessageList',
fCallback,
'MessageList',
params,
30000,
sGetAdd,
@ -283,43 +63,27 @@ class RemoteUserFetch extends AbstractFetchRemote {
/**
* @param {?Function} fCallback
* @param {Array} aDownloads
*/
messageUploadAttachments(fCallback, aDownloads) {
this.defaultRequest(
fCallback,
'MessageUploadAttachments',
{
Attachments: aDownloads
},
999000
);
}
/**
* @param {?Function} fCallback
* @param {string} sFolderFullNameRaw
* @param {string} sFolderFullName
* @param {number} iUid
* @returns {boolean}
*/
message(fCallback, sFolderFullNameRaw, iUid) {
sFolderFullNameRaw = pString(sFolderFullNameRaw);
message(fCallback, sFolderFullName, iUid) {
sFolderFullName = pString(sFolderFullName);
iUid = pInt(iUid);
if (getFolderFromCacheList(sFolderFullNameRaw) && 0 < iUid) {
this.defaultRequest(
if (getFolderFromCacheList(sFolderFullName) && 0 < iUid) {
this.request('Message',
fCallback,
'Message',
{},
null,
'Message/' +
SUB_QUERY_PREFIX +
'/' +
urlsafeArray([
sFolderFullNameRaw,
b64EncodeJSONSafe([
sFolderFullName,
iUid,
SettingsGet('ProjectHash'),
AppUserStore.threadsAllowed() && SettingsUserStore.useThreads() ? 1 : 0
AppUserStore.threadsAllowed() && SettingsUserStore.useThreads() ? 1 : 0,
SettingsGet('AccountHash')
]),
['Message']
);
@ -330,186 +94,12 @@ class RemoteUserFetch extends AbstractFetchRemote {
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 (isNonEmptyArray(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.join(',') : '',
UidNext: getFolderInboxName() === folder ? getFolderUidNext(folder) : ''
});
} 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 {string} sMessageUid
* @param {string} sReadReceipt
* @param {string} sSubject
* @param {string} sText
*/
sendReadReceiptMessage(fCallback, sMessageFolder, sMessageUid, sReadReceipt, sSubject, sText) {
this.defaultRequest(fCallback, 'SendReadReceiptMessage', {
MessageFolder: sMessageFolder,
MessageUid: sMessageUid,
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 {Object} oData
*/
saveSettings(fCallback, oData) {
this.defaultRequest(fCallback, 'SettingsUpdate', oData);
this.request('SettingsUpdate', fCallback, oData);
}
/**
@ -523,234 +113,15 @@ class RemoteUserFetch extends AbstractFetchRemote {
});
}
/**
* @param {?Function} fCallback
* @param {string} sFolderFullNameRaw
*/
folderClear(fCallback, sFolderFullNameRaw) {
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,
/*
folderMove(sPrevFolderFullName, sNewFolderFullName, bSubscribe) {
return this.post('FolderMove', FolderUserStore.foldersRenaming, {
Folder: sPrevFolderFullName,
NewFolder: sNewFolderFullName,
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();

View file

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

View file

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

View file

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

View file

@ -1,33 +1,32 @@
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 { getFolderFromCacheList, getFolderFullNameRaw, getFolderInboxName } from 'Common/Cache';
import { setLayoutResizer } from 'Common/UtilsUser';
import { getFolderFromCacheList, getFolderFullName, getFolderInboxName } from 'Common/Cache';
import { i18n } from 'Common/Translator';
import { SettingsUserStore } from 'Stores/User/Settings';
import { AppUserStore } from 'Stores/User/App';
import { AccountUserStore } from 'Stores/User/Account';
import { FolderUserStore } from 'Stores/User/Folder';
import { MessageUserStore } from 'Stores/User/Message';
import { MessagelistUserStore } from 'Stores/User/Messagelist';
import { ThemeStore } from 'Stores/Theme';
import { SystemDropDownMailBoxUserView } from 'View/User/MailBox/SystemDropDown';
import { FolderListMailBoxUserView } from 'View/User/MailBox/FolderList';
import { MessageListMailBoxUserView } from 'View/User/MailBox/MessageList';
import { MessageViewMailBoxUserView } from 'View/User/MailBox/MessageView';
import { warmUpScreenPopup } from 'Knoin/Knoin';
import { SystemDropDownUserView } from 'View/User/SystemDropDown';
import { MailFolderList } from 'View/User/MailBox/FolderList';
import { MailMessageList } from 'View/User/MailBox/MessageList';
import { MailMessageView } from 'View/User/MailBox/MessageView';
import { AbstractScreen } from 'Knoin/AbstractScreen';
import { ComposePopupView } from 'View/Popup/Compose';
export class MailBoxUserScreen extends AbstractScreen {
constructor() {
super('mailbox', [
SystemDropDownMailBoxUserView,
FolderListMailBoxUserView,
MessageListMailBoxUserView,
MessageViewMailBoxUserView
SystemDropDownUserView,
MailFolderList,
MailMessageList,
MailMessageView
]);
}
@ -52,7 +51,7 @@ export class MailBoxUserScreen extends AbstractScreen {
onShow() {
this.updateWindowTitle();
AppUserStore.focusedState(Scope.None);
AppUserStore.focusedState('none');
AppUserStore.focusedState(Scope.MessageList);
ThemeStore.isMobile() && leftPanelDisabled(true);
@ -65,20 +64,17 @@ export class MailBoxUserScreen extends AbstractScreen {
* @returns {void}
*/
onRoute(folderHash, page, search) {
const folder = getFolderFromCacheList(getFolderFullNameRaw(folderHash.replace(/~([\d]+)$/, '')));
const folder = getFolderFromCacheList(getFolderFullName(folderHash.replace(/~([\d]+)$/, '')));
if (folder) {
let threadUid = folderHash.replace(/^.+~([\d]+)$/, '$1');
if (folderHash === threadUid) {
threadUid = '';
}
let threadUid = folderHash.replace(/^.+~(\d+)$/, '$1');
FolderUserStore.currentFolder(folder);
MessageUserStore.listPage(1 > page ? 1 : page);
MessageUserStore.listSearch(search);
MessageUserStore.listThreadUid(threadUid);
MessagelistUserStore.page(1 > page ? 1 : page);
MessagelistUserStore.listSearch(search);
MessagelistUserStore.threadUid((folderHash === threadUid) ? 0 : pInt(threadUid));
rl.app.reloadMessageList();
MessagelistUserStore.reload();
}
}
@ -86,28 +82,41 @@ export class MailBoxUserScreen extends AbstractScreen {
* @returns {void}
*/
onStart() {
if (!this.__started) {
super.onStart();
setTimeout(() => warmUpScreenPopup(ComposePopupView), 500);
super.onStart();
addEventListener('mailbox.inbox-unread-count', e => {
FolderUserStore.foldersInboxUnreadCount(e.detail);
const email = AccountUserStore.email();
AccountUserStore.accounts.forEach(item =>
item && email === item.email && item.count(e.detail)
);
this.updateWindowTitle();
});
}
addEventListener('mailbox.inbox-unread-count', e => {
FolderUserStore.foldersInboxUnreadCount(e.detail);
/* // Disabled in SystemDropDown.html
const email = AccountUserStore.email();
AccountUserStore.accounts.forEach(item =>
item && email === item.email && item.count(e.detail)
);
*/
this.updateWindowTitle();
});
}
/**
* @returns {void}
*/
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 =>
event.target.closest('#rl-right') && moveAction(false)
@ -115,19 +124,23 @@ export class MailBoxUserScreen extends AbstractScreen {
}
/**
* Parse link as generated by mailBox()
* @returns {Array}
*/
routes() {
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]))];
return [
// Folder: INBOX | INBOX.sub | Sent | fullNameHash
[/^([^/]*)$/, { 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]))]
}],
[/^([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 { keyScope, leftPanelDisabled, Settings } from 'Common/Globals';
import { Scope } from 'Common/Enums';
import { keyScope, leftPanelDisabled, SettingsCapa } from 'Common/Globals';
import { runSettingsViewModelHooks } from 'Common/Plugins';
import { initOnStartOrLangChange, i18n } from 'Common/Translator';
@ -9,23 +9,53 @@ import { ThemeStore } from 'Stores/Theme';
import { AbstractSettingsScreen, settingsAddViewModel } from 'Screen/AbstractSettings';
import { GeneralUserSettings } from 'Settings/User/General';
import { ContactsUserSettings } from 'Settings/User/Contacts';
import { AccountsUserSettings } from 'Settings/User/Accounts';
import { FiltersUserSettings } from 'Settings/User/Filters';
import { SecurityUserSettings } from 'Settings/User/Security';
import { TemplatesUserSettings } from 'Settings/User/Templates';
import { FoldersUserSettings } from 'Settings/User/Folders';
import { ThemesUserSettings } from 'Settings/User/Themes';
import { OpenPgpUserSettings } from 'Settings/User/OpenPgp';
import { UserSettingsGeneral } from 'Settings/User/General';
import { UserSettingsContacts } from 'Settings/User/Contacts';
import { UserSettingsAccounts } from 'Settings/User/Accounts';
import { UserSettingsFilters } from 'Settings/User/Filters';
import { UserSettingsSecurity } from 'Settings/User/Security';
import { UserSettingsFolders } from 'Settings/User/Folders';
import { UserSettingsThemes } from 'Settings/User/Themes';
import { SystemDropDownSettingsUserView } from 'View/User/Settings/SystemDropDown';
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 {
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);
}
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(
() => this.sSettingsTitle = i18n('TITLES/SETTINGS'),
@ -33,67 +63,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() {
this.setSettingsTitle();
keyScope(Scope.Settings);

View file

@ -1,8 +1,15 @@
import ko from 'ko';
import { Settings } from 'Common/Globals';
import Remote from 'Remote/Admin/Fetch';
export class AboutAdminSettings {
export class AdminSettingsAbout /*extends AbstractViewSettings*/ {
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';
import { settingsSaveHelperSimpleFunction } from 'Common/Utils';
import Remote from 'Remote/Admin/Fetch';
export class BrandingAdminSettings {
export class AdminSettingsBranding extends AbstractViewSettings {
constructor() {
this.title = ko.observable(SettingsGet('Title')).idleTrigger();
this.loadingDesc = ko.observable(SettingsGet('LoadingDescription')).idleTrigger();
this.faviconUrl = ko.observable(SettingsGet('FaviconUrl')).idleTrigger();
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()
})
);
super();
this.addSetting('Title');
this.addSetting('LoadingDescription');
this.addSetting('FaviconUrl');
}
}

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 { SaveSettingsStep } from 'Common/Enums';
import { SettingsGet } from 'Common/Globals';
import {
settingsSaveHelperSimpleFunction,
defaultOptionsAfterRender,
addObservablesTo,
addSubscribablesTo
} from 'Common/Utils';
import { defaultOptionsAfterRender } from 'Common/Utils';
import { addObservablesTo } from 'External/ko';
import Remote from 'Remote/Admin/Fetch';
import { decorateKoCommands } from 'Knoin/Knoin';
import { AbstractViewSettings } from 'Knoin/AbstractViews';
export class ContactsAdminSettings {
export class AdminSettingsContacts extends AbstractViewSettings {
constructor() {
super();
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, {
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,
testContactsSuccess: false,
testContactsError: false,
@ -54,61 +49,23 @@ export class ContactsAdminSettings {
this.mainContactsType = ko
.computed({
read: this.contactsType,
read: this.contactsPdoType,
write: value => {
if (value !== this.contactsType()) {
if (value !== this.contactsPdoType()) {
if (supportedTypes.includes(value)) {
this.contactsType(value);
this.contactsPdoType(value);
} else if (types.length) {
this.contactsType('');
this.contactsPdoType('');
}
} else {
this.contactsType.valueHasMutated();
this.contactsPdoType.valueHasMutated();
}
}
})
.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()
})
})
this.onTestContactsResponse = this.onTestContactsResponse.bind(this);
decorateKoCommands(this, {
testContactsCommand: self => self.pdoDsn() && self.pdoUser()
testContactsCommand: self => self.contactsPdoDsn() && self.contactsPdoUser()
});
}
@ -118,31 +75,31 @@ export class ContactsAdminSettings {
this.testContactsErrorMessage('');
this.testing(true);
Remote.testContacts(this.onTestContactsResponse, {
ContactsPdoType: this.contactsType(),
ContactsPdoDsn: this.pdoDsn(),
ContactsPdoUser: this.pdoUser(),
ContactsPdoPassword: this.pdoPassword()
});
}
onTestContactsResponse(iError, data) {
this.testContactsSuccess(false);
this.testContactsError(false);
this.testContactsErrorMessage('');
if (!iError && data.Result.Result) {
this.testContactsSuccess(true);
} else {
this.testContactsError(true);
if (data && data.Result) {
this.testContactsErrorMessage(data.Result.Message || '');
} else {
Remote.request('AdminContactsTest',
(iError, data) => {
this.testContactsSuccess(false);
this.testContactsError(false);
this.testContactsErrorMessage('');
}
}
this.testing(false);
if (!iError && data.Result.Result) {
this.testContactsSuccess(true);
} else {
this.testContactsError(true);
if (data && data.Result) {
this.testContactsErrorMessage(data.Result.Message || '');
} else {
this.testContactsErrorMessage('');
}
}
this.testing(false);
}, {
ContactsPdoType: this.contactsPdoType(),
ContactsPdoDsn: this.contactsPdoDsn(),
ContactsPdoUser: this.contactsPdoUser(),
ContactsPdoPassword: this.contactsPdoPassword()
}
);
}
onShow() {

View file

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

View file

@ -2,28 +2,29 @@ import ko from 'ko';
import {
isArray,
pInt,
settingsSaveHelperSimpleFunction,
changeTheme,
convertThemeName,
addObservablesTo,
addSubscribablesTo
convertThemeName
} from 'Common/Utils';
import { Capa, SaveSettingsStep } from 'Common/Enums';
import { Settings, SettingsGet } from 'Common/Globals';
import { reload as translatorReload, convertLangName } from 'Common/Translator';
import { addObservablesTo, addSubscribablesTo, addComputablesTo } from 'External/ko';
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 Remote from 'Remote/Admin/Fetch';
import { ThemeStore } from 'Stores/Theme';
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() {
super();
this.language = LanguageStore.language;
this.languages = LanguageStore.languages;
@ -36,18 +37,16 @@ export class GeneralAdminSettings {
this.theme = ThemeStore.theme;
this.themes = ThemeStore.themes;
this.addSettings(['AllowLanguagesOnSettings','NewMoveToFolder']);
addObservablesTo(this, {
allowLanguagesOnSettings: !!SettingsGet('AllowLanguagesOnSettings'),
newMoveToFolder: !!SettingsGet('NewMoveToFolder'),
attachmentLimitTrigger: SaveSettingsStep.Idle,
languageTrigger: SaveSettingsStep.Idle,
themeTrigger: SaveSettingsStep.Idle,
capaThemes: Settings.capa(Capa.Themes),
capaUserBackground: Settings.capa(Capa.UserBackground),
capaAdditionalAccounts: Settings.capa(Capa.AdditionalAccounts),
capaIdentities: Settings.capa(Capa.Identities),
capaAttachmentThumbnails: Settings.capa(Capa.AttachmentThumbnails),
capaTemplates: Settings.capa(Capa.Templates),
capaThemes: SettingsCapa('Themes'),
capaUserBackground: SettingsCapa('UserBackground'),
capaAdditionalAccounts: SettingsCapa('AdditionalAccounts'),
capaIdentities: SettingsCapa('Identities'),
capaAttachmentThumbnails: SettingsCapa('AttachmentThumbnails'),
dataFolderAccess: false
});
@ -59,10 +58,14 @@ export class GeneralAdminSettings {
}
*/
this.mainAttachmentLimit = ko
.observable(pInt(SettingsGet('AttachmentLimit')) / (1024 * 1024))
this.attachmentLimit = ko
.observable(SettingsGet('AttachmentLimit') / (1024 * 1024))
.extend({ debounce: 500 });
this.addSetting('Language');
this.addSetting('AttachmentLimit');
this.addSetting('Theme', value => changeTheme(value, this.themeTrigger));
this.uploadData = SettingsGet('PhpUploadSizes');
this.uploadDataDesc =
this.uploadData && (this.uploadData.upload_max_filesize || this.uploadData.post_max_size)
@ -74,12 +77,12 @@ export class GeneralAdminSettings {
].join('')
: '';
this.themesOptions = ko.computed(() =>
this.themes.map(theme => ({ optValue: theme, optText: convertThemeName(theme) }))
);
addComputablesTo(this, {
themesOptions: () => this.themes.map(theme => ({ optValue: theme, optText: convertThemeName(theme) })),
this.languageFullName = ko.computed(() => convertLangName(this.language()));
this.languageAdminFullName = ko.computed(() => convertLangName(this.languageAdmin()));
languageFullName: () => convertLangName(this.language()),
languageAdminFullName: () => convertLangName(this.languageAdmin())
});
this.languageAdminTrigger = ko.observable(SaveSettingsStep.Idle).extend({ debounce: 100 });
@ -87,55 +90,25 @@ export class GeneralAdminSettings {
this.languageAdminTrigger(saveSettingsStep);
setTimeout(() => this.languageAdminTrigger(SaveSettingsStep.Idle), 1000);
},
fSaveBoolHelper = (key, fn) =>
value => {
const data = {};
data[key] = value ? 1 : 0;
Remote.saveAdminConfig(fn, data);
};
fSaveHelper = key => value => Remote.saveSetting(key, value);
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 => {
this.languageAdminTrigger(SaveSettingsStep.Animate);
translatorReload(true, value)
.then(fReloadLanguageHelper(SaveSettingsStep.TrueResult), fReloadLanguageHelper(SaveSettingsStep.FalseResult))
.then(() => Remote.saveAdminConfig(null, {
LanguageAdmin: value.trim()
}));
.then(() => Remote.saveSetting('LanguageAdmin', value));
},
theme: value => {
changeTheme(value, this.themeTrigger);
Remote.saveAdminConfig(settingsSaveHelperSimpleFunction(this.themeTrigger, this), {
Theme: value.trim()
});
},
capaAdditionalAccounts: fSaveHelper('CapaAdditionalAccounts'),
capaAdditionalAccounts: fSaveBoolHelper('CapaAdditionalAccounts'),
capaIdentities: fSaveHelper('CapaIdentities'),
capaIdentities: fSaveBoolHelper('CapaIdentities'),
capaAttachmentThumbnails: fSaveHelper('CapaAttachmentThumbnails'),
capaTemplates: fSaveBoolHelper('CapaTemplates'),
capaThemes: fSaveHelper('CapaThemes'),
capaAttachmentThumbnails: fSaveBoolHelper('CapaAttachmentThumbnails'),
capaThemes: fSaveBoolHelper('CapaThemes'),
capaUserBackground: fSaveBoolHelper('CapaUserBackground'),
allowLanguagesOnSettings: fSaveBoolHelper('AllowLanguagesOnSettings'),
newMoveToFolder: fSaveBoolHelper('NewMoveToFolder')
capaUserBackground: fSaveHelper('CapaUserBackground')
});
}

View file

@ -1,46 +1,9 @@
import ko from 'ko';
import { AbstractViewSettings } from 'Knoin/AbstractViews';
import { Settings, SettingsGet } from 'Common/Globals';
import { settingsSaveHelperSimpleFunction, addObservablesTo, addSubscribablesTo } from 'Common/Utils';
import Remote from 'Remote/Admin/Fetch';
export class LoginAdminSettings {
export class AdminSettingsLogin extends AbstractViewSettings {
constructor() {
addObservablesTo(this, {
determineUserLanguage: !!SettingsGet('DetermineUserLanguage'),
determineUserDomain: !!SettingsGet('DetermineUserDomain'),
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()
})
});
super();
this.addSetting('LoginDefaultDomain');
this.addSettings(['DetermineUserLanguage','DetermineUserDomain','AllowLanguagesOnLogin','hideSubmitButton']);
}
}

View file

@ -6,42 +6,58 @@ import { getNotification } from 'Common/Translator';
import { PackageAdminStore } from 'Stores/Admin/Package';
import Remote from 'Remote/Admin/Fetch';
export class PackagesAdminSettings {
import { showScreenPopup } from 'Knoin/Knoin';
import { PluginPopupView } from 'View/Popup/Plugin';
import { addObservablesTo, addComputablesTo } from 'External/ko';
import { AbstractViewSettings } from 'Knoin/AbstractViews';
export class AdminSettingsPackages extends AbstractViewSettings {
constructor() {
this.packagesError = ko.observable('');
super();
this.addSettings(['EnabledPlugins']);
addObservablesTo(this, {
packagesError: ''
});
this.packages = PackageAdminStore;
this.packagesCurrent = ko.computed(() =>
PackageAdminStore.filter(item => item && item.installed && !item.compare)
);
this.packagesAvailableForUpdate = ko.computed(() =>
PackageAdminStore.filter(item => item && item.installed && !!item.compare)
);
this.packagesAvailableForInstallation = ko.computed(() =>
PackageAdminStore.filter(item => item && !item.installed)
);
addComputablesTo(this, {
packagesCurrent: () => PackageAdminStore().filter(item => item && item.installed && !item.canBeUpdated),
packagesUpdate: () => PackageAdminStore().filter(item => item && item.installed && item.canBeUpdated),
packagesAvailable: () => PackageAdminStore().filter(item => item && !item.installed),
this.visibility = ko.computed(() => (PackageAdminStore.loading() ? 'visible' : 'hidden'));
visibility: () => (PackageAdminStore.loading() ? 'visible' : 'hidden')
});
}
onShow() {
this.packagesError('');
}
onBuild() {
onBuild(oDom) {
PackageAdminStore.fetch();
oDom.addEventListener('click', event => {
// configurePlugin
let el = event.target.closestWithin('.package-configure', oDom),
data = el ? ko.dataFor(el) : 0;
data && Remote.request('AdminPluginLoad',
(iError, data) => iError || showScreenPopup(PluginPopupView, [data.Result]),
{
Id: data.id
}
);
// disablePlugin
el = event.target.closestWithin('.package-active', oDom);
data = el ? ko.dataFor(el) : 0;
data && this.disablePlugin(data);
});
}
requestHelper(packageToRequest, install) {
return (iError, data) => {
if (iError) {
this.packagesError(
getNotification(install ? Notification.CantInstallPackage : Notification.CantDeletePackage)
// ':\n' + getNotification(iError);
);
}
PackageAdminStore.forEach(item => {
if (item && packageToRequest && item.loading && item.loading() && packageToRequest.file === item.file) {
packageToRequest.loading(false);
@ -49,7 +65,12 @@ export class PackagesAdminSettings {
}
});
if (!iError && data.Result.Reload) {
if (iError) {
this.packagesError(
getNotification(install ? Notification.CantInstallPackage : Notification.CantDeletePackage)
+ (data.ErrorMessage ? ':\n' + data.ErrorMessage : '')
);
} else if (data.Result.Reload) {
location.reload();
} else {
PackageAdminStore.fetch();
@ -60,14 +81,49 @@ export class PackagesAdminSettings {
deletePackage(packageToDelete) {
if (packageToDelete) {
packageToDelete.loading(true);
Remote.packageDelete(this.requestHelper(packageToDelete, false), packageToDelete);
Remote.request('AdminPackageDelete',
this.requestHelper(packageToDelete, false),
{
Id: packageToDelete.id
}
);
}
}
installPackage(packageToInstall) {
if (packageToInstall) {
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) {
let disable = plugin.enabled();
plugin.enabled(!disable);
Remote.request('AdminPluginDisable',
(iError, data) => {
if (iError) {
plugin.enabled(disable);
this.packagesError(
(Notification.UnsupportedPluginPackage === iError && data && data.ErrorMessage)
? data.ErrorMessage
: getNotification(iError)
);
}
// PackageAdminStore.fetch();
}, {
Id: plugin.id,
Disabled: disable ? 1 : 0
}
);
}
}

View file

@ -1,73 +0,0 @@
import ko from 'ko';
import { Notification } from 'Common/Enums';
import { SettingsGet } from 'Common/Globals';
import { getNotification } from 'Common/Translator';
import { showScreenPopup } from 'Knoin/Knoin';
import { PluginAdminStore } from 'Stores/Admin/Plugin';
import Remote from 'Remote/Admin/Fetch';
import { PluginPopupView } from 'View/Popup/Plugin';
export class PluginsAdminSettings {
constructor() {
this.enabledPlugins = ko.observable(!!SettingsGet('EnabledPlugins'));
this.plugins = PluginAdminStore;
this.pluginsError = PluginAdminStore.error;
this.onPluginLoadRequest = this.onPluginLoadRequest.bind(this);
this.onPluginDisableRequest = this.onPluginDisableRequest.bind(this);
this.enabledPlugins.subscribe(value =>
Remote.saveAdminConfig(null, {
EnabledPlugins: value ? 1 : 0
})
);
}
disablePlugin(plugin) {
plugin.disabled(!plugin.disabled());
Remote.pluginDisable(this.onPluginDisableRequest, plugin.name, plugin.disabled());
}
configurePlugin(plugin) {
Remote.plugin(this.onPluginLoadRequest, plugin.name);
}
onBuild(oDom) {
oDom.addEventListener('click', event => {
let el = event.target.closestWithin('.e-item .configure-plugin-action', oDom);
el && ko.dataFor(el) && this.configurePlugin(ko.dataFor(el));
el = event.target.closestWithin('.e-item .disabled-plugin', oDom);
el && ko.dataFor(el) && this.disablePlugin(ko.dataFor(el));
});
}
onShow() {
PluginAdminStore.error('');
PluginAdminStore.fetch();
}
onPluginLoadRequest(iError, data) {
if (!iError) {
showScreenPopup(PluginPopupView, [data.Result]);
}
}
onPluginDisableRequest(iError, data) {
if (iError) {
if (Notification.UnsupportedPluginPackage === iError && data && data.ErrorMessage) {
PluginAdminStore.error(data.ErrorMessage);
} else {
PluginAdminStore.error(getNotification(iError));
}
}
PluginAdminStore.fetch();
}
}

View file

@ -1,34 +1,40 @@
import { Capa } from 'Common/Enums';
import { Settings, SettingsGet } from 'Common/Globals';
import { addObservablesTo, addSubscribablesTo } from 'Common/Utils';
import { SettingsGet, SettingsCapa } from 'Common/Globals';
import { addObservablesTo, addSubscribablesTo } from 'External/ko';
import Remote from 'Remote/Admin/Fetch';
import { decorateKoCommands } from 'Knoin/Knoin';
import { AbstractViewSettings } from 'Knoin/AbstractViews';
export class SecurityAdminSettings {
export class AdminSettingsSecurity extends AbstractViewSettings {
constructor() {
super();
this.addSettings(['UseLocalProxyForExternalImages','VerifySslCertificate','AllowSelfSigned']);
this.weakPassword = rl.app.weakPassword;
addObservablesTo(this, {
useLocalProxyForExternalImages: !!SettingsGet('UseLocalProxyForExternalImages'),
verifySslCertificate: !!SettingsGet('VerifySslCertificate'),
allowSelfSigned: !!SettingsGet('AllowSelfSigned'),
adminLogin: SettingsGet('AdminLogin'),
adminLoginError: false,
adminPassword: '',
adminPasswordNew: '',
adminPasswordNew2: '',
adminPasswordNewError: false,
adminTOTP: SettingsGet('AdminTOTP'),
adminPasswordUpdateError: false,
adminPasswordUpdateSuccess: false,
capaOpenPGP: Settings.capa(Capa.OpenPGP)
capaOpenPGP: SettingsCapa('OpenPGP')
});
const reset = () => {
this.adminPasswordUpdateError(false);
this.adminPasswordUpdateSuccess(false);
this.adminPasswordNewError(false);
};
addSubscribablesTo(this, {
adminPassword: () => {
this.adminPasswordUpdateError(false);
@ -37,43 +43,13 @@ export class SecurityAdminSettings {
adminLogin: () => this.adminLoginError(false),
adminPasswordNew: () => {
this.adminPasswordUpdateError(false);
this.adminPasswordUpdateSuccess(false);
this.adminPasswordNewError(false);
},
adminPasswordNew: reset,
adminPasswordNew2: () => {
this.adminPasswordUpdateError(false);
this.adminPasswordUpdateSuccess(false);
this.adminPasswordNewError(false);
},
adminPasswordNew2: reset,
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
})
capaOpenPGP: value => Remote.saveSetting('CapaOpenPGP', value)
});
this.onNewAdminPasswordResponse = this.onNewAdminPasswordResponse.bind(this);
decorateKoCommands(this, {
saveNewAdminPasswordCommand: self => self.adminLogin().trim() && self.adminPassword()
});
@ -93,29 +69,28 @@ export class SecurityAdminSettings {
this.adminPasswordUpdateError(false);
this.adminPasswordUpdateSuccess(false);
Remote.saveNewAdminPassword(this.onNewAdminPasswordResponse, {
Remote.request('AdminPasswordUpdate', (iError, data) => {
if (iError) {
this.adminPasswordUpdateError(true);
} else {
this.adminPassword('');
this.adminPasswordNew('');
this.adminPasswordNew2('');
this.adminPasswordUpdateSuccess(true);
this.weakPassword(!!data.Result.Weak);
}
}, {
'Login': this.adminLogin(),
'Password': this.adminPassword(),
'NewPassword': this.adminPasswordNew()
'NewPassword': this.adminPasswordNew(),
'TOTP': this.adminTOTP()
});
return true;
}
onNewAdminPasswordResponse(iError, data) {
if (iError) {
this.adminPasswordUpdateError(true);
} else {
this.adminPassword('');
this.adminPasswordNew('');
this.adminPasswordNew2('');
this.adminPasswordUpdateSuccess(true);
this.weakPassword(!!data.Result.Weak);
}
}
onHide() {
this.adminPassword('');
this.adminPasswordNew('');

View file

@ -1,7 +1,6 @@
import ko from 'ko';
import { Capa } from 'Common/Enums';
import { Settings } from 'Common/Globals';
import { SettingsCapa, SettingsGet } from 'Common/Globals';
import { AccountUserStore } from 'Stores/User/Account';
import { IdentityUserStore } from 'Stores/User/Identity';
@ -12,17 +11,18 @@ import { showScreenPopup } from 'Knoin/Knoin';
import { AccountPopupView } from 'View/Popup/Account';
import { IdentityPopupView } from 'View/Popup/Identity';
export class AccountsUserSettings {
export class UserSettingsAccounts /*extends AbstractViewSettings*/ {
constructor() {
this.allowAdditionalAccount = Settings.capa(Capa.AdditionalAccounts);
this.allowIdentities = Settings.capa(Capa.Identities);
this.allowAdditionalAccount = SettingsCapa('AdditionalAccounts');
this.allowIdentities = SettingsCapa('Identities');
this.accounts = AccountUserStore.accounts;
this.loading = AccountUserStore.loading;
this.identities = IdentityUserStore;
this.mainEmail = SettingsGet('MainEmail');
this.accountForDeletion = ko.observable(null).deleteAccessHelper();
this.identityForDeletion = ko.observable(null).deleteAccessHelper();
this.accountForDeletion = ko.observable(null).askDeleteHelper();
this.identityForDeletion = ko.observable(null).askDeleteHelper();
}
addNewAccount() {
@ -30,7 +30,7 @@ export class AccountsUserSettings {
}
editAccount(account) {
if (account && account.canBeEdit()) {
if (account && account.isAdditional()) {
showScreenPopup(AccountPopupView, [account]);
}
}
@ -48,19 +48,21 @@ export class AccountsUserSettings {
* @returns {void}
*/
deleteAccount(accountToRemove) {
if (accountToRemove && accountToRemove.deleteAccess()) {
if (accountToRemove && accountToRemove.askDelete()) {
this.accountForDeletion(null);
if (accountToRemove) {
this.accounts.remove((account) => accountToRemove === account);
Remote.accountDelete((iError, data) => {
Remote.request('AccountDelete', (iError, data) => {
if (!iError && data.Reload) {
rl.route.root();
setTimeout(() => location.reload(), 1);
} else {
rl.app.accountsAndIdentities();
}
}, accountToRemove.email);
}, {
EmailToDelete: accountToRemove.email
});
}
}
}
@ -70,26 +72,31 @@ export class AccountsUserSettings {
* @returns {void}
*/
deleteIdentity(identityToRemove) {
if (identityToRemove && identityToRemove.deleteAccess()) {
if (identityToRemove && identityToRemove.askDelete()) {
this.identityForDeletion(null);
if (identityToRemove) {
IdentityUserStore.remove(oIdentity => identityToRemove === oIdentity);
Remote.identityDelete(() => rl.app.accountsAndIdentities(), identityToRemove.id());
Remote.request('IdentityDelete', () => rl.app.accountsAndIdentities(), {
IdToDelete: identityToRemove.id()
});
}
}
}
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) {
oDom.addEventListener('click', event => {
let el = event.target.closestWithin('.accounts-list .account-item .e-action', oDom);
let el = event.target.closestWithin('.accounts-list .e-action', oDom);
el && ko.dataFor(el) && this.editAccount(ko.dataFor(el));
el = event.target.closestWithin('.identities-list .identity-item .e-action', oDom);
el = event.target.closestWithin('.identities-list .e-action', oDom);
el && ko.dataFor(el) && this.editIdentity(ko.dataFor(el));
});
}

View file

@ -1,10 +1,11 @@
import ko from 'ko';
import { koComputable } from 'External/ko';
import { SettingsGet } from 'Common/Globals';
import { ContactUserStore } from 'Stores/User/Contact';
import Remote from 'Remote/User/Fetch';
export class ContactsUserSettings {
export class UserSettingsContacts /*extends AbstractViewSettings*/ {
constructor() {
this.contactsAutosave = ko.observable(!!SettingsGet('ContactsAutosave'));
@ -14,8 +15,7 @@ export class ContactsUserSettings {
this.contactsSyncUser = ContactUserStore.syncUser;
this.contactsSyncPass = ContactUserStore.syncPass;
this.saveTrigger = ko
.computed(() =>
this.saveTrigger = koComputable(() =>
[
ContactUserStore.enableSync() ? '1' : '0',
ContactUserStore.syncUrl(),
@ -26,19 +26,16 @@ export class ContactsUserSettings {
.extend({ debounce: 500 });
this.contactsAutosave.subscribe(value =>
Remote.saveSettings(null, {
ContactsAutosave: value ? 1 : 0
})
Remote.saveSettings(null, { ContactsAutosave: value })
);
this.saveTrigger.subscribe(() =>
Remote.saveContactsSyncData(
null,
ContactUserStore.enableSync(),
ContactUserStore.syncUrl(),
ContactUserStore.syncUser(),
ContactUserStore.syncPass()
)
Remote.request('SaveContactsSyncData', null, {
Enable: ContactUserStore.enableSync() ? 1 : 0,
Url: ContactUserStore.syncUrl(),
User: ContactUserStore.syncUser(),
Password: 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';
import { addObservablesTo } from 'Common/Utils';
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 {
//export class UserSettingsFilters /*extends AbstractViewSettings*/ {
export class UserSettingsFilters /*extends AbstractViewSettings*/ {
constructor() {
this.scripts = SieveUserStore.scripts;
this.loading = ko.observable(false).extend({ debounce: 200 });
this.scripts = ko.observableArray();
this.loading = ko.observable(true).extend({ debounce: 200 });
addObservablesTo(this, {
serverError: false,
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.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)
});
}
});
}
this.scriptForDeletion = ko.observable(null).askDeleteHelper();
}
addScript() {
showScreenPopup(SieveScriptPopupView, [new SieveScriptModel()]);
this.editScript();
}
editScript(script) {
showScreenPopup(SieveScriptPopupView, [script]);
window.Sieve.ScriptView.showModal(script ? [script] : null);
}
deleteScript(script) {
this.serverError(false);
Remote.filtersScriptDelete(
(iError, data) => {
if (iError) {
this.setError((data && data.ErrorMessageAdditional) || getNotification(iError));
} else {
this.scripts.remove(script);
delegateRunOnDestroy(script);
}
},
script.name()
);
window.Sieve.deleteScript(script);
}
toggleScript(script) {
let name = script.active() ? '' : script.name();
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
);
window.Sieve.toggleScript(script);
}
onBuild(oDom) {
@ -106,6 +50,6 @@ export class FiltersUserSettings {
}
onShow() {
this.updateList();
window.Sieve && window.Sieve.updateList();
}
}

View file

@ -1,13 +1,15 @@
import ko from 'ko';
import { koComputable } from 'External/ko';
import { Notification } from 'Common/Enums';
import { ClientSideKeyName } from 'Common/EnumsUser';
import { Settings } from 'Common/Globals';
import { FolderMetadataKeys } from 'Common/EnumsUser';
import { SettingsCapa } from 'Common/Globals';
import { getNotification } from 'Common/Translator';
import { removeFolderFromCacheList } from 'Common/Cache';
import * as Local from 'Storage/Client';
import { setFolder, getFolderFromCacheList, removeFolderFromCacheList } from 'Common/Cache';
import { defaultOptionsAfterRender } from 'Common/Utils';
import { sortFolders } from 'Common/Folders';
import { initOnStartOrLangChange, i18n } from 'Common/Translator';
import { FolderUserStore } from 'Stores/User/Folder';
import { SettingsUserStore } from 'Stores/User/Settings';
@ -18,16 +20,36 @@ import { showScreenPopup } from 'Knoin/Knoin';
import { FolderCreatePopupView } from 'View/Popup/FolderCreate';
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() {
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.folderList = FolderUserStore.folderList;
this.folderListOptimized = FolderUserStore.folderListOptimized;
this.folderListError = FolderUserStore.folderListError;
this.hideUnsubscribed = SettingsUserStore.hideUnsubscribed;
this.loading = ko.computed(() => {
this.loading = koComputable(() => {
const loading = FolderUserStore.foldersLoading(),
creating = FolderUserStore.foldersCreating(),
deleting = FolderUserStore.foldersDeleting(),
@ -36,28 +58,43 @@ export class FoldersUserSettings {
return loading || creating || deleting || renaming;
});
this.folderForDeletion = ko.observable(null).deleteAccessHelper();
this.folderForDeletion = folderForDeletion;
this.folderForEdit = ko.observable(null).extend({ toggleSubscribeProperty: [this, 'edited'] });
this.useImapSubscribe = Settings.app('useImapSubscribe');
this.hideUnsubscribed.subscribe(value => Remote.saveSetting('HideUnsubscribed', value ? 1 : 0));
SettingsUserStore.hideUnsubscribed.subscribe(value => Remote.saveSetting('HideUnsubscribed', value));
}
folderEditOnEnter(folder) {
const nameToEdit = folder ? folder.nameForEdit().trim() : '';
if (nameToEdit && folder.name() !== nameToEdit) {
Local.set(ClientSideKeyName.FoldersLashHash, '');
rl.app.foldersPromisesActionHelper(
Remote.folderRename(folder.fullNameRaw, nameToEdit),
Notification.CantRenameFolder
);
removeFolderFromCacheList(folder.fullNameRaw);
folder.name(nameToEdit);
Remote.abort('Folders').post('FolderRename', FolderUserStore.foldersRenaming, {
Folder: folder.fullName,
NewFolderName: nameToEdit,
Subscribe: folder.subscribed() ? 1 : 0
})
.then(data => {
folder.name(nameToEdit/*data.Name*/);
if (folder.subFolders.length) {
Remote.setTrigger(FolderUserStore.foldersLoading, true);
// 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);
@ -85,58 +122,69 @@ export class FoldersUserSettings {
}
deleteFolder(folderToRemove) {
if (
folderToRemove &&
folderToRemove.canBeDeleted() &&
folderToRemove.deleteAccess() &&
0 === folderToRemove.privateMessageCountAll()
if (folderToRemove
&& folderToRemove.canBeDeleted()
&& folderToRemove.askDelete()
) {
this.folderForDeletion(null);
if (0 < folderToRemove.privateMessageCountAll()) {
// FolderUserStore.folderListError(getNotification(Notification.CantDeleteNonEmptyFolder));
folderToRemove.errorMsg(getNotification(Notification.CantDeleteNonEmptyFolder));
} else {
folderForDeletion(null);
if (folderToRemove) {
const fRemoveFolder = function(folder) {
if (folderToRemove === folder) {
return true;
}
folder.subFolders.remove(fRemoveFolder);
return false;
};
Local.set(ClientSideKeyName.FoldersLashHash, '');
FolderUserStore.folderList.remove(fRemoveFolder);
rl.app.foldersPromisesActionHelper(
Remote.folderDelete(folderToRemove.fullNameRaw),
Notification.CantDeleteFolder
);
removeFolderFromCacheList(folderToRemove.fullNameRaw);
if (folderToRemove) {
Remote.abort('Folders').post('FolderDelete', FolderUserStore.foldersDeleting, {
Folder: folderToRemove.fullName
}).then(
() => {
// folderToRemove.flags.push('\\nonexistent');
folderToRemove.selectable(false);
// folderToRemove.subscribed(false);
// folderToRemove.checkable(false);
if (!folderToRemove.subFolders.length) {
removeFolderFromCacheList(folderToRemove.fullName);
const folder = getFolderFromCacheList(folderToRemove.parentName);
(folder ? folder.subFolders : FolderUserStore.folderList).remove(folderToRemove);
}
},
error => {
FolderUserStore.folderListError(
getNotification(error.code, '', Notification.CantDeleteFolder)
+ '.\n' + error.message
);
}
);
}
}
} else if (0 < folderToRemove.privateMessageCountAll()) {
FolderUserStore.folderListError(getNotification(Notification.CantDeleteNonEmptyFolder));
}
}
subscribeFolder(folder) {
Local.set(ClientSideKeyName.FoldersLashHash, '');
Remote.folderSetSubscribe(()=>{}, folder.fullNameRaw, true);
folder.subscribed(true);
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);
}
unSubscribeFolder(folder) {
Local.set(ClientSideKeyName.FoldersLashHash, '');
Remote.folderSetSubscribe(()=>{}, folder.fullNameRaw, false);
folder.subscribed(false);
toggleFolderSubscription(folder) {
let subscribe = !folder.subscribed();
Remote.request('FolderSubscribe', null, {
Folder: folder.fullName,
Subscribe: subscribe ? 1 : 0
});
folder.subscribed(subscribe);
}
checkableTrueFolder(folder) {
Remote.folderSetCheckable(()=>{}, folder.fullNameRaw, true);
folder.checkable(true);
}
checkableFalseFolder(folder) {
Remote.folderSetCheckable(()=>{}, folder.fullNameRaw, false);
folder.checkable(false);
toggleFolderCheckable(folder) {
let checkable = !folder.checkable();
Remote.request('FolderCheckable', null, {
Folder: folder.fullName,
Checkable: checkable ? 1 : 0
});
folder.checkable(checkable);
}
}

View file

@ -1,12 +1,13 @@
import ko from 'ko';
import { MESSAGES_PER_PAGE_VALUES } from 'Common/Consts';
import { SaveSettingsStep } from 'Common/Enums';
import { EditorDefaultType, Layout } from 'Common/EnumsUser';
import { SettingsGet } from 'Common/Globals';
import { isArray, settingsSaveHelperSimpleFunction, addObservablesTo, addSubscribablesTo } from 'Common/Utils';
import { i18n, trigger as translatorTrigger, reload as translatorReload, convertLangName } from 'Common/Translator';
import { Settings, SettingsGet } from 'Common/Globals';
import { isArray } from 'Common/Utils';
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 { AppUserStore } from 'Stores/User/App';
@ -15,27 +16,33 @@ import { SettingsUserStore } from 'Stores/User/Settings';
import { IdentityUserStore } from 'Stores/User/Identity';
import { NotificationUserStore } from 'Stores/User/Notification';
import { MessageUserStore } from 'Stores/User/Message';
import { MessagelistUserStore } from 'Stores/User/Messagelist';
import Remote from 'Remote/User/Fetch';
import { IdentityPopupView } from 'View/Popup/Identity';
import { LanguagesPopupView } from 'View/Popup/Languages';
export class GeneralUserSettings {
export class UserSettingsGeneral extends AbstractViewSettings {
constructor() {
super();
this.language = LanguageStore.language;
this.languages = LanguageStore.languages;
this.messageReadDelay = SettingsUserStore.messageReadDelay;
this.messagesPerPage = SettingsUserStore.messagesPerPage;
this.messagesPerPageArray = MESSAGES_PER_PAGE_VALUES;
this.editorDefaultType = SettingsUserStore.editorDefaultType;
this.layout = SettingsUserStore.layout;
this.enableSoundNotification = NotificationUserStore.enableSoundNotification;
this.soundNotification = NotificationUserStore.enableSoundNotification;
this.notificationSound = ko.observable(SettingsGet('NotificationSound'));
this.notificationSounds = ko.observableArray(SettingsGet('NewMailSounds'));
this.enableDesktopNotification = NotificationUserStore.enableDesktopNotification;
this.isDesktopNotificationDenied = NotificationUserStore.isDesktopNotificationDenied;
this.desktopNotification = NotificationUserStore.enableDesktopNotification;
this.isDesktopNotificationAllowed = NotificationUserStore.isDesktopNotificationAllowed;
this.viewHTML = SettingsUserStore.viewHTML;
this.showImages = SettingsUserStore.showImages;
this.removeColors = SettingsUserStore.removeColors;
this.useCheckboxesInList = SettingsUserStore.useCheckboxesInList;
@ -44,97 +51,87 @@ export class GeneralUserSettings {
this.replySameFolder = SettingsUserStore.replySameFolder;
this.allowLanguagesOnSettings = !!SettingsGet('AllowLanguagesOnSettings');
this.languageFullName = ko.computed(() => convertLangName(this.language()));
this.languageTrigger = ko.observable(SaveSettingsStep.Idle).extend({ debounce: 100 });
addObservablesTo(this, {
mppTrigger: SaveSettingsStep.Idle,
editorDefaultTypeTrigger: SaveSettingsStep.Idle,
layoutTrigger: SaveSettingsStep.Idle
});
this.languageTrigger = ko.observable(SaveSettingsStep.Idle);
this.identities = IdentityUserStore;
this.identityMain = ko.computed(() => {
const list = this.identities();
return isArray(list) ? list.find(item => item && !item.id()) : null;
addComputablesTo(this, {
languageFullName: () => convertLangName(this.language()),
identityMain: () => {
const list = this.identities();
return isArray(list) ? list.find(item => item && !item.id()) : null;
},
identityMainDesc: () => {
const identity = this.identityMain();
return identity ? identity.formattedName() : '---';
},
editorDefaultTypes: () => {
translatorTrigger();
return [
{ id: EditorDefaultType.Html, name: i18n('SETTINGS_GENERAL/LABEL_EDITOR_HTML') },
{ id: EditorDefaultType.Plain, name: i18n('SETTINGS_GENERAL/LABEL_EDITOR_PLAIN') },
{ id: EditorDefaultType.HtmlForced, name: i18n('SETTINGS_GENERAL/LABEL_EDITOR_HTML_FORCED') },
{ id: EditorDefaultType.PlainForced, name: i18n('SETTINGS_GENERAL/LABEL_EDITOR_PLAIN_FORCED') }
];
},
layoutTypes: () => {
translatorTrigger();
return [
{ id: Layout.NoPreview, name: i18n('SETTINGS_GENERAL/LABEL_LAYOUT_NO_SPLIT') },
{ id: Layout.SidePreview, name: i18n('SETTINGS_GENERAL/LABEL_LAYOUT_VERTICAL_SPLIT') },
{ id: Layout.BottomPreview, name: i18n('SETTINGS_GENERAL/LABEL_LAYOUT_HORIZONTAL_SPLIT') }
];
}
});
this.identityMainDesc = ko.computed(() => {
const identity = this.identityMain();
return identity ? identity.formattedName() : '---';
});
this.addSetting('EditorDefaultType');
this.addSetting('MessageReadDelay');
this.addSetting('MessagesPerPage');
this.addSetting('Layout', () => MessagelistUserStore([]));
this.editorDefaultTypes = ko.computed(() => {
translatorTrigger();
return [
{ id: EditorDefaultType.Html, name: i18n('SETTINGS_GENERAL/LABEL_EDITOR_HTML') },
{ id: EditorDefaultType.Plain, name: i18n('SETTINGS_GENERAL/LABEL_EDITOR_PLAIN') },
{ id: EditorDefaultType.HtmlForced, name: i18n('SETTINGS_GENERAL/LABEL_EDITOR_HTML_FORCED') },
{ id: EditorDefaultType.PlainForced, name: i18n('SETTINGS_GENERAL/LABEL_EDITOR_PLAIN_FORCED') }
];
});
this.layoutTypes = ko.computed(() => {
translatorTrigger();
return [
{ id: Layout.NoPreview, name: i18n('SETTINGS_GENERAL/LABEL_LAYOUT_NO_SPLIT') },
{ id: Layout.SidePreview, name: i18n('SETTINGS_GENERAL/LABEL_LAYOUT_VERTICAL_SPLIT') },
{ id: Layout.BottomPreview, name: i18n('SETTINGS_GENERAL/LABEL_LAYOUT_HORIZONTAL_SPLIT') }
];
});
this.addSettings(['ViewHTML', 'ShowImages', 'UseCheckboxesInList', 'ReplySameFolder',
'DesktopNotifications', 'SoundNotification']);
const fReloadLanguageHelper = (saveSettingsStep) => () => {
this.languageTrigger(saveSettingsStep);
setTimeout(() => this.languageTrigger(SaveSettingsStep.Idle), 1000);
};
addSubscribablesTo(this, {
language: value => {
this.languageTrigger(SaveSettingsStep.Animate);
translatorReload(false, value)
.then(fReloadLanguageHelper(SaveSettingsStep.TrueResult),
fReloadLanguageHelper(SaveSettingsStep.FalseResult))
.then(fReloadLanguageHelper(SaveSettingsStep.TrueResult), fReloadLanguageHelper(SaveSettingsStep.FalseResult))
.then(() => Remote.saveSetting('Language', value));
},
editorDefaultType: value => Remote.saveSetting('EditorDefaultType', value,
settingsSaveHelperSimpleFunction(this.editorDefaultTypeTrigger, this)),
messagesPerPage: value => Remote.saveSetting('MPP', value,
settingsSaveHelperSimpleFunction(this.mppTrigger, this)),
showImages: value => Remote.saveSetting('ShowImages', value ? 1 : 0),
removeColors: value => {
MessageUserStore.messagesBodiesDom().innerHTML = '';
Remote.saveSetting('RemoveColors', value ? 1 : 0);
let dom = MessageUserStore.bodiesDom();
if (dom) {
dom.innerHTML = '';
}
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),
replySameFolder: value => Remote.saveSetting('ReplySameFolder', value ? 1 : 0),
notificationSound: value => {
Remote.saveSetting('NotificationSound', value);
Settings.set('NotificationSound', value);
},
useThreads: value => {
MessageUserStore.list([]);
Remote.saveSetting('UseThreads', value ? 1 : 0);
},
layout: value => {
MessageUserStore.list([]);
Remote.saveSetting('Layout', value, settingsSaveHelperSimpleFunction(this.layoutTrigger, this));
MessagelistUserStore([]);
Remote.saveSetting('UseThreads', value);
}
});
}
editMainIdentity() {
const identity = this.identityMain();
if (identity) {
showScreenPopup(IdentityPopupView, [identity]);
}
identity && showScreenPopup(IdentityPopupView, [identity]);
}
testSoundNotification() {

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