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 && \ RUN docker-php-ext-configure intl && \
docker-php-ext-configure ldap && \ docker-php-ext-configure ldap && \
docker-php-ext-configure gd --with-freetype-dir=/usr/include/ --with-jpeg-dir=/usr/include/ && \ docker-php-ext-configure gd --with-freetype=/usr/include/ --with-jpeg=/usr/include/ && \
docker-php-ext-install opcache pdo_mysql zip intl gd ldap docker-php-ext-install opcache pdo_mysql zip intl gd ldap
RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer

View file

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

View file

@ -1,4 +1,4 @@
; SnappyMail Webmail configuration file ; SnappyMail configuration file
; Please don't add custom parameters here, those will be overwritten ; Please don't add custom parameters here, those will be overwritten
[webmail] [webmail]
@ -30,35 +30,24 @@ allow_additional_identities = On
; Number of messages displayed on page by default ; Number of messages displayed on page by default
messages_per_page = 20 messages_per_page = 20
; Mark message read after N seconds
message_read_delay = 5
; File size limit (MB) for file upload on compose screen ; File size limit (MB) for file upload on compose screen
; 0 for unlimited. ; 0 for unlimited.
attachment_size_limit = 25 attachment_size_limit = 2
[interface] [interface]
show_attachment_thumbnail = On show_attachment_thumbnail = On
use_native_scrollbars = Off
new_move_to_folder_button = On 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] [contacts]
; Enable contacts ; Enable contacts
enable = Off enable = Off
allow_sync = On allow_sync = Off
sync_interval = 20 sync_interval = 20
type = "sqlite" 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_user = "root"
pdo_password = "" pdo_password = ""
suggestions_limit = 30 suggestions_limit = 30
@ -67,23 +56,23 @@ suggestions_limit = 30
; Enable CSRF protection (http://en.wikipedia.org/wiki/Cross-site_request_forgery) ; Enable CSRF protection (http://en.wikipedia.org/wiki/Cross-site_request_forgery)
csrf_protection = On csrf_protection = On
custom_server_signature = "SnappyMail" custom_server_signature = "SnappyMail"
x_frame_options_header = "" x_frame_options_header = "DENY"
x_xss_protection_header = "1; mode=block" x_xss_protection_header = "1; mode=block"
openpgp = Off openpgp = Off
; Login and password for web admin panel ; Login and password for web admin panel
admin_login = "admin" admin_login = "admin"
admin_password = "12345" admin_password = ""
admin_totp = ""
; Access settings ; Access settings
allow_admin_panel = On allow_admin_panel = On
allow_two_factor_auth = Off hide_x_mailer_header = On
force_two_factor_auth = Off
hide_x_mailer_header = Off
admin_panel_host = "" admin_panel_host = ""
admin_panel_key = "admin" admin_panel_key = "admin"
content_security_policy = "" content_security_policy = ""
core_install_access_domain = "" csp_report = Off
encrypt_cipher = "aes-256-cbc-hmac-sha1"
[ssl] [ssl]
; Require verification of SSL certificate used. ; Require verification of SSL certificate used.
@ -102,20 +91,12 @@ capath = ""
client_cert = "" client_cert = ""
[capa] [capa]
folders = On
composer = On
contacts = On contacts = On
settings = On
quota = On quota = On
help = On
reload = On
search = On search = On
search_adv = On search_adv = On
filters = On
x-templates = Off
dangerous_actions = On dangerous_actions = On
message_actions = On message_actions = On
messagelist_actions = On
attachments_actions = On attachments_actions = On
[login] [login]
@ -125,10 +106,7 @@ default_domain = ""
allow_languages_on_login = On allow_languages_on_login = On
determine_user_language = On determine_user_language = On
determine_user_domain = Off determine_user_domain = Off
welcome_page = Off
hide_submit_button = On hide_submit_button = On
forgot_password_link_url = ""
registration_link_url = ""
login_lowercase = On login_lowercase = On
; This option allows webmail to remember the logged in user ; This option allows webmail to remember the logged in user
@ -155,6 +133,7 @@ view_editor_type = "Html"
view_layout = 1 view_layout = 1
view_use_checkboxes = On view_use_checkboxes = On
autologout = 30 autologout = 30
view_html = On
show_images = Off show_images = Off
contacts_autosave = On contacts_autosave = On
mail_use_threads = Off mail_use_threads = Off
@ -165,6 +144,17 @@ mail_reply_same_folder = Off
; Enable logging ; Enable logging
enable = Off enable = Off
; Log messages of set RFC 5424 section 6.2.1 Severity level and higher (0 = highest, 7 = lowest).
; 0 = Emergency
; 1 = Alert
; 2 = Critical
; 3 = Error
; 4 = Warning
; 5 = Notice
; 6 = Informational
; 7 = Debug
level = 4
; Logs entire request only if error occured (php requred) ; Logs entire request only if error occured (php requred)
write_on_error_only = Off write_on_error_only = Off
@ -177,9 +167,8 @@ write_on_timeout_only = 0
; Required for development purposes only. ; Required for development purposes only.
; Disabling this option is not recommended. ; Disabling this option is not recommended.
hide_passwords = On hide_passwords = On
time_offset = "0" time_zone = "UTC"
session_filter = "" session_filter = ""
sentry_dsn = ""
; Log filename. ; Log filename.
; For security reasons, some characters are removed from filename. ; For security reasons, some characters are removed from filename.
@ -207,6 +196,7 @@ sentry_dsn = ""
; filename = "log-{date:Y-m-d}.txt" ; filename = "log-{date:Y-m-d}.txt"
; filename = "{date:Y-m-d}/{user:domain}/{user:email}_{user:uid}.log" ; filename = "{date:Y-m-d}/{user:domain}/{user:email}_{user:uid}.log"
; filename = "{user:email}-{date:Y-m-d}.txt" ; filename = "{user:email}-{date:Y-m-d}.txt"
; filename = "syslog"
filename = "log-{date:Y-m-d}.txt" filename = "log-{date:Y-m-d}.txt"
; Enable auth logging in a separate file (for fail2ban) ; 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_filename = "fail2ban/auth-{date:Y-m-d}.txt"
auth_logging_format = "[{date:Y-m-d H:i:s}] Auth failed: ip={request:ip} user={imap:login} host={imap:host} port={imap:port}" auth_logging_format = "[{date:Y-m-d H:i:s}] Auth failed: ip={request:ip} user={imap:login} host={imap:host} port={imap:port}"
; Enable auth logging to syslog for fail2ban
auth_syslog = On
[debug] [debug]
; Special option required for development purposes ; Special option required for development purposes
enable = Off enable = Off
[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] [cache]
; The section controls caching of the entire application. ; The section controls caching of the entire application.
; ;
@ -252,7 +220,7 @@ enable = On
; Additional caching key. If changed, cache is purged ; Additional caching key. If changed, cache is purged
index = "v1" index = "v1"
; Can be: files, APC, memcache, redis (beta) ; Can be: files, APCU, memcache, redis (beta)
fast_cache_driver = "files" fast_cache_driver = "files"
; Additional caching key. If changed, fast cache is purged ; Additional caching key. If changed, fast cache is purged
@ -268,33 +236,21 @@ http_expires = 3600
server_uids = On server_uids = On
[labs] [labs]
; Experimental settings. Handle with care. allow_prefetch = Off
;
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
cache_system_data = On cache_system_data = On
date_from_headers = On date_from_headers = On
autocreate_system_folders = On autocreate_system_folders = Off
allow_message_append = Off allow_message_append = Off
disable_iconv_if_mbstring_supported = Off
login_fault_delay = 1 login_fault_delay = 1
log_ajax_response_write_limit = 300 log_ajax_response_write_limit = 300
allow_html_editor_source_button = Off
allow_html_editor_biti_buttons = Off allow_html_editor_biti_buttons = Off
allow_ctrl_enter_on_compose = On allow_ctrl_enter_on_compose = On
try_to_detect_hidden_images = Off try_to_detect_hidden_images = Off
hide_dangerous_actions = Off
use_app_debug_js = Off use_app_debug_js = Off
use_mobile_version_for_tablets = Off use_mobile_version_for_tablets = Off
use_app_debug_css = Off use_app_debug_css = Off
use_imap_sort = On use_imap_sort = On
use_imap_force_selection = Off use_imap_force_selection = Off
use_imap_list_subscribe = On
use_imap_thread = On use_imap_thread = On
use_imap_move = Off use_imap_move = Off
use_imap_expunge_all_on_delete = Off use_imap_expunge_all_on_delete = Off
@ -309,51 +265,41 @@ imap_message_all_headers = Off
imap_large_thread_limit = 50 imap_large_thread_limit = 50
imap_folder_list_limit = 200 imap_folder_list_limit = 200
imap_show_login_alert = On imap_show_login_alert = On
imap_use_auth_plain = On imap_use_list_status = On
imap_use_auth_cram_md5 = Off imap_timeout = 300
smtp_show_server_errors = Off smtp_show_server_errors = Off
smtp_use_auth_plain = On smtp_timeout = 60
smtp_use_auth_cram_md5 = Off
sieve_allow_raw_script = Off
sieve_utf8_folder_name = On
sieve_auth_plain_initial = On sieve_auth_plain_initial = On
sieve_allow_fileinto_inbox = Off sieve_allow_fileinto_inbox = Off
imap_timeout = 300
smtp_timeout = 60
sieve_timeout = 10 sieve_timeout = 10
domain_list_limit = 99 sasl_allow_plain = On
sasl_allow_scram_sha = Off
sasl_allow_cram_md5 = Off
mail_func_clear_headers = On mail_func_clear_headers = On
mail_func_additional_parameters = Off mail_func_additional_parameters = Off
favicon_status = On favicon_status = On
folders_spec_limit = 50 folders_spec_limit = 50
owncloud_save_folder = "Attachments"
owncloud_suggestions = On
curl_proxy = "" curl_proxy = ""
curl_proxy_auth = "" curl_proxy_auth = ""
in_iframe = Off in_iframe = Off
force_https = 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 http_client_ip_check_proxy = Off
fast_cache_memcache_host = "127.0.0.1" fast_cache_memcache_host = "127.0.0.1"
fast_cache_memcache_port = 11211 fast_cache_memcache_port = 11211
fast_cache_redis_host = "127.0.0.1" fast_cache_redis_host = "127.0.0.1"
fast_cache_redis_port = 6379 fast_cache_redis_port = 6379
use_local_proxy_for_external_images = Off use_local_proxy_for_external_images = On
detect_image_exif_orientation = On detect_image_exif_orientation = On
cookie_default_path = "" cookie_default_path = ""
cookie_default_secure = Off cookie_default_secure = Off
check_new_messages = On check_new_messages = On
replace_env_in_configuration = "" replace_env_in_configuration = ""
startup_url = ""
strict_html_parser = Off strict_html_parser = Off
allow_cmd = Off boundary_prefix = ""
kolab_enabled = Off
dev_email = "" dev_email = ""
dev_password = "" dev_password = ""
[version] [version]
current = "1.14.0" current = "2.13.4"
saved = "Wed, 08 Apr 2020 16:37:27 +0000" saved = "Fri, 04 Mar 2022 08:55:26 +0000"

View file

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

1
.github/FUNDING.yml vendored
View file

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

8
.gitignore vendored
View file

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

3
.gitmodules vendored Normal file
View file

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

View file

@ -1,6 +1,8 @@
<IfModule mod_rewrite.c>
RewriteEngine On RewriteEngine On
# Redirect cPanel # Redirect cPanel
RewriteRule cpsess.* https://%{HTTP_HOST}/ [L,R=301] RewriteRule cpsess.* https://%{HTTP_HOST}/ [L,R=301]
</IfModule>
<IfModule mod_expires.c> <IfModule mod_expires.c>
ExpiresActive On ExpiresActive On
@ -18,51 +20,18 @@ RewriteRule cpsess.* https://%{HTTP_HOST}/ [L,R=301]
</IfModule> </IfModule>
<IfModule mod_headers.c> <IfModule mod_headers.c>
Header set Cache-Control "public, max-age=31536000" # 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 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 Referrer-Policy "no-referrer"
Header set Strict-Transport-Security "max-age=31536000" Header set Strict-Transport-Security "max-age=31536000"
Header set imagetoolbar "no" Header set imagetoolbar "no"
Header set X-Content-Type-Options "nosniff" # Header set X-Content-Type-Options "nosniff"
Header set X-Frame-Options "DENY" # Header set X-Frame-Options "DENY"
Header set X-XSS-Protection "1; mode=block" # Header set X-XSS-Protection "1; mode=block"
Header set Service-Worker-Allowed "/" Header set Service-Worker-Allowed "/"
RewriteCond %{HTTP:Accept-encoding} br # Google FLoC
RewriteCond "%{REQUEST_FILENAME}\.br" -s # Header set Permissions-Policy "interest-cohort=()"
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>
</IfModule> </IfModule>
#<IfModule mod_brotli.c> #<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** **Getting started**
1. Install node.js - `https://nodejs.org/download/` 1. Install PHP
2. Install yarn - `https://yarnpkg.com/en/docs/install` 2. Install node.js - `https://nodejs.org/download/`
3. Install gulp - `npm install gulp -g` 3. Install yarn - `https://yarnpkg.com/en/docs/install`
4. Fork snappymail - `https://github.com/the-djmaze/snappymail/issues/new#fork-destination-box` 4. Install gulp - `npm install gulp -g`
5. Clone snappymail - `git clone git@github.com:USERNAME/snappymail.git snappymail` 5. Fork snappymail from https://github.com/the-djmaze/snappymail
6. `cd snappymail` 6. Clone snappymail - `git clone git@github.com:USERNAME/snappymail.git snappymail`
7. Install install all dependencies - `yarn install` 7. `cd snappymail`
8. Run gulp - `gulp` 8. Install all dependencies - `yarn install`
9. Run gulp - `gulp`
--- ---
@ -30,6 +31,17 @@
1. Edit data/\_data_/\_default_/configs/application.ini 1. Edit data/\_data_/\_default_/configs/application.ini
2. Set 'cache_system_data' to Off 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. If you have any questions, open an issue or email support@snappymail.eu.

View file

@ -1,75 +1,75 @@
#!make #!make
rebuild: _down rebuild: _down
docker-compose build --no-cache docker compose build --no-cache
up: _up status up: _up status
_up: _up:
docker-compose up -d docker compose up -d
stop: _stop status stop: _stop status
_stop: _stop:
docker-compose stop docker compose stop
down: _down status down: _down status
_down: _down:
docker-compose down docker compose down
restart: _stop _up status restart: _stop _up status
status: status:
@docker-compose ps @docker compose ps
tx: 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: console-node:
@docker-compose run --no-deps --rm node sh @docker compose run --no-deps --rm node sh
console-tx: console-tx:
@docker-compose run --no-deps --rm tx sh @docker compose run --no-deps --rm tx sh
console-php: console-php:
@docker-compose exec php sh @docker compose exec php sh
console: console-node console: console-node
logs: logs:
@docker-compose logs --tail=100 -f @docker compose logs --tail=100 -f
logs-db: logs-db:
@docker-compose logs --tail=100 -f db @docker compose logs --tail=100 -f db
logs-php: logs-php:
@docker-compose logs --tail=100 -f php @docker compose logs --tail=100 -f php
logs-node: logs-node:
@docker-compose logs --tail=100 -f node @docker compose logs --tail=100 -f node
logs-nginx: logs-nginx:
@docker-compose logs --tail=100 -f nginx @docker compose logs --tail=100 -f nginx
logs-mail: logs-mail:
@docker-compose logs --tail=100 -f mail @docker compose logs --tail=100 -f mail
logs-tx: logs-tx:
@docker-compose logs --tail=100 -f tx @docker compose logs --tail=100 -f tx
rl-lint: rl-lint:
@docker-compose run --no-deps --rm node gulp lint @docker compose run --no-deps --rm node gulp lint
rl-dev: 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: rl-compile:
@docker-compose run --no-deps --rm node gulp build @docker compose run --no-deps --rm node gulp build
rl-compile-with-source: 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: 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: 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: rl-build:
@docker-compose run --no-deps --rm node gulp all @docker compose run --no-deps --rm node gulp all
rl-build-pro: 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: yarn-install:
@docker-compose run --no-deps --rm node yarn install @docker compose run --no-deps --rm node yarn install
yarn-outdated: yarn-outdated:
@docker-compose run --no-deps --rm node yarn outdated @docker compose run --no-deps --rm node yarn outdated
yarn-upgrade: 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: gpg:
docker run -it --rm -w=/var/www \ docker run -it --rm -w=/var/www \

116
README.md
View file

@ -1,6 +1,6 @@
<div align="center"> <div align="center">
<a href="https://github.com/the-djmaze/snappymail"> <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> </a>
<br> <br>
<h1>SnappyMail</h1> <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)**. **GNU AFFERO GENERAL PUBLIC LICENSE Version 3 (AGPL)**.
http://www.gnu.org/licenses/agpl-3.0.html http://www.gnu.org/licenses/agpl-3.0.html
Copyright (c) 2020 - 2021 SnappyMail Copyright (c) 2020 - 2022 SnappyMail
Copyright (c) 2013 - 2021 RainLoop Copyright (c) 2013 - 2021 RainLoop
## Modifications ## Modifications
@ -45,24 +45,25 @@ This fork of RainLoop has the following changes:
* Admin uses password_hash/password_verify * Admin uses password_hash/password_verify
* Auth failed attempts written to syslog * Auth failed attempts written to syslog
* Added Fail2ban instructions * Added Fail2ban instructions
* ES2015 * ES2018
* PHP 7.3+ required * PHP 7.3+ required
* PHP mbstring extension required * PHP mbstring extension required
* PHP replaced pclZip with PharData and ZipArchive * PHP replaced pclZip with PharData and ZipArchive
* PHP yaml extension else use the old Spyc * Dark mode
* Added option to remove background/font colors from messages for real "dark mode" * Added option to remove background/font colors from messages for real "dark mode"
* Removed BackwardCapability (class \RainLoop\Account) * Removed BackwardCapability (class \RainLoop\Account)
* Removed ChangePassword (re-implemented as plugin) * Removed ChangePassword (re-implemented as plugin)
* Removed OAuth support
* Removed POP3 support * Removed POP3 support
* Removed background video support * Removed background video support
* Removed Sentry (Application Monitoring and Error Tracking Software) * Removed Sentry (Application Monitoring and Error Tracking Software)
* Removed Spyc yaml
* Replaced gulp-uglify with gulp-terser * Replaced gulp-uglify with gulp-terser
* CRLF => LF line endings * CRLF => LF line endings
* Embed boot.js and boot.css into index.html * Embed boot.js and boot.css into index.html
* Ongoing removal of old JavaScript code (things are native these days) * Ongoing removal of old JavaScript code (things are native these days)
* Added modified [Squire](https://github.com/neilj/Squire) HTML editor as replacement for CKEditor * Added modified [Squire](https://github.com/neilj/Squire) HTML editor as replacement for CKEditor
* Split Admin specific JavaScript code from User code * Split Admin specific JavaScript code from User code
* Split Sieve specific JavaScript code from User code
* JSON reviver * JSON reviver
* Better memory garbage collection management * Better memory garbage collection management
* Added serviceworker for Notifications * Added serviceworker for Notifications
@ -71,27 +72,49 @@ This fork of RainLoop has the following changes:
* Replaced webpack with rollup * Replaced webpack with rollup
* No user-agent detection (use device width) * No user-agent detection (use device width)
* Added support to load plugins as .phar * 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 ### 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). 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 * Added dev/prototype.js for some additional features
* boot.js without webpack overhead * boot.js without webpack overhead
* Modified Jua.js to be without jQuery * Modified Jua.js to be without jQuery
* Replaced ProgressJS with simple native dropin
* Replaced Autolinker with simple https/email detection * Replaced Autolinker with simple https/email detection
* Replaced ifvisible.js with simple drop-in replacement * Replaced ifvisible.js with simple drop-in replacement
* Replaced momentToNode with proper HTML5 <time> * Replaced momentToNode with proper HTML5 `<time>`
* Replaced resize listeners with ResizeObserver * Replaced resize listeners with ResizeObserver
* Replaced bootstrap.js with native drop-in replacement * Replaced bootstrap.js with native drop-in replacement
* Replaced dev/Common/ClientStorageDriver/* with Web Storage Objects polyfill * Replaced dev/Common/ClientStorageDriver/* with Web Storage Objects polyfill
* Replaced *Ajax with *Fetch classes because we use the Fetch API, not jQuery.ajax * 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 knockout-sortable with native HTML5 drag&drop
* Replaced simplestatemanager with CSS @media * Replaced simplestatemanager with CSS @media
* Replaced inputosaurus with own code * 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 momentjs (use Intl)
* Removed opentip (use CSS) * Removed opentip (use CSS)
* Removed non-community (aka Prem/Premium/License) code * Removed non-community (aka Prem/Premium/License) code
* Removed ProgressJS
RainLoop 1.15 vs SnappyMail RainLoop 1.15 vs SnappyMail
|js/* |RainLoop |Snappy | |js/* |RainLoop |Snappy |
|--------------- |--------: |--------: | |--------------- |--------: |--------: |
|admin.js |2.158.025 | 94.549 | |admin.js |2.158.025 | 79.018 |
|app.js |4.215.733 | 458.762 | |app.js |4.215.733 | 407.697 |
|boot.js | 672.433 | 4.554 | |boot.js | 672.433 | 2.025 |
|libs.js | 647.679 | 218.511 | |libs.js | 647.679 | 200.131 |
|sieve.js | 0 | 75.642 |
|polyfills.js | 325.908 | 0 | |polyfills.js | 325.908 | 0 |
|serviceworker.js | 0 | 285 | |serviceworker.js | 0 | 285 |
|TOTAL |8.019.778 | 776.661 | |TOTAL |8.019.778 | 764.798 |
|js/min/* |RainLoop |Snappy |RL gzip |SM gzip |RL brotli |SM brotli | |js/min/* |RainLoop |Snappy |RL gzip |SM gzip |RL brotli |SM brotli |
|--------------- |--------: |--------: |------: |------: |--------: |--------: | |--------------- |--------: |--------: |------: |------: |--------: |--------: |
|admin.min.js | 255.514 | 49.871 | 73.899 | 14.771 | 60.674 | 13.213 | |admin.min.js | 255.514 | 39.256 | 73.899 | 13.076 | 60.674 | 11.702 |
|app.min.js | 516.000 | 235.474 |140.430 | 69.223 |110.657 | 58.398 | |app.min.js | 516.000 | 194.148 |140.430 | 62.297 |110.657 | 53.432 |
|boot.min.js | 66.456 | 2.442 | 22.553 | 1.371 | 20.043 | 1.178 | |boot.min.js | 66.456 | 1.252 | 22.553 | 782 | 20.043 | 631 |
|libs.min.js | 574.626 | 106.574 |177.280 | 38.600 |151.855 | 34.532 | |libs.min.js | 574.626 | 96.201 |177.280 | 35.522 |151.855 | 31.746 |
|sieve.min.js | 0 | 36.632 | 0 | 9.689 | 0 | 8.770 |
|polyfills.min.js | 32.608 | 0 | 11.315 | 0 | 10.072 | 0 | |polyfills.min.js | 32.608 | 0 | 11.315 | 0 | 10.072 | 0 |
|TOTAL |1.445.204 | 394.361 |425.477 |123.965 |353.301 |107.321 | |TOTAL user |1.189.690 | 291.601 |351.061 | 98.601 |292.627 | 85.809 |
|TOTAL (no admin) |1.189.690 | 344.490 |351.061 |109.194 |292.627 | 94.108 | |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 ### CSS changes
@ -142,7 +169,7 @@ For a user its around 66% smaller and faster than traditional RainLoop.
* Themes work in mobile mode * Themes work in mobile mode
* Bugfix invalid/conflicting css rules * Bugfix invalid/conflicting css rules
* Use flexbox * Use flexbox
* Split app.css to have seperate admin.css * Split app.css to have separate admin.css
* Remove oldschool 'float' * Remove oldschool 'float'
* Remove unused css * Remove unused css
* Removed html.no-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 Internet Explorer from normalize.css
* Removed node_modules/opentip/css/opentip.css * Removed node_modules/opentip/css/opentip.css
* Removed node_modules/pikaday/css/pikaday.css * Removed node_modules/pikaday/css/pikaday.css
* Removed vendors/bootstrap/less/breadcrumbs.less * Removed unused vendors/bootstrap/less/*
* Removed vendors/bootstrap/less/navbar.less
* Removed vendors/bootstrap/less/popovers.less
* Removed vendors/bootstrap/less/progress-bars.less
* Removed vendors/bootstrap/less/scaffolding.less
* Removed vendors/bootstrap/less/sprites.less
* Removed vendors/bootstrap/less/tooltip.less
* Removed vendors/jquery-nanoscroller/nanoscroller.css * Removed vendors/jquery-nanoscroller/nanoscroller.css
* Removed vendors/jquery-letterfx/jquery-letterfx.min.css * Removed vendors/jquery-letterfx/jquery-letterfx.min.css
* Removed vendors/Progress.js/minified/progressjs.min.css * Removed vendors/Progress.js/minified/progressjs.min.css
@ -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 | |css/* |RainLoop |Snappy |RL gzip |SM gzip |SM brotli |
|------------ |-------: |-------: |------: |------: |--------: | |------------ |-------: |------: |------: |------: |--------: |
|app.css | 340.334 | 106.864 | 46,959 | 18.751 | 16.110 | |app.css | 340.334 | 80.865 | 46.959 | 16.751 | 14.420 |
|app.min.css | 274.791 | 88.077 | 39.618 | 16.788 | 14.787 | |app.min.css | 274.791 | 65.086 | 39.618 | 14.855 | 13.088 |
|boot.css | | 2.066 | | 913 | 742 | |boot.css | | 1.326 | | 664 | 545 |
|boot.min.css | | 1.696 | | 818 | 664 | |boot.min.css | | 1.071 | | 590 | 474 |
|admin.css | | 46.337 | | 9.221 | 8.058 | |admin.css | | 29.977 | | 6.795 | 5.900 |
|admin.min.css | | 37.251 | | 8.178 | 7.269 | |admin.min.css | | 24.101 | | 6.167 | 5.421 |
### PGP
RainLoop uses the old OpenPGP.js v2
SnappyMail v2.12 uses OpenPGP.js v5, GnuPG and Mailvelope.
SnappyMail is able to use and generate ECDSA and EDDSA keys, where RainLoop does not.
Since SnappyMail tries to achieve the best mobile experience, it forked OpenPGP.js to strip it down.
* remove all unused Node.js
* remove all old browsers support
See https://github.com/the-djmaze/openpgpjs for development
|OpenPGP |RainLoop |Snappy |RL gzip |SM gzip |RL brotli |SM brotli |
|--------------- |--------: |--------: |------: |-------: |--------: |--------: |
|openpgp.min.js | 330.742 | 539.642 |102.388 | 167.112 | 84.241 | 137.447 |
|openpgp.worker | 1.499 | | 824 | | 695 | |
### Squire vs CKEditor ### Squire vs CKEditor
@ -184,7 +220,7 @@ Still TODO:
| | normal | min | gzip | min gzip | | | 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 | ? | 520.035 | ? | 155.916 |
CKEditor including the 7 asset requests (css,language,plugins,icons) is 633.46 KB / 180.47 KB (gzip). 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'); //header('Strict-Transport-Security: max-age=31536000');
// Uncomment to use gzip encoded output /**
* Uncomment to use gzip compressed output
*/
//define('USE_GZIP', 1); //define('USE_GZIP', 1);
// Uncomment to enable multiple domain installation. /**
* Uncomment to use brotli compressed output
*/
//define('USE_BROTLI', 1);
/**
* Uncomment to enable multiple domain installation.
*/
//define('MULTIDOMAIN', 1); //define('MULTIDOMAIN', 1);
/** /**
* Custom 'data' folder path * Uncomment to disable APCU.
* @return string
*/ */
function __get_custom_data_full_path() //define('APP_USE_APCU_CACHE', false);
{
return ''; /**
return dirname(__DIR__) . '/snappymail-data'; * Custom 'data' folder path
return '/var/external-snappymail-data-folder'; */
} //define('APP_DATA_FOLDER_PATH', dirname(__DIR__) . '/snappymail-data/');
//define('APP_DATA_FOLDER_PATH', '/var/external-snappymail-data-folder/');
/** /**
* Additional configuration file name * Additional configuration file name
* @return string
*/ */
function __get_additional_configuration_name() //define('APP_CONFIGURATION_NAME', $_SERVER['HTTP_HOST'].'.ini');
{
return '';
return defined('APP_SITE') && 0 < strlen(APP_SITE) ? APP_SITE.'.ini' : '';
}

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

View file

@ -1,4 +1,4 @@
import 'External/Admin/ko'; import 'External/ko';
import { Settings, SettingsGet } from 'Common/Globals'; import { Settings, SettingsGet } from 'Common/Globals';
@ -16,12 +16,8 @@ class AdminApp extends AbstractApp {
this.weakPassword = ko.observable(false); this.weakPassword = ko.observable(false);
} }
bootstart() { start() {
super.bootstart(); if (!Settings.app('adminAllowed')) {
this.hideLoading();
if (!Settings.app('allowAdminPanel')) {
rl.route.root(); rl.route.root();
setTimeout(() => location.href = '/', 1); setTimeout(() => location.href = '/', 1);
} else if (SettingsGet('Auth')) { } else if (SettingsGet('Auth')) {
@ -30,8 +26,6 @@ class AdminApp extends AbstractApp {
} else { } else {
startScreens([LoginAdminScreen]); 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 * as Links from 'Common/Links';
import { doc } from 'Common/Globals'; import { doc, SettingsGet, fireEvent, addEventsListener } from 'Common/Globals';
let notificator = null, let notificator = null,
player = null, player = null,
@ -12,7 +12,7 @@ let notificator = null,
player.src = url; player.src = url;
player.play(); player.play();
name = name.trim(); name = name.trim();
dispatchEvent(new CustomEvent('audio.start', {detail:name.replace(/\.([a-z0-9]{3})$/, '') || 'audio'})); fireEvent('audio.start', name.replace(/\.([a-z0-9]{3})$/, '') || 'audio');
} }
}, },
@ -73,8 +73,7 @@ export const SMAudio = new class {
this.supportedOgg = canPlay('audio/ogg; codecs="vorbis"'); this.supportedOgg = canPlay('audio/ogg; codecs="vorbis"');
if (player) { if (player) {
const stopFn = () => this.pause(); const stopFn = () => this.pause();
player.addEventListener('ended', stopFn); addEventsListener(player, ['ended','error'], stopFn);
player.addEventListener('error', stopFn);
addEventListener('audio.api.stop', stopFn); addEventListener('audio.api.stop', stopFn);
} }
} }
@ -89,7 +88,7 @@ export const SMAudio = new class {
pause() { pause() {
player && player.pause(); player && player.pause();
dispatchEvent(new CustomEvent('audio.stop')); fireEvent('audio.stop');
} }
playMp3(url, name) { playMp3(url, name) {
@ -106,11 +105,11 @@ export const SMAudio = new class {
playNotification(silent) { playNotification(silent) {
if ('running' == audioCtx.state && (this.supportedMp3 || this.supportedOgg)) { if ('running' == audioCtx.state && (this.supportedMp3 || this.supportedOgg)) {
if (!notificator) { notificator = notificator || createNewObject();
notificator = createNewObject();
notificator.src = Links.staticLink('sounds/new-mail.'+ (this.supportedMp3 ? 'mp3' : 'ogg'));
}
if (notificator) { if (notificator) {
notificator.src = Links.staticLink('sounds/'
+ SettingsGet('NotificationSound')
+ (this.supportedMp3 ? '.mp3' : '.ogg'));
notificator.volume = silent ? 0.01 : 1; notificator.volume = silent ? 0.01 : 1;
notificator.play(); notificator.play();
} }

View file

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

View file

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

View file

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

View file

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

View file

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

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

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

View file

@ -1,58 +1,78 @@
import ko from 'ko'; 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; $htmlCL = doc.documentElement.classList,
export const SettingsGet = rl.settings.get;
export const dropdownVisibility = ko.observable(false).extend({ rateLimit: 0 }); elementById = id => doc.getElementById(id),
export const moveAction = ko.observable(false); exitFullscreen = () => getFullscreenElement() && (doc.exitFullscreen || doc.webkitExitFullscreen).call(doc),
export const leftPanelDisabled = ko.observable(false); getFullscreenElement = () => doc.fullscreenElement || doc.webkitFullscreenElement,
export const createElement = (name, attr) => { 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); let el = doc.createElement(name);
attr && Object.entries(attr).forEach(([k,v]) => el.setAttribute(k,v)); attr && Object.entries(attr).forEach(([k,v]) => el.setAttribute(k,v));
return el; 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 => { leftPanelDisabled.subscribe(value => {
value && moveAction() && moveAction(false); value && moveAction() && moveAction(false);
$htmlCL.toggle('rl-left-panel-disabled', value); $htmlCL.toggle('rl-left-panel-disabled', value);
}); });
moveAction.subscribe(value => value && leftPanelDisabled() && leftPanelDisabled(false)); 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 const
tpl = createElement('template'),
htmlre = /[&<>"']/g, htmlre = /[&<>"']/g,
htmlmap = { htmlmap = {
'&': '&amp;', '&': '&amp;',
@ -6,17 +11,489 @@ const
'>': '&gt;', '>': '&gt;',
'"': '&quot;', '"': '&quot;',
"'": '&#x27;' "'": '&#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 * @param {string} text
* @returns {string} * @returns {string}
*/ */
export function encodeHtml(text) { encodeHtml = text => (text && text.toString ? text.toString() : '' + text).replace(htmlre, m => htmlmap[m]),
return (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();
} }
class HtmlEditor { tpl.content.querySelectorAll('*').forEach(oElement => {
const name = oElement.tagName,
oStyle = oElement.style;
// \MailSo\Base\HtmlUtils::ClearTags()
if (disallowedTags.includes(name)
|| 'none' == oStyle.display
|| 'hidden' == oStyle.visibility
// || (oStyle.lineHeight && 1 > parseFloat(oStyle.lineHeight)
// || (oStyle.maxHeight && 1 > parseFloat(oStyle.maxHeight)
// || (oStyle.maxWidth && 1 > parseFloat(oStyle.maxWidth)
// || ('0' === oStyle.opacity
|| (nonEmptyTags.includes(name) && ('' == oElement.textContent.trim() && !oElement.querySelector('img')))
) {
oElement.remove();
return;
}
// if (['CENTER','FORM'].includes(name)) {
if ('FORM' === name || 'O:P' === name) {
replaceWithChildren(oElement);
return;
}
/*
// Idea to allow CSS
if ('STYLE' === name) {
msgId = '#rl-msg-061eb4d647771be4185943ce91f0039d';
oElement.textContent = oElement.textContent
.replace(/[^{}]+{/g, m => msgId + ' ' + m.replace(',', ', '+msgId+' '))
.replace(/(background-)color:[^};]+/g, '');
return;
}
*/
const aAttrsForRemove = [],
hasAttribute = name => oElement.hasAttribute(name),
getAttribute = name => hasAttribute(name) ? oElement.getAttribute(name).trim() : '',
setAttribute = (name, value) => oElement.setAttribute(name, value),
delAttribute = name => oElement.removeAttribute(name);
if ('mail-body' === oElement.className) {
forEachObjectEntry(tasks, (name, cb) => {
if (hasAttribute(name)) {
cb(getAttribute(name), oElement);
delAttribute(name);
}
});
}
if (oElement.hasAttributes()) {
let i = oElement.attributes.length;
while (i--) {
let sAttrName = oElement.attributes[i].name.toLowerCase();
if (!allowedAttributes.includes(sAttrName)) {
delAttribute(sAttrName);
aAttrsForRemove.push(sAttrName);
}
}
}
let value;
if ('TABLE' === name) {
if (hasAttribute('width')) {
oStyle.width = getAttribute('width');
delAttribute('width');
}
value = oStyle.width;
if (value && !value.includes('%')) {
oStyle.maxWidth = value;
oStyle.width = '100%';
}
}
else if ('A' === name) {
value = oElement.href;
if (!/^([a-z]+):/i.test(value)) {
setAttribute('data-x-broken-href', value);
delAttribute('href');
} else {
oElement.href = stripTracking(value);
setAttribute('target', '_blank');
setAttribute('rel', 'external nofollow noopener noreferrer');
}
setAttribute('tabindex', '-1');
}
// SVG xlink:href
/*
if (hasAttribute('xlink:href')) {
delAttribute('xlink:href');
}
*/
let skipStyle = false;
if (hasAttribute('src')) {
value = getAttribute('src');
delAttribute('src');
if (detectHiddenImages
&& 'IMG' === name
&& (('' != getAttribute('height') && 3 > pInt(getAttribute('height')))
|| ('' != getAttribute('width') && 3 > pInt(getAttribute('width')))
|| [
'email.microsoftemail.com/open',
'github.com/notifications/beacon/',
'mandrillapp.com/track/open',
'list-manage.com/track/open'
].filter(uri => value.toLowerCase().includes(uri)).length
)) {
skipStyle = true;
setAttribute('style', 'display:none');
setAttribute('data-x-hidden-src', value);
}
else if (contentLocationUrls[value])
{
setAttribute('data-x-src-location', value);
result.foundContentLocationUrls.push(value);
}
else if ('cid:' === value.slice(0, 4))
{
setAttribute('data-x-src-cid', value.slice(4));
result.foundCIDs.push(value.slice(4));
}
else if (/^https?:\/\//i.test(value) || '//' === value.slice(0, 2))
{
setAttribute('data-x-src', useProxy ? proxy(value) : value);
result.hasExternals = true;
}
else if ('data:image/' === value.slice(0, 11))
{
setAttribute('src', value);
}
else
{
setAttribute('data-x-broken-src', value);
}
}
if (hasAttribute('background')) {
oStyle.backgroundImage = 'url("' + getAttribute('background') + '")';
delAttribute('background');
}
if (hasAttribute('bgcolor')) {
oStyle.backgroundColor = getAttribute('bgcolor');
delAttribute('bgcolor');
}
if (hasAttribute('color')) {
oStyle.color = getAttribute('color');
delAttribute('color');
}
if (!skipStyle) {
/*
if ('fixed' === oStyle.position) {
oStyle.position = 'absolute';
}
*/
oStyle.removeProperty('behavior');
oStyle.removeProperty('cursor');
oStyle.removeProperty('min-width');
const urls = {
cid: [], // 'data-x-style-cid'
remote: [], // 'data-x-style-url'
broken: [] // 'data-x-broken-style-src'
};
['backgroundImage', 'listStyleImage', 'content'].forEach(property => {
if (oStyle[property]) {
let value = oStyle[property],
found = value.match(/url\s*\(([^)]+)\)/gi);
if (found) {
oStyle[property] = null;
found = found[0].replace(/^["'\s]+|["'\s]+$/g, '');
let lowerUrl = found.toLowerCase();
if ('cid:' === lowerUrl.slice(0, 4)) {
found = found.slice(4);
urls.cid[property] = found
result.foundCIDs.push(found);
} else if (/http[s]?:\/\//.test(lowerUrl) || '//' === found.slice(0, 2)) {
result.hasExternals = true;
urls.remote[property] = useProxy ? proxy(found) : found;
} else if ('data:image/' === lowerUrl.slice(0, 11)) {
oStyle[property] = value;
} else {
urls.broken[property] = found;
}
}
}
});
// oStyle.removeProperty('background-image');
// oStyle.removeProperty('list-style-image');
if (urls.cid.length) {
setAttribute('data-x-style-cid', JSON.stringify(urls.cid));
}
if (urls.remote.length) {
setAttribute('data-x-style-url', JSON.stringify(urls.remote));
}
if (urls.broken.length) {
setAttribute('data-x-style-broken-urls', JSON.stringify(urls.broken));
}
if (11 > pInt(oStyle.fontSize)) {
oStyle.removeProperty('font-size');
}
// Removes background and color
// Many e-mails incorrectly only define one, not both
// And in dark theme mode this kills the readability
if (removeColors) {
oStyle.removeProperty('background-color');
oStyle.removeProperty('background-image');
oStyle.removeProperty('color');
}
// Drop Microsoft Office style properties
// oStyle.cssText = oStyle.cssText.replace(/mso-[^:;]+:[^;]+/gi, '');
}
if (debug && aAttrsForRemove.length) {
setAttribute('data-removed-attrs', aAttrsForRemove.join(', '));
}
});
// return tpl.content.firstChild;
result.html = tpl.innerHTML.trim();
return result;
},
/**
* @param {string} html
* @returns {string}
*/
htmlToPlain = html => {
const
hr = '⎯'.repeat(64),
forEach = (selector, fn) => tpl.content.querySelectorAll(selector).forEach(fn),
blockquotes = node => {
let bq;
while ((bq = node.querySelector('blockquote'))) {
// Convert child blockquote first
blockquotes(bq);
// Convert blockquote
// bq.innerHTML = '\n' + ('\n' + bq.innerHTML.replace(/\n{3,}/gm, '\n\n').trim() + '\n').replace(/^/gm, '&gt; ');
// replaceWithChildren(bq);
bq.replaceWith(
'\n' + ('\n' + bq.textContent.replace(/\n{3,}/g, '\n\n').trim() + '\n').replace(/^/gm, '> ')
);
}
};
html = html
.replace(/<pre[^>]*>([\s\S]*?)<\/pre>/gim, (...args) =>
1 < args.length ? args[1].toString().replace(/\n/g, '<br>') : '')
.replace(/\r?\n/g, '')
.replace(/\s+/gm, ' ');
while (/<(div|tr)[\s>]/i.test(html)) {
html = html.replace(/\n*<(div|tr)(\s[\s\S]*?)?>\n*/gi, '\n');
}
while (/<\/(div|tr)[\s>]/i.test(html)) {
html = html.replace(/\n*<\/(div|tr)(\s[\s\S]*?)?>\n*/gi, '\n');
}
tpl.innerHTML = html
.replace(/<t[dh](\s[\s\S]*?)?>/gi, '\t')
.replace(/<\/tr(\s[\s\S]*?)?>/gi, '\n');
// Convert line-breaks
forEach('br', br => br.replaceWith('\n'));
// lines
forEach('hr', node => node.replaceWith(`\n\n${hr}\n\n`));
// headings
forEach('h1,h2,h3,h4,h5,h6', h => h.replaceWith(`\n\n${'#'.repeat(h.tagName[1])} ${h.textContent}\n\n`));
// paragraphs
forEach('p', node => {
node.prepend('\n\n');
if ('' == node.textContent.trim()) {
node.remove();
} else {
node.after('\n\n');
}
});
// proper indenting and numbering of (un)ordered lists
forEach('ol,ul', node => {
let prefix = '',
parent = node,
ordered = 'OL' == node.tagName,
i = 0;
while (parent && parent.parentNode && parent.parentNode.closest) {
parent = parent.parentNode.closest('ol,ul');
parent && (prefix = ' ' + prefix);
}
node.querySelectorAll(':scope > li').forEach(li => {
li.prepend('\n' + prefix + (ordered ? `${++i}. ` : ' * '));
});
node.prepend('\n\n');
node.after('\n\n');
});
// Convert anchors
forEach('a', a => {
let txt = a.textContent, href = a.href;
return a.replaceWith((txt.trim() == href ? txt : txt + ' ' + href + ' '));
});
// Bold
forEach('b,strong', b => b.replaceWith(`**${b.textContent}**`));
// Italic
forEach('i,em', i => i.replaceWith(`*${i.textContent}*`));
// Blockquotes must be last
blockquotes(tpl.content);
return (tpl.content.textContent || '').replace(/\n{3,}/gm, '\n\n').trim();
},
/**
* @param {string} plain
* @param {boolean} findEmailAndLinksInText = false
* @returns {string}
*/
plainToHtml = plain => {
plain = stripTracking(plain)
.toString()
.replace(/\r/g, '')
.replace(/^>[> ]>+/gm, ([match]) => (match ? match.replace(/[ ]+/g, '') : match));
let bIn = false,
bDo = true,
bStart = true,
aNextText = [],
aText = plain.split('\n');
do {
bDo = false;
aNextText = [];
aText.forEach(sLine => {
bStart = '>' === sLine.slice(0, 1);
if (bStart && !bIn) {
bDo = true;
bIn = true;
aNextText.push('~~~blockquote~~~');
aNextText.push(sLine.slice(1));
} else if (!bStart && bIn) {
if (sLine) {
bIn = false;
aNextText.push('~~~/blockquote~~~');
aNextText.push(sLine);
} else {
aNextText.push(sLine);
}
} else if (bStart && bIn) {
aNextText.push(sLine.slice(1));
} else {
aNextText.push(sLine);
}
});
if (bIn) {
bIn = false;
aNextText.push('~~~/blockquote~~~');
}
aText = aNextText;
} while (bDo);
return aText.join('\n')
// .replace(/~~~\/blockquote~~~\n~~~blockquote~~~/g, '\n')
.replace(/&/g, '&amp;')
.replace(/>/g, '&gt;')
.replace(/</g, '&lt;')
.replace(/~~~blockquote~~~\s*/g, '<blockquote>')
.replace(/\s*~~~\/blockquote~~~/g, '</blockquote>')
.replace(/\n/g, '<br>');
};
export class HtmlEditor {
/** /**
* @param {Object} element * @param {Object} element
* @param {Function=} onBlur * @param {Function=} onBlur
@ -24,25 +501,37 @@ class HtmlEditor {
* @param {Function=} onModeChange * @param {Function=} onModeChange
*/ */
constructor(element, onBlur = null, onReady = null, onModeChange = null) { constructor(element, onBlur = null, onReady = null, onModeChange = null) {
this.editor;
this.blurTimer = 0; this.blurTimer = 0;
this.__resizable = false;
this.__inited = false;
this.onBlur = onBlur; this.onBlur = onBlur;
this.onReady = onReady;
this.onModeChange = onModeChange; this.onModeChange = onModeChange;
this.element = element; if (element) {
let editor;
this.resize = (() => { onReady = onReady ? [onReady] : [];
try { this.onReady = fn => onReady.push(fn);
this.editor && this.__resizable && this.editor.resize(element.clientWidth, element.clientHeight); const readyCallback = () => {
} catch (e) {} // eslint-disable-line no-empty this.editor = editor;
}).throttle(100); 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() { blurTrigger() {
@ -70,9 +559,9 @@ class HtmlEditor {
* @returns {void} * @returns {void}
*/ */
clearCachedSignature() { clearCachedSignature() {
this.editor && this.editor.execCommand('insertSignature', { this.onReady(() => this.editor.execCommand('insertSignature', {
clearCache: true clearCache: true
}); }));
} }
/** /**
@ -82,75 +571,66 @@ class HtmlEditor {
* @returns {void} * @returns {void}
*/ */
setSignature(signature, html, insertBefore = false) { setSignature(signature, html, insertBefore = false) {
this.editor && this.editor.execCommand('insertSignature', { this.onReady(() => this.editor.execCommand('insertSignature', {
isHtml: html, isHtml: html,
insertBefore: insertBefore, insertBefore: insertBefore,
signature: signature signature: signature
}); }));
} }
/** /**
* @param {boolean=} wrapIsHtml = false * @param {boolean=} wrapIsHtml = false
* @returns {string} * @returns {string}
*/ */
getData(wrapIsHtml = false) { getData() {
let result = ''; let result = '';
if (this.editor) { if (this.editor) {
try { try {
if (this.isPlain() && this.editor.plugins.plain && this.editor.__plain) { if (this.isPlain() && this.editor.plugins.plain && this.editor.__plain) {
result = this.editor.__plain.getRawData(); result = this.editor.__plain.getRawData();
} else { } else {
result = wrapIsHtml result = this.editor.getData();
? '<div data-html-editor-font-wrapper="true" style="font-family: arial, sans-serif; font-size: 13px;">' +
this.editor.getData() +
'</div>'
: this.editor.getData();
} }
} catch (e) {} // eslint-disable-line no-empty } catch (e) {} // eslint-disable-line no-empty
} }
return result; return result;
} }
/** /**
* @param {boolean=} wrapIsHtml = false
* @returns {string} * @returns {string}
*/ */
getDataWithHtmlMark(wrapIsHtml = false) { getDataWithHtmlMark() {
return (this.isHtml() ? ':HTML:' : '') + this.getData(wrapIsHtml); return (this.isHtml() ? ':HTML:' : '') + this.getData();
} }
modeWysiwyg() { modeWysiwyg() {
try { this.onReady(() => this.editor.setMode('wysiwyg'));
this.editor && this.editor.setMode('wysiwyg');
} catch (e) { console.error(e); }
} }
modePlain() { modePlain() {
try { this.onReady(() => this.editor.setMode('plain'));
this.editor && this.editor.setMode('plain');
} catch (e) { console.error(e); }
} }
setHtmlOrPlain(text) { setHtmlOrPlain(text) {
if (':HTML:' === text.substr(0, 6)) { if (':HTML:' === text.slice(0, 6)) {
this.setHtml(text.substr(6)); this.setHtml(text.slice(6));
} else { } else {
this.setPlain(text); this.setPlain(text);
} }
} }
setData(mode, data) { setData(mode, data) {
if (this.editor && this.__inited) { this.onReady(() => {
const editor = this.editor;
this.clearCachedSignature(); this.clearCachedSignature();
try { try {
this.editor.setMode(mode); editor.setMode(mode);
if (this.isPlain() && this.editor.plugins.plain && this.editor.__plain) { if (this.isPlain() && editor.plugins.plain && editor.__plain) {
this.editor.__plain.setRawData(data); editor.__plain.setRawData(data);
} else { } else {
this.editor.setData(data); editor.setData(data);
} }
} catch (e) { console.error(e); } } catch (e) { console.error(e); }
} });
} }
setHtml(html) { setHtml(html) {
@ -161,46 +641,8 @@ class HtmlEditor {
this.setData('plain', txt); 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() { focus() {
try { this.onReady(() => this.editor.focus());
this.editor && this.editor.focus();
} catch (e) {} // eslint-disable-line no-empty
} }
hasFocus() { hasFocus() {
@ -212,14 +654,15 @@ class HtmlEditor {
} }
blur() { blur() {
try { this.onReady(() => this.editor.focusManager.blur(true));
this.editor && this.editor.focusManager.blur(true);
} catch (e) {} // eslint-disable-line no-empty
} }
clear() { 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,36 +1,29 @@
import { pString, pInt } from 'Common/Utils'; import { pString, pInt } from 'Common/Utils';
import { Settings, SettingsGet } from 'Common/Globals'; import { Settings } from 'Common/Globals';
const const
ROOT = './',
HASH_PREFIX = '#/', HASH_PREFIX = '#/',
SERVER_PREFIX = './?', SERVER_PREFIX = './?',
VERSION = Settings.app('version'), VERSION = Settings.app('version'),
VERSION_PREFIX = Settings.app('webVersionPath') || 'snappymail/v/' + VERSION + '/', VERSION_PREFIX = () => Settings.app('webVersionPath') || 'snappymail/v/' + VERSION + '/',
getHash = () => SettingsGet('AuthAccountHash') || '0'; adminPath = () => rl.adminArea() && !Settings.app('adminHostUse'),
/** prefix = () => SERVER_PREFIX + (adminPath() ? Settings.app('adminPath') : '');
* @returns {string}
*/ export const
export const SUB_QUERY_PREFIX = '&q[]='; SUB_QUERY_PREFIX = '&q[]=',
/** /**
* @param {string=} startupUrl * @param {string=} startupUrl
* @returns {string} * @returns {string}
*/ */
export function root(startupUrl = '') { root = () => HASH_PREFIX,
return HASH_PREFIX + pString(startupUrl);
}
/** /**
* @returns {string} * @returns {string}
*/ */
export function logoutLink() { logoutLink = () => adminPath() ? prefix() : './',
return (rl.adminArea() && !Settings.app('adminHostUse'))
? SERVER_PREFIX + (Settings.app('adminPath') || 'admin')
: ROOT;
}
/** /**
* @param {string} type * @param {string} type
@ -38,154 +31,110 @@ export function logoutLink() {
* @param {string=} customSpecSuffix * @param {string=} customSpecSuffix
* @returns {string} * @returns {string}
*/ */
export function serverRequestRaw(type, hash, customSpecSuffix) { serverRequestRaw = (type, hash) =>
return SERVER_PREFIX + '/Raw/' + SUB_QUERY_PREFIX + '/' SERVER_PREFIX + '/Raw/' + SUB_QUERY_PREFIX + '/'
+ (null == customSpecSuffix ? getHash() : customSpecSuffix) + '/' + '0/' // AuthAccountHash ?
+ (type + (type
? type + '/' + (hash ? SUB_QUERY_PREFIX + '/' + hash : '') ? type + '/' + (hash ? SUB_QUERY_PREFIX + '/' + hash : '')
: '') : ''),
;
}
/** /**
* @param {string} download * @param {string} download
* @param {string=} customSpecSuffix * @param {string=} customSpecSuffix
* @returns {string} * @returns {string}
*/ */
export function attachmentDownload(download, customSpecSuffix) { attachmentDownload = (download, customSpecSuffix) =>
return serverRequestRaw('Download', download, customSpecSuffix); serverRequestRaw('Download', download, customSpecSuffix),
}
proxy = url =>
SERVER_PREFIX + '/ProxyExternal/' + btoa(url).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''),
/*
return './?/ProxyExternal/'.Utils::EncodeKeyValuesQ(array(
'Rnd' => \md5(\microtime(true)),
'Token' => Utils::GetConnectionToken(),
'Url' => $sUrl
)).'/';
*/
/** /**
* @param {string} type * @param {string} type
* @returns {string} * @returns {string}
*/ */
export function serverRequest(type) { serverRequest = type => prefix() + '/' + type + '/' + SUB_QUERY_PREFIX + '/0/',
return SERVER_PREFIX + '/' + type + '/' + SUB_QUERY_PREFIX + '/' + getHash() + '/';
}
/**
* @param {string} email
* @returns {string}
*/
export function change(email) {
return serverRequest('Change') + encodeURIComponent(email) + '/';
}
/**
* @param {string} hash
* @returns {string}
*/
export function userBackground(hash) {
return serverRequestRaw('UserBackground', hash);
}
/** /**
* @param {string} lang * @param {string} lang
* @param {boolean} isAdmin * @param {boolean} isAdmin
* @returns {string} * @returns {string}
*/ */
export function langLink(lang, isAdmin) { langLink = (lang, isAdmin) =>
return SERVER_PREFIX + '/Lang/0/' + (isAdmin ? 'Admin' : 'App') + '/' + encodeURI(lang) + '/' + VERSION + '/'; SERVER_PREFIX + '/Lang/0/' + (isAdmin ? 'Admin' : 'App') + '/' + encodeURI(lang) + '/' + VERSION + '/',
}
/** /**
* @param {string} path * @param {string} path
* @returns {string} * @returns {string}
*/ */
export function staticLink(path) { staticLink = path => VERSION_PREFIX() + 'static/' + path,
return VERSION_PREFIX + 'static/' + path;
}
/**
* @returns {string}
*/
export function openPgpJs() {
return staticLink('js/min/openpgp.min.js');
}
/**
* @returns {string}
*/
export function openPgpWorkerJs() {
return staticLink('js/min/openpgp.worker.min.js');
}
/** /**
* @param {string} theme * @param {string} theme
* @returns {string} * @returns {string}
*/ */
export function themePreviewLink(theme) { themePreviewLink = theme => {
let prefix = VERSION_PREFIX; let prefix = VERSION_PREFIX();
if ('@custom' === theme.substr(-7)) { if ('@custom' === theme.slice(-7)) {
theme = theme.substr(0, theme.length - 7).trim(); theme = theme.slice(0, theme.length - 7).trim();
prefix = Settings.app('webPath') || ''; prefix = Settings.app('webPath') || '';
} }
return prefix + 'themes/' + encodeURI(theme) + '/images/preview.png'; return prefix + 'themes/' + encodeURI(theme) + '/images/preview.png';
} },
/** /**
* @param {string} inboxFolderName = 'INBOX' * @param {string} inboxFolderName = 'INBOX'
* @returns {string} * @returns {string}
*/ */
export function mailbox(inboxFolderName = 'INBOX') { mailbox = (inboxFolderName = 'INBOX') => HASH_PREFIX + 'mailbox/' + inboxFolderName,
return HASH_PREFIX + 'mailbox/' + inboxFolderName;
}
/** /**
* @param {string=} screenName = '' * @param {string=} screenName = ''
* @returns {string} * @returns {string}
*/ */
export function settings(screenName = '') { settings = (screenName = '') => HASH_PREFIX + 'settings' + (screenName ? '/' + screenName : ''),
return HASH_PREFIX + 'settings' + (screenName ? '/' + screenName : '');
}
/** /**
* @param {string} screenName * @param {string=} screenName
* @returns {string} * @returns {string}
*/ */
export function admin(screenName) { admin = screenName => HASH_PREFIX + (
let result = HASH_PREFIX; 'AdminDomains' == screenName ? 'domains'
switch (screenName) { : 'AdminSecurity' == screenName ? 'security'
case 'AdminDomains': : ''
result += 'domains'; ),
break;
case 'AdminSecurity':
result += 'security';
break;
// no default
}
return result;
}
/** /**
* @param {string} folder * @param {string} folder
* @param {number=} page = 1 * @param {number=} page = 1
* @param {string=} search = '' * @param {string=} search = ''
* @param {string=} threadUid = '' * @param {number=} threadUid = 0
* @returns {string} * @returns {string}
*/ */
export function mailBox(folder, page = 1, search = '', threadUid = '') { mailBox = (folder, page, search, threadUid) => {
page = pInt(page, 1); let result = [HASH_PREFIX + 'mailbox'];
search = pString(search);
let result = HASH_PREFIX + 'mailbox/';
if (folder) { if (folder) {
const resultThreadUid = pInt(threadUid); result.push(folder + (threadUid ? '~' + threadUid : ''));
result += encodeURI(folder) + (0 < resultThreadUid ? '~' + resultThreadUid : '');
} }
page = pInt(page, 1);
if (1 < page) { if (1 < page) {
result = result.replace(/\/+$/, '') + '/p' + page; result.push('p' + page);
} }
search = pString(search);
if (search) { if (search) {
result = result.replace(/\/+$/, '') + '/' + encodeURI(search); result.push(encodeURI(search));
}
return result;
} }
return result.join('/');
};

View file

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

View file

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

View file

@ -2,44 +2,24 @@ import ko from 'ko';
import { Notification, UploadErrorCode } from 'Common/Enums'; import { Notification, UploadErrorCode } from 'Common/Enums';
import { langLink } from 'Common/Links'; import { langLink } from 'Common/Links';
import { doc, createElement } from 'Common/Globals'; import { doc, createElement } from 'Common/Globals';
import { getKeyByValue, forEachObjectEntry } from 'Common/Utils';
let I18N_DATA = window.snappymailI18N || {}; let I18N_DATA = {};
export const trigger = ko.observable(false); const
i18nToNode = element => {
/**
* @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; const key = element.dataset.i18n;
if (key) { if (key) {
if ('[' === key.substr(0, 1)) { if ('[' === key.slice(0, 1)) {
switch (key.substr(0, 6)) { switch (key.slice(0, 6)) {
case '[html]': case '[html]':
element.innerHTML = i18n(key.substr(6)); element.innerHTML = i18n(key.slice(6));
break; break;
case '[place': case '[place':
element.placeholder = i18n(key.substr(13)); element.placeholder = i18n(key.slice(13));
break; break;
case '[title': case '[title':
element.title = i18n(key.substr(7)); element.title = i18n(key.slice(7));
break; break;
// no default // no default
} }
@ -49,37 +29,123 @@ const i18nToNode = element => {
} }
}, },
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(), 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')] : '';
};
export const
trigger = ko.observable(false),
/**
* @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;
}
}
if (valueList) {
forEachObjectEntry(valueList, (key, value) => {
result = result.replace('%' + key + '%', value);
});
}
return result;
},
/** /**
* @param {Object} elements * @param {Object} elements
* @param {boolean=} animate = false * @param {boolean=} animate = false
*/ */
export function i18nToNodes(element) { i18nToNodes = element =>
setTimeout(() => setTimeout(() =>
element.querySelectorAll('[data-i18n]').forEach(item => i18nToNode(item)) element.querySelectorAll('[data-i18n]').forEach(item => i18nToNode(item))
, 1); , 1),
timestampToString = (timeStampInUTC, formatStr) => {
const now = Date.now(),
time = 0 < timeStampInUTC ? Math.min(now, timeStampInUTC * 1000) : (0 === timeStampInUTC ? now : 0);
if (31536000000 < time) {
const m = new Date(time);
switch (formatStr) {
case 'FROMNOW':
return m.fromNow();
case 'SHORT': {
if (4 >= (now - time) / 3600000)
return m.fromNow();
const mt = m.getTime(), date = new Date,
dt = date.setHours(0,0,0,0);
if (mt > dt)
return i18n('MESSAGE_LIST/TODAY_AT', {TIME: m.format('LT')});
if (mt > dt - 86400000)
return i18n('MESSAGE_LIST/YESTERDAY_AT', {TIME: m.format('LT')});
if (date.getFullYear() === m.getFullYear())
return m.format('d M');
return m.format('LL');
} }
case 'FULL':
return m.format('LLL');
default:
return m.format(formatStr);
}
}
return '';
},
timeToNode = (element, time) => {
try {
if (time) {
element.dateTime = (new Date(time * 1000)).format('Y-m-d\\TH:i:s');
} else {
time = Date.parse(element.dateTime) / 1000;
}
let key = element.dataset.momentFormat;
if (key) {
element.textContent = timestampToString(time, key);
}
if ((key = element.dataset.momentFormatTitle)) {
element.title = timestampToString(time, key);
}
} catch (e) {
// prevent knockout crashes
console.error(e);
}
},
reloadTime = () => setTimeout(() =>
doc.querySelectorAll('time').forEach(element => timeToNode(element))
, 1),
/** /**
* @param {Function} startCallback * @param {Function} startCallback
* @param {Function=} langCallback = null * @param {Function=} langCallback = null
*/ */
export function initOnStartOrLangChange(startCallback, langCallback = null) { initOnStartOrLangChange = (startCallback, langCallback = null) => {
startCallback && startCallback(); startCallback && startCallback();
startCallback && trigger.subscribe(startCallback); startCallback && trigger.subscribe(startCallback);
langCallback && trigger.subscribe(langCallback); 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 {number} code
@ -87,7 +153,7 @@ function getNotificationMessage(code) {
* @param {*=} defCode = null * @param {*=} defCode = null
* @returns {string} * @returns {string}
*/ */
export function getNotification(code, message = '', defCode = 0) { getNotification = (code, message = '', defCode = 0) => {
code = parseInt(code, 10) || 0; code = parseInt(code, 10) || 0;
if (Notification.ClientViewError === code && message) { if (Notification.ClientViewError === code && message) {
return message; return message;
@ -96,44 +162,31 @@ export function getNotification(code, message = '', defCode = 0) {
return getNotificationMessage(code) return getNotificationMessage(code)
|| getNotificationMessage(parseInt(defCode, 10)) || getNotificationMessage(parseInt(defCode, 10))
|| ''; || '';
} },
/** /**
* @param {*} code * @param {*} code
* @returns {string} * @returns {string}
*/ */
export function getUploadErrorDescByCode(code) { getUploadErrorDescByCode = code => {
let result = 'UNKNOWN'; let key = getKeyByValue(UploadErrorCode, parseInt(code, 10));
code = parseInt(code, 10) || 0; return i18n('UPLOAD/ERROR_' + (key ? i18nKey(key) : 'UNKNOWN'));
switch (code) { },
case UploadErrorCode.FileIsTooBig:
case UploadErrorCode.FilePartiallyUploaded:
case UploadErrorCode.NoFileUploaded:
case UploadErrorCode.MissingTempFolder:
case UploadErrorCode.OnSavingFile:
case UploadErrorCode.FileType:
result = i18nKey(getKeyByValue(UploadErrorCode, code));
break;
}
return i18n('UPLOAD/ERROR_' + result);
}
/** /**
* @param {boolean} admin * @param {boolean} admin
* @param {string} language * @param {string} language
*/ */
export function reload(admin, language) { translatorReload = (admin, language) =>
return new Promise((resolve, reject) => { new Promise((resolve, reject) => {
const script = createElement('script'); const script = createElement('script');
script.onload = () => { script.onload = () => {
// reload the data // reload the data
if (window.snappymailI18N) { if (init()) {
I18N_DATA = window.snappymailI18N;
i18nToNodes(doc); i18nToNodes(doc);
dispatchEvent(new CustomEvent('reload-time')); admin || reloadTime();
trigger(!trigger()); trigger(!trigger());
} }
window.snappymailI18N = null;
script.remove(); script.remove();
resolve(); resolve();
}; };
@ -141,8 +194,7 @@ export function reload(admin, language) {
script.src = langLink(language, admin); script.src = langLink(language, admin);
// script.async = true; // script.async = true;
doc.head.append(script); doc.head.append(script);
}); }),
}
/** /**
* *
@ -150,10 +202,11 @@ export function reload(admin, language) {
* @param {boolean=} isEng = false * @param {boolean=} isEng = false
* @returns {string} * @returns {string}
*/ */
export function convertLangName(language, isEng = false) { convertLangName = (language, isEng = false) =>
return i18n( i18n(
'LANGS_NAMES' + (true === isEng ? '_EN' : '') + '/' + language, 'LANGS_NAMES' + (true === isEng ? '_EN' : '') + '/' + language,
null, null,
language language
); );
}
init();

View file

@ -1,107 +1,50 @@
import { SaveSettingsStep } from 'Common/Enums'; import { SaveSettingsStep } from 'Common/Enums';
import { doc, createElement, elementById } from 'Common/Globals'; import { 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);
};
}
let __themeTimer = 0, let __themeTimer = 0,
__themeJson = null; __themeJson = null;
/** export const
* @param {string} value isArray = Array.isArray,
* @param {function=} themeTrigger = noop arrayLength = array => isArray(array) && array.length,
* @returns {void} isFunction = v => typeof v === 'function',
pString = value => null != value ? '' + value : '',
forEachObjectValue = (obj, fn) => Object.values(obj).forEach(fn),
forEachObjectEntry = (obj, fn) => Object.entries(obj).forEach(([key, value]) => fn(key, value)),
pInt = (value, defaultValue = 0) => {
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)
)),
*/ */
export function changeTheme(value, themeTrigger = ()=>{}) { b64EncodeJSONSafe = data => b64EncodeJSON(data).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''),
const themeLink = elementById('app-theme-link'),
changeTheme = (value, themeTrigger = ()=>0) => {
const themeStyle = elementById('app-theme-style'),
clearTimer = () => { clearTimer = () => {
__themeTimer = setTimeout(() => themeTrigger(SaveSettingsStep.Idle), 1000); __themeTimer = setTimeout(() => themeTrigger(SaveSettingsStep.Idle), 1000);
__themeJson = null; __themeJson = null;
}; },
url = themeStyle.dataset.href.replace(/(Admin|User)\/-\/[^/]+\//, '$1/-/' + value + '/') + 'Json/';
let themeStyle = elementById('app-theme-style'),
url = (themeLink && themeLink.href) || (themeStyle && themeStyle.dataset.href);
if (url) {
url = url.toString()
.replace(/\/-\/[^/]+\/-\//, '/-/' + value + '/-/')
.replace(/\/Css\/[^/]+\/User\//, '/Css/0/User/')
.replace(/\/Hash\/[^/]+\//, '/Hash/-/');
if ('Json/' !== url.substr(-5)) {
url += 'Json/';
}
clearTimeout(__themeTimer); clearTimeout(__themeTimer);
@ -117,36 +60,12 @@ export function changeTheme(value, themeTrigger = ()=>{}) {
} }
rl.fetchJSON(url, init) rl.fetchJSON(url, init)
.then(data => { .then(data => {
if (data && isArray(data) && 2 === data.length) { if (2 === arrayLength(data)) {
if (themeLink && !themeStyle) {
themeStyle = createElement('style');
themeStyle.id = 'app-theme-style';
themeLink.after(themeStyle);
themeLink.remove();
}
if (themeStyle) {
themeStyle.textContent = data[1]; themeStyle.textContent = data[1];
themeStyle.dataset.href = url;
themeStyle.dataset.theme = data[0];
}
themeTrigger(SaveSettingsStep.TrueResult); themeTrigger(SaveSettingsStep.TrueResult);
} }
}) })
.then(clearTimer, clearTimer); .then(clearTimer, clearTimer);
} },
}
export function addObservablesTo(target, observables) { getKeyByValue = (o, v) => Object.keys(o).find(key => o[key] === v);
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));
}

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 { EmailModel } from 'Model/Email';
import { encodeHtml } from 'Common/Html'; import { MessageModel } from 'Model/Message';
import { isArray } from 'Common/Utils'; import { MessageUserStore } from 'Stores/User/Message';
import { createElement } from 'Common/Globals'; import { MessagelistUserStore } from 'Stores/User/Messagelist';
import { SettingsUserStore } from 'Stores/User/Settings';
import * as Local from 'Storage/Client';
import { ThemeStore } from 'Stores/Theme';
import Remote from 'Remote/User/Fetch';
export const
dropdownsDetectVisibility = (() =>
dropdownVisibility(!!dropdowns.find(item => item.classList.contains('show')))
).debounce(50),
/** /**
* @param {(string|number)} value * @param {string} link
* @param {boolean=} includeZero = true
* @returns {boolean} * @returns {boolean}
*/ */
export function isPosNumeric(value, includeZero = true) { download = (link, name = "") => {
return null != value && (includeZero ? /^[0-9]*$/ : /^[1-9]+[0-9]*$/).test(value.toString()); if (ThemeStore.isMobile()) {
} open(link, '_self');
focus();
/**
* @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);
}
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 { } else {
pos = 0; const oLink = createElement('a');
} oLink.href = link;
} else { oLink.target = '_blank';
break; oLink.download = name;
} doc.body.appendChild(oLink).click();
} oLink.remove();
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} * @returns {function}
*/ */
export function computedPaginatorHelper(koCurrentPage, koPageCount) { computedPaginatorHelper = (koCurrentPage, koPageCount) => {
return () => { return () => {
const currentPage = koCurrentPage(), const currentPage = koCurrentPage(),
pageCount = koPageCount(), pageCount = koPageCount(),
@ -395,13 +100,13 @@ export function computedPaginatorHelper(koCurrentPage, koPageCount) {
if (3 === prev) { if (3 === prev) {
fAdd(2, false); fAdd(2, false);
} else if (3 < prev) { } else if (3 < prev) {
fAdd(Math.round((prev - 1) / 2), false, '...'); fAdd(Math.round((prev - 1) / 2), false, '');
} }
if (pageCount - 2 === next) { if (pageCount - 2 === next) {
fAdd(pageCount - 1, true); fAdd(pageCount - 1, true);
} else if (pageCount - 2 > next) { } else if (pageCount - 2 > next) {
fAdd(Math.round((pageCount + next) / 2), true, '...'); fAdd(Math.round((pageCount + next) / 2), true, '');
} }
// first and last // first and last
@ -416,40 +121,27 @@ export function computedPaginatorHelper(koCurrentPage, koPageCount) {
return result; return result;
}; };
} },
/** /**
* @param {string} mailToUrl * @param {string} mailToUrl
* @returns {boolean} * @returns {boolean}
*/ */
export function mailToHelper(mailToUrl) { mailToHelper = mailToUrl => {
if ( if (mailToUrl && 'mailto:' === mailToUrl.slice(0, 7).toLowerCase()) {
mailToUrl && mailToUrl = mailToUrl.slice(7).split('?');
'mailto:' ===
mailToUrl
.toString()
.substr(0, 7)
.toLowerCase()
) {
mailToUrl = mailToUrl.toString().substr(7);
let to = [], const
cc = null, email = mailToUrl[0],
bcc = null, params = new URLSearchParams(mailToUrl[1]),
params = {}; toEmailModel = value => null != value ? EmailModel.parseEmailLine(value) : null;
const email = mailToUrl.replace(/\?.+$/, ''), showMessageComposer([
query = mailToUrl.replace(/^[^?]*\?/, ''); ComposeType.Empty,
null,
query.split('&').forEach(temp => { params.get('to')
temp = temp.split('='); ? Object.values(
params[decodeURIComponent(temp[0])] = decodeURIComponent(temp[1]); toEmailModel(email + ',' + params.get('to')).reduce((result, value) => {
});
if (undefined !== params.to) {
to = EmailModel.parseEmailLine(decodeURIComponent(email + ',' + params.to));
to = Object.values(
to.reduce((result, value) => {
if (value) { if (value) {
if (result[value.email]) { if (result[value.email]) {
if (!result[value.email].name) { if (!result[value.email].name) {
@ -461,36 +153,221 @@ export function mailToHelper(mailToUrl) {
} }
return result; return result;
}, {}) }, {})
); )
} else { : EmailModel.parseEmailLine(email),
to = EmailModel.parseEmailLine(email); toEmailModel(params.get('cc')),
} toEmailModel(params.get('bcc')),
params.get('subject'),
if (undefined !== params.cc) { plainToHtml(params.get('body') || '')
cc = EmailModel.parseEmailLine(decodeURIComponent(params.cc));
}
if (undefined !== params.bcc) {
bcc = EmailModel.parseEmailLine(decodeURIComponent(params.bcc));
}
showMessageComposer([
ComposeType.Empty,
null,
to,
cc,
bcc,
null == params.subject ? null : decodeURIComponent(params.subject),
null == params.body ? null : plainToHtml(decodeURIComponent(params.body))
]); ]);
return true; return true;
} }
return false; return false;
} },
export function showMessageComposer(params = []) showMessageComposer = (params = []) =>
{ {
rl.app.showMessageComposer(params); rl.app.showMessageComposer(params);
},
initFullscreen = (el, fn) =>
{
let event = 'fullscreenchange';
if (!el.requestFullscreen && el.webkitRequestFullscreen) {
el.requestFullscreen = el.webkitRequestFullscreen;
event = 'webkit'+event;
} }
if (el.requestFullscreen) {
el.addEventListener(event, fn);
return el;
}
},
setLayoutResizer = (source, target, sClientSideKeyName, mode) =>
{
if (source.layoutResizer && source.layoutResizer.mode != mode) {
target.removeAttribute('style');
source.removeAttribute('style');
}
// source.classList.toggle('resizable', mode);
if (mode) {
const length = Local.get(sClientSideKeyName+mode);
if (!source.layoutResizer) {
const resizer = createElement('div', {'class':'resizer'}),
size = {},
store = () => {
if ('Width' == resizer.mode) {
target.style.left = source.offsetWidth + 'px';
Local.set(resizer.key+resizer.mode, source.offsetWidth);
} else {
target.style.top = (4 + source.offsetTop + source.offsetHeight) + 'px';
Local.set(resizer.key+resizer.mode, source.offsetHeight);
}
},
cssint = s => {
let value = getComputedStyle(source, null)[s].replace('px', '');
if (value.includes('%')) {
value = source.parentElement['offset'+resizer.mode]
* value.replace('%', '') / 100;
}
return parseFloat(value);
};
source.layoutResizer = resizer;
source.append(resizer);
resizer.addEventListener('mousedown', {
handleEvent: function(e) {
if ('mousedown' == e.type) {
const lmode = resizer.mode.toLowerCase();
e.preventDefault();
size.pos = ('width' == lmode) ? e.pageX : e.pageY;
size.min = cssint('min-'+lmode);
size.max = cssint('max-'+lmode);
size.org = cssint(lmode);
addEventListener('mousemove', this);
addEventListener('mouseup', this);
} else if ('mousemove' == e.type) {
const lmode = resizer.mode.toLowerCase(),
length = size.org + (('width' == lmode ? e.pageX : e.pageY) - size.pos);
if (length >= size.min && length <= size.max ) {
source.style[lmode] = length + 'px';
source.observer || store();
}
} else if ('mouseup' == e.type) {
removeEventListener('mousemove', this);
removeEventListener('mouseup', this);
}
}
});
source.observer = window.ResizeObserver ? new ResizeObserver(store) : null;
}
source.layoutResizer.mode = mode;
source.layoutResizer.key = sClientSideKeyName;
source.observer && source.observer.observe(source, { box: 'border-box' });
if (length) {
source.style[mode] = length + 'px';
}
} else {
source.observer && source.observer.disconnect();
}
},
populateMessageBody = (oMessage, preload) => {
if (oMessage) {
preload || MessageUserStore.hideMessageBodies();
preload || MessageUserStore.loading(true);
Remote.message((iError, oData/*, bCached*/) => {
if (iError) {
if (Notification.RequestAborted !== iError && !preload) {
MessageUserStore.message(null);
MessageUserStore.error(getNotification(iError));
}
} else {
oMessage = preload ? oMessage : null;
let
isNew = false,
json = oData && oData.Result,
message = oMessage || MessageUserStore.message();
if (
json &&
MessageModel.validJson(json) &&
message &&
message.folder === json.Folder
) {
const threads = message.threads(),
messagesDom = MessageUserStore.bodiesDom();
if (!oMessage && message.uid != json.Uid && threads.includes(json.Uid)) {
message = MessageModel.reviveFromJson(json);
if (message) {
message.threads(threads);
MessageFlagsCache.initMessage(message);
// Set clone
MessageUserStore.message(MessageModel.fromMessageListItem(message));
message = MessageUserStore.message();
isNew = true;
}
}
if (message && message.uid == json.Uid) {
oMessage || MessageUserStore.error('');
/*
if (bCached) {
delete json.Flags;
}
*/
isNew || message.revivePropertiesFromJson(json);
addRequestedMessage(message.folder, message.uid);
if (messagesDom) {
let id = 'rl-msg-' + message.hash.replace(/[^a-zA-Z0-9]/g, ''),
body = elementById(id);
if (body) {
message.body = body;
message.isHtml(body.classList.contains('html'));
message.hasImages(body.rlHasImages);
} else {
body = Element.fromHTML('<div id="' + id + '" hidden="" class="b-text-part '
+ (message.pgpSigned() ? ' openpgp-signed' : '')
+ (message.pgpEncrypted() ? ' openpgp-encrypted' : '')
+ '">'
+ '</div>');
message.body = body;
if (!SettingsUserStore.viewHTML() || !message.viewHtml()) {
message.viewPlain();
}
MessageUserStore.purgeMessageBodyCache();
}
messagesDom.append(body);
if (!oMessage) {
MessageUserStore.activeDom(message.body);
MessageUserStore.hideMessageBodies();
message.body.hidden = false;
}
oMessage && message.viewPopupMessage();
}
MessageFlagsCache.initMessage(message);
if (message.isUnseen()) {
MessageUserStore.MessageSeenTimer = setTimeout(
() => MessagelistUserStore.setAction(message.folder, MessageSetAction.SetSeen, [message]),
SettingsUserStore.messageReadDelay() * 1000 // seconds
);
}
if (message && isNew) {
let selectedMessage = MessagelistUserStore.selectedMessage();
if (
selectedMessage &&
(message.folder !== selectedMessage.folder || message.uid != selectedMessage.uid)
) {
MessagelistUserStore.selectedMessage(null);
if (1 === MessagelistUserStore.length) {
MessagelistUserStore.focusedMessage(null);
}
} else if (!selectedMessage) {
selectedMessage = MessagelistUserStore.find(
subMessage =>
subMessage &&
subMessage.folder === message.folder &&
subMessage.uid == message.uid
);
if (selectedMessage) {
MessagelistUserStore.selectedMessage(selectedMessage);
MessagelistUserStore.focusedMessage(selectedMessage);
}
}
}
}
}
}
preload || MessageUserStore.loading(false);
}, oMessage.folder, oMessage.uid);
}
};

View file

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

View file

@ -1,27 +1,15 @@
import ko from 'ko'; import ko from 'ko';
import { AbstractComponent } from 'Component/Abstract';
class AbstractCheckbox extends AbstractComponent { export class AbstractCheckbox {
/** /**
* @param {Object} params = {} * @param {Object} params = {}
*/ */
constructor(params = {}) { constructor(params = {}) {
super(); this.value = ko.isObservable(params.value) ? params.value
: ko.observable(!!params.value);
this.value = params.value; this.enable = ko.isObservable(params.enable) ? params.enable
if (undefined === this.value || !this.value.subscribe) { : ko.observable(undefined === params.enable || !!params.enable);
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.label = params.label || ''; this.label = params.label || '';
this.inline = !!params.inline; this.inline = !!params.inline;
@ -30,10 +18,6 @@ class AbstractCheckbox extends AbstractComponent {
} }
click() { click() {
if (this.enable() && !this.disable()) { this.enable() && this.value(!this.value());
this.value(!this.value());
} }
} }
}
export { AbstractCheckbox };

View file

@ -1,61 +1,57 @@
import ko from 'ko'; import ko from 'ko';
import { pInt } from 'Common/Utils'; import { pInt } from 'Common/Utils';
import { SaveSettingsStep } from 'Common/Enums'; import { SaveSettingsStep } from 'Common/Enums';
import { AbstractComponent } from 'Component/Abstract'; import { koComputable } from 'External/ko';
import { dispose } from 'External/ko';
class AbstractInput extends AbstractComponent { export class AbstractInput {
/** /**
* @param {Object} params * @param {Object} params
*/ */
constructor(params) { constructor(params) {
super();
this.value = params.value || ''; this.value = params.value || '';
this.size = params.size || 0;
this.label = params.label || ''; this.label = params.label || '';
this.preLabel = params.preLabel || ''; this.enable = null == params.enable ? true : params.enable;
this.enable = undefined === params.enable ? true : params.enable;
this.trigger = params.trigger && params.trigger.subscribe ? params.trigger : null; this.trigger = params.trigger && params.trigger.subscribe ? params.trigger : null;
this.placeholder = params.placeholder || ''; this.placeholder = params.placeholder || '';
this.labeled = undefined !== params.label; this.labeled = null != 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);
let size = 0 < params.size ? 'span' + params.size : '';
if (this.trigger) { if (this.trigger) {
this.setTriggerState(this.trigger()); const
classForTrigger = ko.observable(''),
this.disposable.push(this.trigger.subscribe(this.setTriggerState, this)); setTriggerState = value => {
}
}
setTriggerState(value) {
switch (pInt(value)) { switch (pInt(value)) {
case SaveSettingsStep.TrueResult: case SaveSettingsStep.TrueResult:
this.classForTrigger('success'); classForTrigger('success');
break; break;
case SaveSettingsStep.FalseResult: case SaveSettingsStep.FalseResult:
this.classForTrigger('error'); classForTrigger('error');
break; break;
default: default:
this.classForTrigger(''); classForTrigger('');
break; break;
} }
};
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 = [];
} }
} }
export { AbstractInput }; dispose() {
this.disposables.forEach(dispose);
}
}

View file

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

View file

@ -1,41 +1,3 @@
import ko from 'ko';
import { AbstractCheckbox } from 'Component/AbstractCheckbox'; import { AbstractCheckbox } from 'Component/AbstractCheckbox';
export class CheckboxMaterialDesignComponent extends AbstractCheckbox { export class CheckboxMaterialDesignComponent extends AbstractCheckbox {}
/**
* @param {Object} params
*/
constructor(params) {
super(params);
this.animationBox = ko.observable(false).extend({ falseTimeout: 200 });
this.animationCheckmark = ko.observable(false).extend({ falseTimeout: 200 });
this.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);
}
}
}

View file

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

View file

@ -1,11 +1,14 @@
import 'External/ko'; import 'External/ko';
import ko from 'ko'; import ko from 'ko';
import { HtmlEditor } from 'Common/Html'; import { HtmlEditor } from 'Common/Html';
import { timeToNode } from 'Common/Momentor'; import { timeToNode } from 'Common/Translator';
import { elementById } from 'Common/Globals'; import { elementById, addEventsListeners, dropdowns } from 'Common/Globals';
import { isArray } from 'Common/Utils'; import { isArray } from 'Common/Utils';
import { dropdownsDetectVisibility } from 'Common/UtilsUser';
import { EmailAddressesComponent } from 'Component/EmailAddresses'; import { EmailAddressesComponent } from 'Component/EmailAddresses';
import { ThemeStore } from 'Stores/Theme'; import { ThemeStore } from 'Stores/Theme';
import { moveMessagesToFolder } from 'Common/Folders';
import { setExpandedFolder } from 'Model/FolderCollection';
const rlContentType = 'snappymail/action', const rlContentType = 'snappymail/action',
@ -27,12 +30,16 @@ const rlContentType = 'snappymail/action',
id: 0, id: 0,
stop: () => clearTimeout(dragTimer.id), stop: () => clearTimeout(dragTimer.id),
start: fn => dragTimer.id = setTimeout(fn, 500) start: fn => dragTimer.id = setTimeout(fn, 500)
}; },
ttn = (element, fValueAccessor) => timeToNode(element, ko.unwrap(fValueAccessor()));
let dragImage, let dragImage,
dragData; dragData;
ko.bindingHandlers.editor = { Object.assign(ko.bindingHandlers, {
editor: {
init: (element, fValueAccessor) => { init: (element, fValueAccessor) => {
let editor = null; let editor = null;
@ -55,22 +62,20 @@ ko.bindingHandlers.editor = {
// }); // });
} }
} }
}; },
let ttn = (element, fValueAccessor) => timeToNode(element, ko.unwrap(fValueAccessor())); moment: {
ko.bindingHandlers.moment = {
init: ttn, init: ttn,
update: ttn update: ttn
}; },
ko.bindingHandlers.emailsTags = { emailsTags: {
init: (element, fValueAccessor, fAllBindingsAccessor) => { init: (element, fValueAccessor, fAllBindings) => {
const fValue = fValueAccessor(), const fValue = fValueAccessor();
fAllBindings = fAllBindingsAccessor();
element.addresses = new EmailAddressesComponent(element, { element.addresses = new EmailAddressesComponent(element, {
focusCallback: value => fValue.focused && fValue.focused(!!value), focusCallback: value => fValue.focused && fValue.focused(!!value),
autoCompleteSource: fAllBindings.autoCompleteSource || null, autoCompleteSource: fAllBindings.get('autoCompleteSource'),
onChange: value => fValue(value) onChange: value => fValue(value)
}); });
@ -83,10 +88,10 @@ ko.bindingHandlers.emailsTags = {
update: (element, fValueAccessor) => { update: (element, fValueAccessor) => {
element.addresses.value = ko.unwrap(fValueAccessor()); element.addresses.value = ko.unwrap(fValueAccessor());
} }
}; },
// Start dragging selected messages // Start dragging selected messages
ko.bindingHandlers.dragmessages = { dragmessages: {
init: (element, fValueAccessor) => { init: (element, fValueAccessor) => {
element.addEventListener("dragstart", e => { element.addEventListener("dragstart", e => {
let data = fValueAccessor()(e); let data = fValueAccessor()(e);
@ -114,10 +119,10 @@ ko.bindingHandlers.dragmessages = {
element.addEventListener("dragend", () => dragData = null); element.addEventListener("dragend", () => dragData = null);
element.setAttribute('draggable', true); element.setAttribute('draggable', true);
} }
}; },
// Drop selected messages on folder // Drop selected messages on folder
ko.bindingHandlers.dropmessages = { dropmessages: {
init: (element, fValueAccessor) => { init: (element, fValueAccessor) => {
const folder = fValueAccessor(), const folder = fValueAccessor(),
// folder = ko.dataFor(element), // folder = ko.dataFor(element),
@ -133,27 +138,29 @@ ko.bindingHandlers.dropmessages = {
if (folder && folder.collapsed()) { if (folder && folder.collapsed()) {
dragTimer.start(() => { dragTimer.start(() => {
folder.collapsed(false); folder.collapsed(false);
rl.app.setExpandedFolder(folder.fullNameHash, true); setExpandedFolder(folder.fullName, true);
}, 500); }, 500);
} }
} }
}; };
element.addEventListener("dragenter", fnHover); addEventsListeners(element, {
element.addEventListener("dragover", fnHover); dragenter: fnHover,
element.addEventListener("dragleave", fnStop); dragover: fnHover,
element.addEventListener("drop", e => { dragleave: fnStop,
drop: e => {
fnStop(e); fnStop(e);
if ('messages' === getDragAction(e) && ['move','copy'].includes(e.dataTransfer.effectAllowed)) { if ('messages' === getDragAction(e) && ['move','copy'].includes(e.dataTransfer.effectAllowed)) {
let data = dragData.data; let data = dragData.data;
if (folder && data && data.folder && isArray(data.uids)) { if (folder && data && data.folder && isArray(data.uids)) {
rl.app.moveMessagesToFolder(data.folder, data.uids, folder.fullNameRaw, data.copy && e.ctrlKey); moveMessagesToFolder(data.folder, data.uids, folder.fullName, data.copy && e.ctrlKey);
}
} }
} }
}); });
} }
}; },
ko.bindingHandlers.sortableItem = { sortableItem: {
init: (element, fValueAccessor) => { init: (element, fValueAccessor) => {
let options = ko.unwrap(fValueAccessor()) || {}, let options = ko.unwrap(fValueAccessor()) || {},
parent = element.parentNode, parent = element.parentNode,
@ -173,15 +180,16 @@ ko.bindingHandlers.sortableItem = {
} }
} }
}; };
element.addEventListener("dragstart", e => { addEventsListeners(element, {
dragstart: e => {
dragData = { dragData = {
action: 'sortable', action: 'sortable',
element: element element: element
}; };
setDragAction(e, 'sortable', 'move', element, element); setDragAction(e, 'sortable', 'move', element, element);
element.style.opacity = 0.25; element.style.opacity = 0.25;
}); },
element.addEventListener("dragend", e => { dragend: e => {
element.style.opacity = null; element.style.opacity = null;
if ('sortable' === getDragAction(e)) { if ('sortable' === getDragAction(e)) {
dragData.data.style.cssText = ''; dragData.data.style.cssText = '';
@ -191,12 +199,14 @@ ko.bindingHandlers.sortableItem = {
} }
dragData = null; dragData = null;
} }
}
}); });
if (!parent.sortable) { if (!parent.sortable) {
parent.sortable = true; parent.sortable = true;
parent.addEventListener("dragenter", fnHover); addEventsListeners(parent, {
parent.addEventListener("dragover", fnHover); dragenter: fnHover,
parent.addEventListener("drop", e => { dragover: fnHover,
drop: e => {
if ('sortable' === getDragAction(e)) { if ('sortable' === getDragAction(e)) {
e.preventDefault(); e.preventDefault();
let data = ko.dataFor(dragData.data), let data = ko.dataFor(dragData.data),
@ -210,50 +220,33 @@ ko.bindingHandlers.sortableItem = {
dragData = null; dragData = null;
options.afterMove && options.afterMove(); options.afterMove && options.afterMove();
} }
}
}); });
} }
} }
}; },
ko.bindingHandlers.initDom = { initDom: {
init: (element, fValueAccessor) => fValueAccessor()(element) init: (element, fValueAccessor) => fValueAccessor()(element)
}; },
ko.bindingHandlers.onEsc = { registerBootstrapDropdown: {
init: (element, fValueAccessor, fAllBindingsAccessor, viewModel) => {
let fn = event => {
if ('Escape' == event.key) {
element.dispatchEvent(new Event('change'));
fValueAccessor().call(viewModel);
}
};
element.addEventListener('keyup', fn);
ko.utils.domNodeDisposal.addDisposeCallback(element, () => element.removeEventListener('keyup', fn));
}
};
ko.bindingHandlers.registerBootstrapDropdown = {
init: element => { init: element => {
rl.Dropdowns.register(element); dropdowns.push(element);
element.ddBtn = new BSN.Dropdown(element.querySelector('[data-toggle="dropdown"]')); element.ddBtn = new BSN.Dropdown(element.querySelector('.dropdown-toggle'));
} }
}; },
ko.bindingHandlers.openDropdownTrigger = { openDropdownTrigger: {
update: (element, fValueAccessor) => { update: (element, fValueAccessor) => {
if (ko.unwrap(fValueAccessor())) { if (ko.unwrap(fValueAccessor())) {
const el = element.ddBtn; const el = element.ddBtn;
el.open || el.toggle(); el.open || el.toggle();
// el.focus(); // el.focus();
rl.Dropdowns.detectVisibility(); dropdownsDetectVisibility();
fValueAccessor()(false); 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 => { (doc => {
let visible = "visible", let idle = 'idle',
visible = 'visible',
status = visible, status = visible,
timer = 0, timer = 0,
wakeUp = () => { wakeUp = () => {
clearTimeout(timer); clearTimeout(timer);
if (status !== visible) {
status = visible; status = visible;
}
timer = setTimeout(() => { timer = setTimeout(() => {
if (status === visible) { if (status === visible) {
status = "idle"; status = idle;
dispatchEvent(new CustomEvent("idle")); dispatchEvent(new CustomEvent(idle));
} }
}, 10000); }, 10000);
}, },
init = () => { init = () => {
init = ()=>{}; init = () => 0;
// Safari // Safari
addEventListener('pagehide', () => status = "hidden"); addEventListener('pagehide', () => status = 'hidden');
// Else // Else
doc.addEventListener("visibilitychange", () => { doc.addEventListener('visibilitychange', () => {
status = doc.visibilityState; status = doc.visibilityState;
doc.hidden || wakeUp(); doc.hidden || wakeUp();
}); });
wakeUp(); wakeUp();
["mousemove","keyup","touchstart"].forEach(t => doc.addEventListener(t, wakeUp)); ['mousemove','keyup','touchstart'].forEach(t => doc.addEventListener(t, wakeUp));
["scroll","pageshow"].forEach(t => addEventListener(t, wakeUp)); ['scroll','pageshow'].forEach(t => addEventListener(t, wakeUp));
}; };
this.ifvisible = { this.ifvisible = {
idle: callback => { idle: callback => {
init(); init();
addEventListener("idle", callback); addEventListener(idle, callback);
}, },
now: () => { now: () => {
init(); init();

146
dev/External/ko.js vendored
View file

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

View file

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

View file

@ -1,12 +1,8 @@
import { isArray, isFunction, addObservablesTo, addComputablesTo } from 'Common/Utils'; import { isArray, forEachObjectValue, forEachObjectEntry } from 'Common/Utils';
import { dispose, addObservablesTo, addComputablesTo } from 'External/ko';
function dispose(disposable) {
if (disposable && isFunction(disposable.dispose)) {
disposable.dispose();
}
}
function typeCast(curValue, newValue) { function typeCast(curValue, newValue) {
if (null != curValue) {
switch (typeof curValue) switch (typeof curValue)
{ {
case 'boolean': return 0 != newValue && !!newValue; case 'boolean': return 0 != newValue && !!newValue;
@ -19,6 +15,7 @@ function typeCast(curValue, newValue) {
if (isArray(curValue) && !isArray(newValue)) if (isArray(curValue) && !isArray(newValue))
return []; return [];
} }
}
return newValue; return newValue;
} }
@ -29,7 +26,7 @@ export class AbstractModel {
throw new Error("Can't instantiate AbstractModel!"); throw new Error("Can't instantiate AbstractModel!");
} }
*/ */
this.subscribables = []; this.disposables = [];
} }
addObservables(observables) { addObservables(observables) {
@ -41,25 +38,26 @@ export class AbstractModel {
} }
addSubscribables(subscribables) { addSubscribables(subscribables) {
Object.entries(subscribables).forEach(([key, fn]) => this.subscribables.push( this[key].subscribe(fn) ) ); // addSubscribablesTo(this, subscribables);
forEachObjectEntry(subscribables, (key, fn) => this.disposables.push( this[key].subscribe(fn) ) );
} }
/** Called by delegateRunOnDestroy */ /** Called by delegateRunOnDestroy */
onDestroy() { onDestroy() {
/** dispose ko subscribables */ /** dispose ko subscribables */
this.subscribables.forEach(dispose); this.disposables.forEach(dispose);
/** clear object entries */ /** clear object entries */
// Object.entries(this).forEach(([key, value]) => { // forEachObjectEntry(this, (key, value) => {
Object.values(this).forEach(value => { forEachObjectValue(this, value => {
/** clear CollectionModel */ /** clear CollectionModel */
let arr = ko.isObservableArray(value) ? value() : value; let arr = ko.isObservableArray(value) ? value() : value;
arr && arr.onDestroy && value.onDestroy(); arr && arr.onDestroy && arr.onDestroy();
/** destroy ko.observable/ko.computed? */ /** destroy ko.observable/ko.computed? */
dispose(value); // dispose(value);
/** clear object value */ /** clear object value */
// this[key] = null; // TODO: issue with Contacts view // this[key] = null; // TODO: issue with Contacts view
}); });
// this.subscribables = []; // this.disposables = [];
} }
/** /**
@ -84,13 +82,12 @@ export class AbstractModel {
revivePropertiesFromJson(json) { revivePropertiesFromJson(json) {
let model = this.constructor; let model = this.constructor;
try {
if (!model.validJson(json)) { if (!model.validJson(json)) {
return false; return false;
} }
Object.entries(json).forEach(([key, value]) => { forEachObjectEntry(json, (key, value) => {
if ('@' !== key[0]) { if ('@' !== key[0]) try {
key = key[0].toLowerCase() + key.substr(1); key = key[0].toLowerCase() + key.slice(1);
switch (typeof this[key]) switch (typeof this[key])
{ {
case 'function': case 'function':
@ -111,12 +108,11 @@ export class AbstractModel {
default: default:
// console.log((typeof this[key])+' '+(model.name)+'.'+key+' not revived'); // console.log((typeof this[key])+' '+(model.name)+'.'+key+' not revived');
} }
}
});
} catch (e) { } catch (e) {
console.log(model.name); console.log(model.name + '.' + key);
console.error(e); console.error(e);
} }
});
return true; return true;
} }

View file

@ -1,24 +1,10 @@
import { isArray, isNonEmptyArray } from 'Common/Utils'; import { isArray, arrayLength } from 'Common/Utils';
export class AbstractScreen { export class AbstractScreen {
constructor(screenName, viewModels = []) { constructor(screenName, viewModels = []) {
this.oCross = null; this.__cross = null;
this.sScreenName = screenName; this.screenName = screenName;
this.aViewModels = isArray(viewModels) ? viewModels : []; this.viewModels = isArray(viewModels) ? viewModels : [];
}
/**
* @returns {Array}
*/
get viewModels() {
return this.aViewModels;
}
/**
* @returns {string}
*/
screenName() {
return this.sScreenName;
} }
/** /**
@ -28,28 +14,26 @@ export class AbstractScreen {
return null; return null;
} }
/** /*
* @returns {?Object} onBuild(viewModelDom) {}
onShow() {}
onHide() {}
__started
__builded
*/ */
get __cross() {
return this.oCross;
}
/** /**
* @returns {void} * @returns {void}
*/ */
onStart() { onStart() {
if (!this.__started) {
this.__started = true;
const routes = this.routes(); const routes = this.routes();
if (isNonEmptyArray(routes)) { if (arrayLength(routes)) {
let route = new Crossroads(), let route = new Crossroads(),
fMatcher = (this.onRoute || (()=>{})).bind(this); 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 ko from 'ko';
import { inFocus, addObservablesTo, addComputablesTo, addSubscribablesTo } from 'Common/Utils'; import { addObservablesTo, addComputablesTo, addSubscribablesTo } from 'External/ko';
import { Scope } from 'Common/Enums'; import { keyScope, SettingsGet, leftPanelDisabled } from 'Common/Globals';
import { keyScope } from 'Common/Globals'; import { ViewTypePopup, showScreenPopup } from 'Knoin/Knoin';
import { ViewType } from 'Knoin/Knoin';
import { SaveSettingsStep } from 'Common/Enums';
class AbstractView { class AbstractView {
constructor(name, templateID, type) constructor(templateID, type)
{ {
this.viewModelName = 'View/' + name; // Object.defineProperty(this, 'viewModelTemplateID', { value: templateID });
this.viewModelTemplateID = templateID; this.viewModelTemplateID = templateID || this.constructor.name.replace('UserView', '');
this.viewModelPosition = type; this.viewType = type;
this.bDisabeCloseOnEsc = false;
this.sDefaultScope = Scope.None;
this.sCurrentScope = Scope.None;
this.viewModelVisible = false;
this.modalVisibility = ko.observable(false).extend({ rateLimit: 0 });
this.viewModelName = '';
this.viewModelDom = null; this.viewModelDom = null;
this.keyScope = {
scope: 'none',
previous: 'none',
set: function() {
this.previous = keyScope();
keyScope(this.scope);
},
unset: function() {
keyScope(this.previous);
}
};
} }
/** /*
* @returns {void} onBuild() {}
beforeShow() {} // Happens before: hidden = false
onShow() {} // Happens after: hidden = false
onHide() {}
*/ */
storeAndSetScope() {
this.sCurrentScope = keyScope();
keyScope(this.sDefaultScope);
}
/**
* @returns {void}
*/
restoreScope() {
keyScope(this.sCurrentScope);
}
cancelCommand() {}
closeCommand() {}
querySelector(selectors) { querySelector(selectors) {
return this.viewModelDom.querySelector(selectors); return this.viewModelDom.querySelector(selectors);
@ -63,51 +56,115 @@ export class AbstractViewPopup extends AbstractView
{ {
constructor(name) constructor(name)
{ {
super('Popup/' + name, 'Popups' + name, ViewType.Popup); super('Popups' + name, ViewTypePopup);
if (name in Scope) { this.keyScope.scope = name;
this.sDefaultScope = Scope[name]; this.modalVisible = ko.observable(false).extend({ rateLimit: 0 });
} shortcuts.add('escape,close', '', name, () => {
} if (this.modalVisible() && false !== this.onClose()) {
this.close();
/**
* @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; return false;
} }
}
return true; return true;
}); });
} }
// Happens when user hits Escape or Close key
// return false to prevent closing
onClose() {}
/*
beforeShow() {} // Happens before showModal()
onShow() {} // Happens after showModal()
afterShow() {} // Happens after showModal() animation transitionend
onHide() {} // Happens before animation transitionend
afterHide() {} // Happens after animation transitionend
close() {}
*/
} }
export class AbstractViewCenter extends AbstractView AbstractViewPopup.showModal = function(params = []) {
{ showScreenPopup(this, params);
constructor(name, templateID)
{
super(name, templateID, ViewType.Center);
} }
AbstractViewPopup.hidden = function() {
return !this.__vm || !this.__vm.modalVisible();
} }
export class AbstractViewLeft extends AbstractView export class AbstractViewLeft extends AbstractView
{ {
constructor(name, templateID) constructor(templateID)
{ {
super(name, templateID, ViewType.Left); super(templateID, 'Left');
this.leftPanelDisabled = leftPanelDisabled;
} }
} }
export class AbstractViewRight extends AbstractView export class AbstractViewRight extends AbstractView
{ {
constructor(name, templateID) constructor(templateID)
{ {
super(name, templateID, ViewType.Right); super(templateID, 'Right');
}
}
export class AbstractViewSettings
{
/*
onBuild(viewModelDom) {}
beforeShow() {}
onShow() {}
onHide() {}
viewModelDom
*/
addSetting(name, valueCb)
{
let prop = name[0].toLowerCase() + name.slice(1),
trigger = prop + 'Trigger';
addObservablesTo(this, {
[prop]: SettingsGet(name),
[trigger]: SaveSettingsStep.Idle,
});
addSubscribablesTo(this, {
[prop]: (value => {
this[trigger](SaveSettingsStep.Animate);
valueCb && valueCb(value);
rl.app.Remote.saveSetting(name, value,
iError => {
this[trigger](iError ? SaveSettingsStep.FalseResult : SaveSettingsStep.TrueResult);
setTimeout(() => this[trigger](SaveSettingsStep.Idle), 1000);
}
);
}).debounce(999),
});
}
addSettings(names)
{
names.forEach(name => {
let prop = name[0].toLowerCase() + name.slice(1);
this[prop] || (this[prop] = ko.observable(SettingsGet(name)));
this[prop].subscribe(value => rl.app.Remote.saveSetting(name, value));
});
}
}
export class AbstractViewLogin extends AbstractView {
constructor(templateID) {
super(templateID, 'Content');
this.hideSubmitButton = SettingsGet('hideSubmitButton');
this.formError = ko.observable(false).extend({ falseTimeout: 500 });
}
onBuild(dom) {
dom.classList.add('LoginView');
}
onShow() {
rl.route.off();
}
submitForm() {
// return false;
} }
} }

View file

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

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

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

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,4 +1,4 @@
import { isNonEmptyArray } from 'Common/Utils'; import { arrayLength } from 'Common/Utils';
import { ContactPropertyModel, ContactPropertyType } from 'Model/ContactProperty'; import { ContactPropertyModel, ContactPropertyType } from 'Model/ContactProperty';
import { AbstractModel } from 'Knoin/AbstractModel'; import { AbstractModel } from 'Knoin/AbstractModel';
@ -26,7 +26,7 @@ export class ContactModel extends AbstractModel {
let name = '', let name = '',
email = ''; email = '';
if (isNonEmptyArray(this.properties)) { if (arrayLength(this.properties)) {
this.properties.forEach(property => { this.properties.forEach(property => {
if (property) { if (property) {
if (ContactPropertyType.FirstName === property.type()) { if (ContactPropertyType.FirstName === property.type()) {
@ -52,7 +52,7 @@ export class ContactModel extends AbstractModel {
const contact = super.reviveFromJson(json); const contact = super.reviveFromJson(json);
if (contact) { if (contact) {
let list = []; let list = [];
if (isNonEmptyArray(json.properties)) { if (arrayLength(json.properties)) {
json.properties.forEach(property => { json.properties.forEach(property => {
property = ContactPropertyModel.reviveFromJson(property); property = ContactPropertyModel.reviveFromJson(property);
property && list.push(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=} email = ''
* @param {string=} name = '' * @param {string=} name = ''
@ -430,5 +430,3 @@ class EmailModel extends AbstractModel {
return false; return false;
} }
} }
export { EmailModel, EmailModel as default };

View file

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

View file

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

View file

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

View file

@ -39,7 +39,6 @@ export class MessageCollectionModel extends AbstractCollectionModel
if (message) { if (message) {
if (hasNewMessageAndRemoveFromCache(message.folder, message.uid) && 5 >= newCount) { if (hasNewMessageAndRemoveFromCache(message.folder, message.uid) && 5 >= newCount) {
++newCount; ++newCount;
message.newForAnimation(true);
} }
message.deleted(false); 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,27 +1,19 @@
import { Notification } from 'Common/Enums'; import { Notification } from 'Common/Enums';
import { Settings } from 'Common/Globals';
import { isArray, pInt, pString } from 'Common/Utils'; import { isArray, pInt, pString } from 'Common/Utils';
import { serverRequest } from 'Common/Links'; import { serverRequest } from 'Common/Links';
import { runHook } from 'Common/Plugins'; import { runHook } from 'Common/Plugins';
import { getNotification } from 'Common/Translator';
let iJsonErrorCount = 0, let iJsonErrorCount = 0;
iTokenErrorCount = 0;
const getURL = (add = '') => serverRequest('Json') + add, const getURL = (add = '') => serverRequest('Json') + add,
updateToken = data => {
if (data.UpdateToken) {
rl.hash.set();
Settings.set('AuthAccountHash', data.UpdateToken);
}
},
checkResponseError = data => { checkResponseError = data => {
const err = data ? data.ErrorCode : null; const err = data ? data.ErrorCode : null;
if (Notification.InvalidToken === err && 10 < ++iTokenErrorCount) { if (Notification.InvalidToken === err) {
alert(getNotification(err));
rl.logoutReload(); rl.logoutReload();
} else { } else if ([
if ([
Notification.AuthError, Notification.AuthError,
Notification.ConnectionError, Notification.ConnectionError,
Notification.DomainNotAllowed, Notification.DomainNotAllowed,
@ -31,11 +23,8 @@ checkResponseError = data => {
Notification.UnknownError Notification.UnknownError
].includes(err) ].includes(err)
) { ) {
++iJsonErrorCount; if (7 < ++iJsonErrorCount) {
} rl.logoutReload();
if (data.ClearAuth || data.Logout || 7 < iJsonErrorCount) {
rl.hash.clear();
data.ClearAuth || rl.logoutReload();
} }
} }
}, },
@ -57,7 +46,11 @@ abort = (sAction, bClearOnly) => {
fetchJSON = (action, sGetAdd, params, timeout, jsonCallback) => { fetchJSON = (action, sGetAdd, params, timeout, jsonCallback) => {
sGetAdd = pString(sGetAdd); sGetAdd = pString(sGetAdd);
params = params || {}; params = params || {};
if (params instanceof FormData) {
params.set('Action', action);
} else {
params.Action = action; params.Action = action;
}
let init = {}; let init = {};
if (window.AbortController) { if (window.AbortController) {
abort(action); abort(action);
@ -69,6 +62,14 @@ fetchJSON = (action, sGetAdd, params, timeout, jsonCallback) => {
return rl.fetchJSON(getURL(sGetAdd), init, sGetAdd ? null : params).then(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 export class AbstractFetchRemote
{ {
abort(sAction, bClearOnly) { abort(sAction, bClearOnly) {
@ -76,6 +77,45 @@ export class AbstractFetchRemote
return this; return this;
} }
/**
* Allows quicker visual responses to the user.
* Can be used to stream lines of json encoded data, but does not work on all servers.
* Apache needs 'flushpackets' like in <Proxy "fcgi://...." flushpackets=on></Proxy>
*/
streamPerLine(fCallback, sGetAdd) {
rl.fetch(getURL(sGetAdd))
.then(response => response.body)
.then(body => {
// Firefox TextDecoderStream is not defined
// const reader = body.pipeThrough(new TextDecoderStream()).getReader();
const reader = body.getReader(),
re = /\r\n|\n|\r/gm,
utf8decoder = new TextDecoder();
let buffer = '';
function processText({ done, value }) {
buffer += value ? utf8decoder.decode(value, {stream: true}) : '';
for (;;) {
let result = re.exec(buffer);
if (!result) {
if (done) {
break;
}
reader.read().then(processText);
return;
}
fCallback(buffer.slice(0, result.index));
buffer = buffer.slice(result.index + 1);
re.lastIndex = 0;
}
if (buffer.length) {
// last line didn't end in a newline char
fCallback(buffer);
}
}
reader.read().then(processText);
})
}
/** /**
* @param {?Function} fCallback * @param {?Function} fCallback
* @param {string} sAction * @param {string} sAction
@ -84,7 +124,7 @@ export class AbstractFetchRemote
* @param {string=} sGetAdd = '' * @param {string=} sGetAdd = ''
* @param {Array=} aAbortActions = [] * @param {Array=} aAbortActions = []
*/ */
defaultRequest(fCallback, sAction, params, iTimeout, sGetAdd, abortActions) { request(sAction, fCallback, params, iTimeout, sGetAdd, abortActions) {
params = params || {}; params = params || {};
const start = Date.now(); const start = Date.now();
@ -100,14 +140,10 @@ export class AbstractFetchRemote
undefined === iTimeout ? 30000 : pInt(iTimeout), undefined === iTimeout ? 30000 : pInt(iTimeout),
data => { data => {
let cached = false; let cached = false;
if (data) { if (data && data.Time) {
if (data.Time) {
cached = pInt(data.Time) > Date.now() - start; cached = pInt(data.Time) > Date.now() - start;
} }
updateToken(data);
}
let iError = 0; let iError = 0;
if (sAction && oRequests[sAction]) { if (sAction && oRequests[sAction]) {
if (oRequests[sAction].__aborted) { if (oRequests[sAction].__aborted) {
@ -123,7 +159,7 @@ export class AbstractFetchRemote
} }
*/ */
if (data.Result) { if (data.Result) {
iJsonErrorCount = iTokenErrorCount = 0; iJsonErrorCount = 0;
} else { } else {
checkResponseError(data); checkResponseError(data);
iError = data.ErrorCode || Notification.UnknownError iError = data.ErrorCode || Notification.UnknownError
@ -153,32 +189,11 @@ export class AbstractFetchRemote
}); });
} }
/**
* @param {?Function} fCallback
*/
noop(fCallback) {
this.defaultRequest(fCallback, 'Noop');
}
/** /**
* @param {?Function} fCallback * @param {?Function} fCallback
*/ */
getPublicKey(fCallback) { getPublicKey(fCallback) {
this.defaultRequest(fCallback, 'GetPublicKey'); this.request('GetPublicKey', fCallback);
}
/**
* @param {?Function} fCallback
* @param {string} sVersion
*/
jsVersion(fCallback, sVersion) {
this.defaultRequest(fCallback, 'Version', {
Version: sVersion
});
}
fastResolve(mData) {
return Promise.resolve(mData);
} }
setTrigger(trigger, value) { setTrigger(trigger, value) {
@ -190,17 +205,15 @@ export class AbstractFetchRemote
} }
} }
postRequest(action, fTrigger, params, timeOut) { post(action, fTrigger, params, timeOut) {
this.setTrigger(fTrigger, true); this.setTrigger(fTrigger, true);
return fetchJSON(action, '', params, pInt(timeOut, 30000), return fetchJSON(action, '', params, pInt(timeOut, 30000),
data => { data => {
abort(action, true); abort(action, true);
if (!data) { if (!data) {
return Promise.reject(Notification.JsonParse); return Promise.reject(new FetchError(Notification.JsonParse));
} }
updateToken(data);
/* /*
let isCached = false, type = ''; let isCached = false, type = '';
if (data && data.Time) { if (data && data.Time) {
@ -223,8 +236,10 @@ export class AbstractFetchRemote
if (!data.Result || action !== data.Action) { if (!data.Result || action !== data.Action) {
checkResponseError(data); checkResponseError(data);
const err = data ? data.ErrorCode : 0; return Promise.reject(new FetchError(
return Promise.reject(err || Notification.JsonFalse); data ? data.ErrorCode : 0,
data ? (data.ErrorMessageAdditional || data.ErrorMessage) : ''
));
} }
return data; return data;

View file

@ -1,212 +1,14 @@
import { AbstractFetchRemote } from 'Remote/AbstractFetch'; import { AbstractFetchRemote } from 'Remote/AbstractFetch';
class RemoteAdminFetch extends AbstractFetchRemote { class RemoteAdminFetch extends AbstractFetchRemote {
/**
* @param {?Function} fCallback
* @param {string} sLogin
* @param {string} sPassword
*/
adminLogin(fCallback, sLogin, sPassword) {
this.defaultRequest(fCallback, 'AdminLogin', {
Login: sLogin,
Password: sPassword
});
}
/** /**
* @param {string} key
* @param {?scalar} value
* @param {?Function} fCallback * @param {?Function} fCallback
*/ */
adminLogout(fCallback) { saveSetting(key, value, fCallback) {
this.defaultRequest(fCallback, 'AdminLogout'); this.request('AdminSettingsUpdate', fCallback, {[key]: value});
}
/**
* @param {?Function} fCallback
* @param {?} oData
*/
saveAdminConfig(fCallback, oData) {
this.defaultRequest(fCallback, 'AdminSettingsUpdate', oData);
}
/**
* @param {?Function} fCallback
* @param {boolean=} bIncludeAliases = true
*/
domainList(fCallback, bIncludeAliases = true) {
this.defaultRequest(fCallback, 'AdminDomainList', {
IncludeAliases: bIncludeAliases ? 1 : 0
});
}
/**
* @param {?Function} fCallback
*/
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);
} }
} }

View file

@ -1,11 +1,9 @@
import { isArray, isNonEmptyArray, pString, pInt } from 'Common/Utils'; import { pString, pInt, b64EncodeJSONSafe } from 'Common/Utils';
import { import {
getFolderHash, getFolderHash,
getFolderInboxName,
getFolderUidNext, getFolderUidNext,
getFolderFromCacheList, getFolderFromCacheList
MessageFlagsCache
} from 'Common/Cache'; } from 'Common/Cache';
import { SettingsGet } from 'Common/Globals'; import { SettingsGet } from 'Common/Globals';
@ -17,224 +15,7 @@ import { FolderUserStore } from 'Stores/User/Folder';
import { AbstractFetchRemote } from 'Remote/AbstractFetch'; import { AbstractFetchRemote } from 'Remote/AbstractFetch';
import { FolderCollectionModel } from 'Model/FolderCollection';
//const toUTF8 = window.TextEncoder
// ? text => String.fromCharCode(...new TextEncoder().encode(text))
// : text => unescape(encodeURIComponent(text)),
const urlsafeArray = array => btoa(unescape(encodeURIComponent(array.join('\x00').replace(/\r\n/g, '\n'))))
.replace('+', '-')
.replace('/', '_')
.replace('=', '');
class RemoteUserFetch extends AbstractFetchRemote { class RemoteUserFetch extends AbstractFetchRemote {
/**
* @param {?Function} fCallback
*/
folders(fCallback) {
this.defaultRequest(
fCallback,
'Folders',
{
SentFolder: SettingsGet('SentFolder'),
DraftFolder: SettingsGet('DraftFolder'),
SpamFolder: SettingsGet('SpamFolder'),
TrashFolder: SettingsGet('TrashFolder'),
ArchiveFolder: SettingsGet('ArchiveFolder')
},
null,
'',
['Folders']
);
}
/**
* @param {?Function} fCallback
* @param {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 * @param {Function} fCallback
@ -243,23 +24,23 @@ class RemoteUserFetch extends AbstractFetchRemote {
*/ */
messageList(fCallback, params, bSilent = false) { messageList(fCallback, params, bSilent = false) {
const const
sFolderFullNameRaw = pString(params.Folder), sFolderFullName = pString(params.Folder),
folderHash = getFolderHash(sFolderFullNameRaw), folderHash = getFolderHash(sFolderFullName);
useThreads = AppUserStore.threadsAllowed() && SettingsUserStore.useThreads() ? 1 : 0,
inboxUidNext = getFolderInboxName() === sFolderFullNameRaw ? getFolderUidNext(sFolderFullNameRaw) : '';
params.Folder = sFolderFullNameRaw;
params.ThreadUid = useThreads ? params.ThreadUid : '';
params = Object.assign({ params = Object.assign({
Folder: '',
Offset: 0, Offset: 0,
Limit: SettingsUserStore.messagesPerPage(), Limit: SettingsUserStore.messagesPerPage(),
Search: '', Search: '',
UidNext: inboxUidNext, UidNext: getFolderUidNext(sFolderFullName), // Used to check for new messages
UseThreads: useThreads, Sort: FolderUserStore.sortMode(),
ThreadUid: '', Hash: folderHash + SettingsGet('AccountHash')
Sort: FolderUserStore.sortMode()
}, params); }, params);
params.Folder = sFolderFullName;
if (AppUserStore.threadsAllowed() && SettingsUserStore.useThreads()) {
params.UseThreads = 1;
} else {
params.ThreadUid = 0;
}
let sGetAdd = ''; let sGetAdd = '';
@ -267,13 +48,12 @@ class RemoteUserFetch extends AbstractFetchRemote {
sGetAdd = 'MessageList/' + sGetAdd = 'MessageList/' +
SUB_QUERY_PREFIX + SUB_QUERY_PREFIX +
'/' + '/' +
urlsafeArray([SettingsGet('ProjectHash'),folderHash].concat(Object.values(params))); b64EncodeJSONSafe(params);
params = {}; params = {};
} }
this.defaultRequest( this.request('MessageList',
fCallback, fCallback,
'MessageList',
params, params,
30000, 30000,
sGetAdd, sGetAdd,
@ -283,43 +63,27 @@ class RemoteUserFetch extends AbstractFetchRemote {
/** /**
* @param {?Function} fCallback * @param {?Function} fCallback
* @param {Array} aDownloads * @param {string} sFolderFullName
*/
messageUploadAttachments(fCallback, aDownloads) {
this.defaultRequest(
fCallback,
'MessageUploadAttachments',
{
Attachments: aDownloads
},
999000
);
}
/**
* @param {?Function} fCallback
* @param {string} sFolderFullNameRaw
* @param {number} iUid * @param {number} iUid
* @returns {boolean} * @returns {boolean}
*/ */
message(fCallback, sFolderFullNameRaw, iUid) { message(fCallback, sFolderFullName, iUid) {
sFolderFullNameRaw = pString(sFolderFullNameRaw); sFolderFullName = pString(sFolderFullName);
iUid = pInt(iUid); iUid = pInt(iUid);
if (getFolderFromCacheList(sFolderFullNameRaw) && 0 < iUid) { if (getFolderFromCacheList(sFolderFullName) && 0 < iUid) {
this.defaultRequest( this.request('Message',
fCallback, fCallback,
'Message',
{}, {},
null, null,
'Message/' + 'Message/' +
SUB_QUERY_PREFIX + SUB_QUERY_PREFIX +
'/' + '/' +
urlsafeArray([ b64EncodeJSONSafe([
sFolderFullNameRaw, sFolderFullName,
iUid, iUid,
SettingsGet('ProjectHash'), AppUserStore.threadsAllowed() && SettingsUserStore.useThreads() ? 1 : 0,
AppUserStore.threadsAllowed() && SettingsUserStore.useThreads() ? 1 : 0 SettingsGet('AccountHash')
]), ]),
['Message'] ['Message']
); );
@ -330,186 +94,12 @@ class RemoteUserFetch extends AbstractFetchRemote {
return false; return false;
} }
/**
* @param {?Function} fCallback
* @param {Array} aExternals
*/
composeUploadExternals(fCallback, aExternals) {
this.defaultRequest(
fCallback,
'ComposeUploadExternals',
{
Externals: aExternals
},
999000
);
}
/**
* @param {?Function} fCallback
* @param {string} sUrl
* @param {string} sAccessToken
*/
composeUploadDrive(fCallback, sUrl, sAccessToken) {
this.defaultRequest(
fCallback,
'ComposeUploadDrive',
{
AccessToken: sAccessToken,
Url: sUrl
},
999000
);
}
/**
* @param {?Function} fCallback
* @param {string} folder
* @param {Array=} list = []
*/
folderInformation(fCallback, folder, list = []) {
let request = true;
const uids = [];
if (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 {?Function} fCallback
* @param {Object} oData * @param {Object} oData
*/ */
saveSettings(fCallback, 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 folderMove(sPrevFolderFullName, sNewFolderFullName, bSubscribe) {
* @param {string} sFolderFullNameRaw return this.post('FolderMove', FolderUserStore.foldersRenaming, {
*/ Folder: sPrevFolderFullName,
folderClear(fCallback, sFolderFullNameRaw) { NewFolder: sNewFolderFullName,
this.defaultRequest(fCallback, 'FolderClear', {
Folder: sFolderFullNameRaw
});
}
/**
* @param {?Function} fCallback
* @param {string} sFolderFullNameRaw
* @param {boolean} bSubscribe
*/
folderSetSubscribe(fCallback, sFolderFullNameRaw, bSubscribe) {
this.defaultRequest(fCallback, 'FolderSubscribe', {
Folder: sFolderFullNameRaw,
Subscribe: bSubscribe ? 1 : 0 Subscribe: bSubscribe ? 1 : 0
}); });
} }
/**
* @param {?Function} fCallback
* @param {string} sFolderFullNameRaw
* @param {boolean} bCheckable
*/ */
folderSetCheckable(fCallback, sFolderFullNameRaw, bCheckable) {
this.defaultRequest(fCallback, 'FolderCheckable', {
Folder: sFolderFullNameRaw,
Checkable: bCheckable ? 1 : 0
});
}
/**
* @param {?Function} fCallback
* @param {string} sFolder
* @param {string} sToFolder
* @param {Array} aUids
* @param {string=} sLearning
* @param {boolean=} bMarkAsRead
*/
messagesMove(fCallback, sFolder, sToFolder, aUids, sLearning, bMarkAsRead) {
this.defaultRequest(
fCallback,
'MessageMove',
{
FromFolder: sFolder,
ToFolder: sToFolder,
Uids: aUids.join(','),
MarkAsRead: bMarkAsRead ? 1 : 0,
Learning: sLearning || ''
},
null,
'',
['MessageList']
);
}
/**
* @param {?Function} fCallback
* @param {string} sFolder
* @param {string} sToFolder
* @param {Array} aUids
*/
messagesCopy(fCallback, sFolder, sToFolder, aUids) {
this.defaultRequest(fCallback, 'MessageCopy', {
FromFolder: sFolder,
ToFolder: sToFolder,
Uids: aUids.join(',')
});
}
/**
* @param {?Function} fCallback
* @param {string} sFolder
* @param {Array} aUids
*/
messagesDelete(fCallback, sFolder, aUids) {
this.defaultRequest(
fCallback,
'MessageDelete',
{
Folder: sFolder,
Uids: aUids.join(',')
},
null,
'',
['MessageList']
);
}
/**
* @param {?Function} fCallback
*/
appDelayStart(fCallback) {
this.defaultRequest(fCallback, 'AppDelayStart');
}
/**
* @param {?Function} fCallback
*/
quota(fCallback) {
this.defaultRequest(fCallback, 'Quota');
}
/**
* @param {?Function} fCallback
* @param {number} iOffset
* @param {number} iLimit
* @param {string} sSearch
*/
contacts(fCallback, iOffset, iLimit, sSearch) {
this.defaultRequest(
fCallback,
'Contacts',
{
Offset: iOffset,
Limit: iLimit,
Search: sSearch
},
null,
'',
['Contacts']
);
}
/**
* @param {?Function} fCallback
* @param {string} sRequestUid
* @param {string} sUid
* @param {Array} aProperties
*/
contactSave(fCallback, sRequestUid, sUid, aProperties) {
this.defaultRequest(fCallback, 'ContactSave', {
RequestUid: sRequestUid,
Uid: sUid,
Properties: aProperties
});
}
/**
* @param {?Function} fCallback
* @param {Array} aUids
*/
contactsDelete(fCallback, aUids) {
this.defaultRequest(fCallback, 'ContactsDelete', {
Uids: aUids.join(',')
});
}
/**
* @param {?Function} fCallback
* @param {string} sQuery
* @param {number} iPage
*/
suggestions(fCallback, sQuery, iPage) {
this.defaultRequest(
fCallback,
'Suggestions',
{
Query: sQuery,
Page: iPage
},
null,
'',
['Suggestions']
);
}
/**
* @param {?Function} fCallback
*/
clearUserBackground(fCallback) {
this.defaultRequest(fCallback, 'ClearUserBackground');
}
foldersReload(fCallback) {
this.abort('Folders')
.postRequest('Folders', FolderUserStore.foldersLoading)
.then(data => {
data = FolderCollectionModel.reviveFromJson(data.Result);
data && data.storeIt();
fCallback && fCallback(true);
})
.catch(() => fCallback && setTimeout(() => fCallback(false), 1));
}
foldersReloadWithTimeout() {
this.setTrigger(FolderUserStore.foldersLoading, true);
clearTimeout(this.foldersTimeout);
this.foldersTimeout = setTimeout(() => this.foldersReload(), 500);
}
folderDelete(sFolderFullNameRaw) {
return this.postRequest('FolderDelete', FolderUserStore.foldersDeleting, {
Folder: sFolderFullNameRaw
});
}
folderCreate(sNewFolderName, sParentName) {
return this.postRequest('FolderCreate', FolderUserStore.foldersCreating, {
Folder: sNewFolderName,
Parent: sParentName
});
}
folderMove(sPrevFolderFullNameRaw, sNewFolderFullName) {
return this.postRequest('FolderMove', FolderUserStore.foldersRenaming, {
Folder: sPrevFolderFullNameRaw,
NewFolder: sNewFolderFullName
});
}
folderRename(sPrevFolderFullNameRaw, sNewFolderName) {
return this.postRequest('FolderRename', FolderUserStore.foldersRenaming, {
Folder: sPrevFolderFullNameRaw,
NewFolderName: sNewFolderName
});
}
attachmentsActions(sAction, aHashes, fTrigger) {
return this.postRequest('AttachmentsActions', fTrigger, {
Do: sAction,
Hashes: aHashes
});
}
} }
export default new RemoteUserFetch(); export default new RemoteUserFetch();

View file

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

View file

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

View file

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

View file

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

View file

@ -1,5 +1,5 @@
import { Capa, Scope } from 'Common/Enums'; import { Scope } from 'Common/Enums';
import { keyScope, leftPanelDisabled, Settings } from 'Common/Globals'; import { keyScope, leftPanelDisabled, SettingsCapa } from 'Common/Globals';
import { runSettingsViewModelHooks } from 'Common/Plugins'; import { runSettingsViewModelHooks } from 'Common/Plugins';
import { initOnStartOrLangChange, i18n } from 'Common/Translator'; import { initOnStartOrLangChange, i18n } from 'Common/Translator';
@ -9,23 +9,53 @@ import { ThemeStore } from 'Stores/Theme';
import { AbstractSettingsScreen, settingsAddViewModel } from 'Screen/AbstractSettings'; import { AbstractSettingsScreen, settingsAddViewModel } from 'Screen/AbstractSettings';
import { GeneralUserSettings } from 'Settings/User/General'; import { UserSettingsGeneral } from 'Settings/User/General';
import { ContactsUserSettings } from 'Settings/User/Contacts'; import { UserSettingsContacts } from 'Settings/User/Contacts';
import { AccountsUserSettings } from 'Settings/User/Accounts'; import { UserSettingsAccounts } from 'Settings/User/Accounts';
import { FiltersUserSettings } from 'Settings/User/Filters'; import { UserSettingsFilters } from 'Settings/User/Filters';
import { SecurityUserSettings } from 'Settings/User/Security'; import { UserSettingsSecurity } from 'Settings/User/Security';
import { TemplatesUserSettings } from 'Settings/User/Templates'; import { UserSettingsFolders } from 'Settings/User/Folders';
import { FoldersUserSettings } from 'Settings/User/Folders'; import { UserSettingsThemes } from 'Settings/User/Themes';
import { ThemesUserSettings } from 'Settings/User/Themes';
import { OpenPgpUserSettings } from 'Settings/User/OpenPgp';
import { SystemDropDownSettingsUserView } from 'View/User/Settings/SystemDropDown'; import { SystemDropDownUserView } from 'View/User/SystemDropDown';
import { MenuSettingsUserView } from 'View/User/Settings/Menu'; import { SettingsMenuUserView } from 'View/User/Settings/Menu';
import { PaneSettingsUserView } from 'View/User/Settings/Pane'; import { SettingsPaneUserView } from 'View/User/Settings/Pane';
export class SettingsUserScreen extends AbstractSettingsScreen { export class SettingsUserScreen extends AbstractSettingsScreen {
constructor() { constructor() {
super([SystemDropDownSettingsUserView, MenuSettingsUserView, PaneSettingsUserView]); super([SystemDropDownUserView, SettingsMenuUserView, SettingsPaneUserView]);
const views = [
UserSettingsGeneral
];
if (AppUserStore.allowContacts()) {
views.push(UserSettingsContacts);
}
if (SettingsCapa('AdditionalAccounts') || SettingsCapa('Identities')) {
views.push(UserSettingsAccounts);
}
if (SettingsCapa('Sieve')) {
views.push(UserSettingsFilters);
}
if (SettingsCapa('AutoLogout') || SettingsCapa('OpenPGP') || SettingsCapa('GnuPG')) {
views.push(UserSettingsSecurity);
}
views.push(UserSettingsFolders);
if (SettingsCapa('Themes')) {
views.push(UserSettingsThemes);
}
views.forEach((item, index) =>
settingsAddViewModel(item, item.name.replace('User', ''), 0, 0, 0 === index)
);
runSettingsViewModelHooks(false);
initOnStartOrLangChange( initOnStartOrLangChange(
() => this.sSettingsTitle = i18n('TITLES/SETTINGS'), () => this.sSettingsTitle = i18n('TITLES/SETTINGS'),
@ -33,67 +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() { onShow() {
this.setSettingsTitle(); this.setSettingsTitle();
keyScope(Scope.Settings); keyScope(Scope.Settings);

View file

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

View file

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

View file

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

View file

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

View file

@ -8,14 +8,11 @@ import Remote from 'Remote/Admin/Fetch';
import { DomainPopupView } from 'View/Popup/Domain'; import { DomainPopupView } from 'View/Popup/Domain';
import { DomainAliasPopupView } from 'View/Popup/DomainAlias'; import { DomainAliasPopupView } from 'View/Popup/DomainAlias';
export class DomainsAdminSettings { export class AdminSettingsDomains /*extends AbstractViewSettings*/ {
constructor() { constructor() {
this.domains = DomainAdminStore; this.domains = DomainAdminStore;
this.domainForDeletion = ko.observable(null).deleteAccessHelper(); this.domainForDeletion = ko.observable(null).askDeleteHelper();
this.onDomainListChangeRequest = this.onDomainListChangeRequest.bind(this);
this.onDomainLoadRequest = this.onDomainLoadRequest.bind(this);
} }
createDomain() { createDomain() {
@ -28,30 +25,32 @@ export class DomainsAdminSettings {
deleteDomain(domain) { deleteDomain(domain) {
DomainAdminStore.remove(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) { disableDomain(domain) {
domain.disabled(!domain.disabled()); 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) { onBuild(oDom) {
oDom.addEventListener('click', event => { oDom.addEventListener('click', event => {
let el = event.target.closestWithin('.b-admin-domains-list-table .e-item .e-action', oDom); let el = event.target.closestWithin('.b-admin-domains-list-table .e-action', oDom);
el && ko.dataFor(el) && Remote.domain(this.onDomainLoadRequest, ko.dataFor(el).name); el && ko.dataFor(el) && Remote.request('AdminDomainLoad',
(iError, oData) => iError || showScreenPopup(DomainPopupView, [oData.Result]),
{
Name: ko.dataFor(el).name
}
);
}); });
DomainAdminStore.fetch(); 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 { import {
isArray, isArray,
pInt,
settingsSaveHelperSimpleFunction,
changeTheme, changeTheme,
convertThemeName, convertThemeName
addObservablesTo,
addSubscribablesTo
} from 'Common/Utils'; } from 'Common/Utils';
import { Capa, SaveSettingsStep } from 'Common/Enums'; import { addObservablesTo, addSubscribablesTo, addComputablesTo } from 'External/ko';
import { Settings, SettingsGet } from 'Common/Globals';
import { reload as translatorReload, convertLangName } from 'Common/Translator';
import { SaveSettingsStep } from 'Common/Enums';
import { Settings, SettingsGet, SettingsCapa } from 'Common/Globals';
import { translatorReload, convertLangName } from 'Common/Translator';
import { AbstractViewSettings } from 'Knoin/AbstractViews';
import { showScreenPopup } from 'Knoin/Knoin'; import { showScreenPopup } from 'Knoin/Knoin';
import Remote from 'Remote/Admin/Fetch'; import Remote from 'Remote/Admin/Fetch';
import { ThemeStore } from 'Stores/Theme'; import { ThemeStore } from 'Stores/Theme';
import { LanguageStore } from 'Stores/Language'; import { LanguageStore } from 'Stores/Language';
import LanguagesPopupView from 'View/Popup/Languages'; import { LanguagesPopupView } from 'View/Popup/Languages';
export class GeneralAdminSettings { export class AdminSettingsGeneral extends AbstractViewSettings {
constructor() { constructor() {
super();
this.language = LanguageStore.language; this.language = LanguageStore.language;
this.languages = LanguageStore.languages; this.languages = LanguageStore.languages;
@ -36,18 +37,16 @@ export class GeneralAdminSettings {
this.theme = ThemeStore.theme; this.theme = ThemeStore.theme;
this.themes = ThemeStore.themes; this.themes = ThemeStore.themes;
this.addSettings(['AllowLanguagesOnSettings','NewMoveToFolder']);
addObservablesTo(this, { addObservablesTo(this, {
allowLanguagesOnSettings: !!SettingsGet('AllowLanguagesOnSettings'),
newMoveToFolder: !!SettingsGet('NewMoveToFolder'),
attachmentLimitTrigger: SaveSettingsStep.Idle, attachmentLimitTrigger: SaveSettingsStep.Idle,
languageTrigger: SaveSettingsStep.Idle,
themeTrigger: SaveSettingsStep.Idle, themeTrigger: SaveSettingsStep.Idle,
capaThemes: Settings.capa(Capa.Themes), capaThemes: SettingsCapa('Themes'),
capaUserBackground: Settings.capa(Capa.UserBackground), capaUserBackground: SettingsCapa('UserBackground'),
capaAdditionalAccounts: Settings.capa(Capa.AdditionalAccounts), capaAdditionalAccounts: SettingsCapa('AdditionalAccounts'),
capaIdentities: Settings.capa(Capa.Identities), capaIdentities: SettingsCapa('Identities'),
capaAttachmentThumbnails: Settings.capa(Capa.AttachmentThumbnails), capaAttachmentThumbnails: SettingsCapa('AttachmentThumbnails'),
capaTemplates: Settings.capa(Capa.Templates),
dataFolderAccess: false dataFolderAccess: false
}); });
@ -59,10 +58,14 @@ export class GeneralAdminSettings {
} }
*/ */
this.mainAttachmentLimit = ko this.attachmentLimit = ko
.observable(pInt(SettingsGet('AttachmentLimit')) / (1024 * 1024)) .observable(SettingsGet('AttachmentLimit') / (1024 * 1024))
.extend({ debounce: 500 }); .extend({ debounce: 500 });
this.addSetting('Language');
this.addSetting('AttachmentLimit');
this.addSetting('Theme', value => changeTheme(value, this.themeTrigger));
this.uploadData = SettingsGet('PhpUploadSizes'); this.uploadData = SettingsGet('PhpUploadSizes');
this.uploadDataDesc = this.uploadDataDesc =
this.uploadData && (this.uploadData.upload_max_filesize || this.uploadData.post_max_size) this.uploadData && (this.uploadData.upload_max_filesize || this.uploadData.post_max_size)
@ -74,12 +77,12 @@ export class GeneralAdminSettings {
].join('') ].join('')
: ''; : '';
this.themesOptions = ko.computed(() => addComputablesTo(this, {
this.themes.map(theme => ({ optValue: theme, optText: convertThemeName(theme) })) themesOptions: () => this.themes.map(theme => ({ optValue: theme, optText: convertThemeName(theme) })),
);
this.languageFullName = ko.computed(() => convertLangName(this.language())); languageFullName: () => convertLangName(this.language()),
this.languageAdminFullName = ko.computed(() => convertLangName(this.languageAdmin())); languageAdminFullName: () => convertLangName(this.languageAdmin())
});
this.languageAdminTrigger = ko.observable(SaveSettingsStep.Idle).extend({ debounce: 100 }); this.languageAdminTrigger = ko.observable(SaveSettingsStep.Idle).extend({ debounce: 100 });
@ -87,55 +90,25 @@ export class GeneralAdminSettings {
this.languageAdminTrigger(saveSettingsStep); this.languageAdminTrigger(saveSettingsStep);
setTimeout(() => this.languageAdminTrigger(SaveSettingsStep.Idle), 1000); setTimeout(() => this.languageAdminTrigger(SaveSettingsStep.Idle), 1000);
}, },
fSaveBoolHelper = (key, fn) => fSaveHelper = key => value => Remote.saveSetting(key, value);
value => {
const data = {};
data[key] = value ? 1 : 0;
Remote.saveAdminConfig(fn, data);
};
addSubscribablesTo(this, { addSubscribablesTo(this, {
mainAttachmentLimit: value =>
Remote.saveAdminConfig(settingsSaveHelperSimpleFunction(this.attachmentLimitTrigger, this), {
AttachmentLimit: pInt(value)
}),
language: value =>
Remote.saveAdminConfig(settingsSaveHelperSimpleFunction(this.languageTrigger, this), {
Language: value.trim()
}),
languageAdmin: value => { languageAdmin: value => {
this.languageAdminTrigger(SaveSettingsStep.Animate); this.languageAdminTrigger(SaveSettingsStep.Animate);
translatorReload(true, value) translatorReload(true, value)
.then(fReloadLanguageHelper(SaveSettingsStep.TrueResult), fReloadLanguageHelper(SaveSettingsStep.FalseResult)) .then(fReloadLanguageHelper(SaveSettingsStep.TrueResult), fReloadLanguageHelper(SaveSettingsStep.FalseResult))
.then(() => Remote.saveAdminConfig(null, { .then(() => Remote.saveSetting('LanguageAdmin', value));
LanguageAdmin: value.trim()
}));
}, },
theme: value => { capaAdditionalAccounts: fSaveHelper('CapaAdditionalAccounts'),
changeTheme(value, this.themeTrigger);
Remote.saveAdminConfig(settingsSaveHelperSimpleFunction(this.themeTrigger, this), {
Theme: value.trim()
});
},
capaAdditionalAccounts: fSaveBoolHelper('CapaAdditionalAccounts'), capaIdentities: fSaveHelper('CapaIdentities'),
capaIdentities: fSaveBoolHelper('CapaIdentities'), capaAttachmentThumbnails: fSaveHelper('CapaAttachmentThumbnails'),
capaTemplates: fSaveBoolHelper('CapaTemplates'), capaThemes: fSaveHelper('CapaThemes'),
capaAttachmentThumbnails: fSaveBoolHelper('CapaAttachmentThumbnails'), capaUserBackground: fSaveHelper('CapaUserBackground')
capaThemes: fSaveBoolHelper('CapaThemes'),
capaUserBackground: fSaveBoolHelper('CapaUserBackground'),
allowLanguagesOnSettings: fSaveBoolHelper('AllowLanguagesOnSettings'),
newMoveToFolder: fSaveBoolHelper('NewMoveToFolder')
}); });
} }

View file

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

View file

@ -6,42 +6,58 @@ import { getNotification } from 'Common/Translator';
import { PackageAdminStore } from 'Stores/Admin/Package'; import { PackageAdminStore } from 'Stores/Admin/Package';
import Remote from 'Remote/Admin/Fetch'; 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() { constructor() {
this.packagesError = ko.observable(''); super();
this.addSettings(['EnabledPlugins']);
addObservablesTo(this, {
packagesError: ''
});
this.packages = PackageAdminStore; this.packages = PackageAdminStore;
this.packagesCurrent = ko.computed(() => addComputablesTo(this, {
PackageAdminStore.filter(item => item && item.installed && !item.compare) packagesCurrent: () => PackageAdminStore().filter(item => item && item.installed && !item.canBeUpdated),
); packagesUpdate: () => PackageAdminStore().filter(item => item && item.installed && item.canBeUpdated),
this.packagesAvailableForUpdate = ko.computed(() => packagesAvailable: () => PackageAdminStore().filter(item => item && !item.installed),
PackageAdminStore.filter(item => item && item.installed && !!item.compare)
);
this.packagesAvailableForInstallation = ko.computed(() =>
PackageAdminStore.filter(item => item && !item.installed)
);
this.visibility = ko.computed(() => (PackageAdminStore.loading() ? 'visible' : 'hidden')); visibility: () => (PackageAdminStore.loading() ? 'visible' : 'hidden')
});
} }
onShow() { onShow() {
this.packagesError(''); this.packagesError('');
} }
onBuild() { onBuild(oDom) {
PackageAdminStore.fetch(); 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) { requestHelper(packageToRequest, install) {
return (iError, data) => { return (iError, data) => {
if (iError) {
this.packagesError(
getNotification(install ? Notification.CantInstallPackage : Notification.CantDeletePackage)
// ':\n' + getNotification(iError);
);
}
PackageAdminStore.forEach(item => { PackageAdminStore.forEach(item => {
if (item && packageToRequest && item.loading && item.loading() && packageToRequest.file === item.file) { if (item && packageToRequest && item.loading && item.loading() && packageToRequest.file === item.file) {
packageToRequest.loading(false); 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(); location.reload();
} else { } else {
PackageAdminStore.fetch(); PackageAdminStore.fetch();
@ -60,14 +81,49 @@ export class PackagesAdminSettings {
deletePackage(packageToDelete) { deletePackage(packageToDelete) {
if (packageToDelete) { if (packageToDelete) {
packageToDelete.loading(true); packageToDelete.loading(true);
Remote.packageDelete(this.requestHelper(packageToDelete, false), packageToDelete); Remote.request('AdminPackageDelete',
this.requestHelper(packageToDelete, false),
{
Id: packageToDelete.id
}
);
} }
} }
installPackage(packageToInstall) { installPackage(packageToInstall) {
if (packageToInstall) { if (packageToInstall) {
packageToInstall.loading(true); packageToInstall.loading(true);
Remote.packageInstall(this.requestHelper(packageToInstall, true), packageToInstall); Remote.request('AdminPackageInstall',
this.requestHelper(packageToInstall, true),
{
Id: packageToInstall.id,
Type: packageToInstall.type,
File: packageToInstall.file
},
60000
);
} }
} }
disablePlugin(plugin) {
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 { SettingsGet, SettingsCapa } from 'Common/Globals';
import { Settings, SettingsGet } from 'Common/Globals'; import { addObservablesTo, addSubscribablesTo } from 'External/ko';
import { addObservablesTo, addSubscribablesTo } from 'Common/Utils';
import Remote from 'Remote/Admin/Fetch'; import Remote from 'Remote/Admin/Fetch';
import { decorateKoCommands } from 'Knoin/Knoin'; import { decorateKoCommands } from 'Knoin/Knoin';
import { AbstractViewSettings } from 'Knoin/AbstractViews';
export class SecurityAdminSettings { export class AdminSettingsSecurity extends AbstractViewSettings {
constructor() { constructor() {
super();
this.addSettings(['UseLocalProxyForExternalImages','VerifySslCertificate','AllowSelfSigned']);
this.weakPassword = rl.app.weakPassword; this.weakPassword = rl.app.weakPassword;
addObservablesTo(this, { addObservablesTo(this, {
useLocalProxyForExternalImages: !!SettingsGet('UseLocalProxyForExternalImages'),
verifySslCertificate: !!SettingsGet('VerifySslCertificate'),
allowSelfSigned: !!SettingsGet('AllowSelfSigned'),
adminLogin: SettingsGet('AdminLogin'), adminLogin: SettingsGet('AdminLogin'),
adminLoginError: false, adminLoginError: false,
adminPassword: '', adminPassword: '',
adminPasswordNew: '', adminPasswordNew: '',
adminPasswordNew2: '', adminPasswordNew2: '',
adminPasswordNewError: false, adminPasswordNewError: false,
adminTOTP: SettingsGet('AdminTOTP'),
adminPasswordUpdateError: false, adminPasswordUpdateError: false,
adminPasswordUpdateSuccess: false, adminPasswordUpdateSuccess: false,
capaOpenPGP: Settings.capa(Capa.OpenPGP) capaOpenPGP: SettingsCapa('OpenPGP')
}); });
const reset = () => {
this.adminPasswordUpdateError(false);
this.adminPasswordUpdateSuccess(false);
this.adminPasswordNewError(false);
};
addSubscribablesTo(this, { addSubscribablesTo(this, {
adminPassword: () => { adminPassword: () => {
this.adminPasswordUpdateError(false); this.adminPasswordUpdateError(false);
@ -37,42 +43,12 @@ export class SecurityAdminSettings {
adminLogin: () => this.adminLoginError(false), adminLogin: () => this.adminLoginError(false),
adminPasswordNew: () => { adminPasswordNew: reset,
this.adminPasswordUpdateError(false);
this.adminPasswordUpdateSuccess(false);
this.adminPasswordNewError(false);
},
adminPasswordNew2: () => { adminPasswordNew2: reset,
this.adminPasswordUpdateError(false);
this.adminPasswordUpdateSuccess(false);
this.adminPasswordNewError(false);
},
capaOpenPGP: value => capaOpenPGP: value => Remote.saveSetting('CapaOpenPGP', value)
Remote.saveAdminConfig(null, {
CapaOpenPGP: value ? 1 : 0
}),
useLocalProxyForExternalImages: value =>
Remote.saveAdminConfig(null, {
UseLocalProxyForExternalImages: value ? 1 : 0
}),
verifySslCertificate: value => {
value => value || this.allowSelfSigned(true);
Remote.saveAdminConfig(null, {
VerifySslCertificate: value ? 1 : 0
}); });
},
allowSelfSigned: value =>
Remote.saveAdminConfig(null, {
AllowSelfSigned: value ? 1 : 0
})
});
this.onNewAdminPasswordResponse = this.onNewAdminPasswordResponse.bind(this);
decorateKoCommands(this, { decorateKoCommands(this, {
saveNewAdminPasswordCommand: self => self.adminLogin().trim() && self.adminPassword() saveNewAdminPasswordCommand: self => self.adminLogin().trim() && self.adminPassword()
@ -93,16 +69,7 @@ export class SecurityAdminSettings {
this.adminPasswordUpdateError(false); this.adminPasswordUpdateError(false);
this.adminPasswordUpdateSuccess(false); this.adminPasswordUpdateSuccess(false);
Remote.saveNewAdminPassword(this.onNewAdminPasswordResponse, { Remote.request('AdminPasswordUpdate', (iError, data) => {
'Login': this.adminLogin(),
'Password': this.adminPassword(),
'NewPassword': this.adminPasswordNew()
});
return true;
}
onNewAdminPasswordResponse(iError, data) {
if (iError) { if (iError) {
this.adminPasswordUpdateError(true); this.adminPasswordUpdateError(true);
} else { } else {
@ -114,6 +81,14 @@ export class SecurityAdminSettings {
this.weakPassword(!!data.Result.Weak); this.weakPassword(!!data.Result.Weak);
} }
}, {
'Login': this.adminLogin(),
'Password': this.adminPassword(),
'NewPassword': this.adminPasswordNew(),
'TOTP': this.adminTOTP()
});
return true;
} }
onHide() { onHide() {

View file

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

View file

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

View file

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

View file

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

View file

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

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