mirror of
https://github.com/the-djmaze/snappymail.git
synced 2026-07-13 03:27:39 +03:00
Merge branch 'the-djmaze:master' into nc-theme
This commit is contained in:
commit
47ed699525
881 changed files with 48156 additions and 36724 deletions
|
|
@ -53,26 +53,44 @@ pdo_password = ""
|
||||||
suggestions_limit = 30
|
suggestions_limit = 30
|
||||||
|
|
||||||
[security]
|
[security]
|
||||||
; Enable CSRF protection (http://en.wikipedia.org/wiki/Cross-site_request_forgery)
|
|
||||||
csrf_protection = On
|
|
||||||
custom_server_signature = "SnappyMail"
|
custom_server_signature = "SnappyMail"
|
||||||
x_xss_protection_header = "1; mode=block"
|
x_xss_protection_header = "1; mode=block"
|
||||||
openpgp = Off
|
openpgp = Off
|
||||||
|
|
||||||
|
; Access settings
|
||||||
|
allow_admin_panel = On
|
||||||
|
|
||||||
; Login and password for web admin panel
|
; Login and password for web admin panel
|
||||||
admin_login = "admin"
|
admin_login = "admin"
|
||||||
admin_password = ""
|
admin_password = ""
|
||||||
admin_totp = ""
|
admin_totp = ""
|
||||||
|
|
||||||
; Access settings
|
|
||||||
allow_admin_panel = On
|
|
||||||
hide_x_mailer_header = On
|
|
||||||
admin_panel_host = ""
|
admin_panel_host = ""
|
||||||
admin_panel_key = "admin"
|
admin_panel_key = "admin"
|
||||||
|
force_https = Off
|
||||||
|
hide_x_mailer_header = On
|
||||||
|
|
||||||
|
; For example to allow all images use "img-src https:". More info at https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy#directives
|
||||||
content_security_policy = ""
|
content_security_policy = ""
|
||||||
|
|
||||||
|
; Report CSP errors to PHP and/or SnappyMail Log
|
||||||
csp_report = Off
|
csp_report = Off
|
||||||
|
|
||||||
|
; A valid cipher method from https://php.net/openssl_get_cipher_methods
|
||||||
encrypt_cipher = "aes-256-cbc-hmac-sha1"
|
encrypt_cipher = "aes-256-cbc-hmac-sha1"
|
||||||
|
|
||||||
|
; Strict, Lax or None
|
||||||
|
cookie_samesite = "Strict"
|
||||||
|
|
||||||
|
; Additional allowed Sec-Fetch combinations separated by ";".
|
||||||
|
; For example:
|
||||||
|
; * Allow iframe on same domain in any mode: dest=iframe,site=same-origin
|
||||||
|
; * Allow navigate to iframe on same domain: mode=navigate,dest=iframe,site=same-origin
|
||||||
|
; * Allow navigate to iframe on (sub)domain: mode=navigate,dest=iframe,site=same-site
|
||||||
|
; * Allow navigate to iframe from any domain: mode=navigate,dest=iframe,site=cross-site
|
||||||
|
;
|
||||||
|
; Default is "site=same-origin;site=none"
|
||||||
|
secfetch_allow = ""
|
||||||
|
|
||||||
[admin_panel]
|
[admin_panel]
|
||||||
allow_update = Off
|
allow_update = Off
|
||||||
|
|
||||||
|
|
@ -93,20 +111,35 @@ cafile = ""
|
||||||
capath = ""
|
capath = ""
|
||||||
|
|
||||||
; Location of client certificate file (pem format with private key) on local filesystem
|
; Location of client certificate file (pem format with private key) on local filesystem
|
||||||
client_cert = ""
|
local_cert = ""
|
||||||
|
|
||||||
|
; This can help mitigate the CRIME attack vector.
|
||||||
|
disable_compression = On
|
||||||
|
|
||||||
[capa]
|
[capa]
|
||||||
quota = On
|
quota = On
|
||||||
|
|
||||||
|
; Allow clear folder and delete messages without moving to trash
|
||||||
dangerous_actions = On
|
dangerous_actions = On
|
||||||
message_actions = On
|
|
||||||
|
; Allow download attachments as Zip (and optionally others)
|
||||||
attachments_actions = On
|
attachments_actions = On
|
||||||
|
|
||||||
[login]
|
[login]
|
||||||
|
; If someone logs in without "@domain.tld", this value will be used
|
||||||
|
; When this value is HTTP_HOST, the $_SERVER["HTTP_HOST"] value is used.
|
||||||
|
; When this value is SERVER_NAME, the $_SERVER["SERVER_NAME"] value is used.
|
||||||
|
; When this value is gethostname, the gethostname() value is used.
|
||||||
|
;
|
||||||
default_domain = ""
|
default_domain = ""
|
||||||
|
|
||||||
; Allow language selection on webmail login screen
|
; Allow language selection on webmail login screen
|
||||||
allow_languages_on_login = On
|
allow_languages_on_login = On
|
||||||
|
|
||||||
|
; Detect language from browser header `Accept-Language`
|
||||||
determine_user_language = On
|
determine_user_language = On
|
||||||
|
|
||||||
|
; Like default_domain but then HTTP_HOST/SERVER_NAME without www.
|
||||||
determine_user_domain = Off
|
determine_user_domain = Off
|
||||||
login_lowercase = On
|
login_lowercase = On
|
||||||
|
|
||||||
|
|
@ -123,7 +156,7 @@ sign_me_auto = "DefaultOff"
|
||||||
; Enable plugin support
|
; Enable plugin support
|
||||||
enable = Off
|
enable = Off
|
||||||
|
|
||||||
; List of enabled plugins
|
; Comma-separated list of enabled plugins
|
||||||
enabled_list = ""
|
enabled_list = ""
|
||||||
|
|
||||||
[defaults]
|
[defaults]
|
||||||
|
|
@ -145,6 +178,9 @@ mail_reply_same_folder = Off
|
||||||
; Enable logging
|
; Enable logging
|
||||||
enable = Off
|
enable = Off
|
||||||
|
|
||||||
|
; Path where log files will be stored
|
||||||
|
path = ""
|
||||||
|
|
||||||
; Log messages of set RFC 5424 section 6.2.1 Severity level and higher (0 = highest, 7 = lowest).
|
; Log messages of set RFC 5424 section 6.2.1 Severity level and higher (0 = highest, 7 = lowest).
|
||||||
; 0 = Emergency
|
; 0 = Emergency
|
||||||
; 1 = Alert
|
; 1 = Alert
|
||||||
|
|
@ -201,6 +237,8 @@ auth_syslog = On
|
||||||
[debug]
|
[debug]
|
||||||
; Special option required for development purposes
|
; Special option required for development purposes
|
||||||
enable = Off
|
enable = Off
|
||||||
|
javascript = Off
|
||||||
|
css = Off
|
||||||
|
|
||||||
[cache]
|
[cache]
|
||||||
; The section controls caching of the entire application.
|
; The section controls caching of the entire application.
|
||||||
|
|
@ -208,6 +246,9 @@ enable = Off
|
||||||
; Enables caching in the system
|
; Enables caching in the system
|
||||||
enable = On
|
enable = On
|
||||||
|
|
||||||
|
; Path where cache files will be stored
|
||||||
|
path = ""
|
||||||
|
|
||||||
; Additional caching key. If changed, cache is purged
|
; Additional caching key. If changed, cache is purged
|
||||||
index = "v1"
|
index = "v1"
|
||||||
|
|
||||||
|
|
@ -225,55 +266,41 @@ http_expires = 3600
|
||||||
|
|
||||||
; Caching message UIDs when searching and sorting (threading)
|
; Caching message UIDs when searching and sorting (threading)
|
||||||
server_uids = On
|
server_uids = On
|
||||||
|
system_data = On
|
||||||
|
|
||||||
[imap]
|
[imap]
|
||||||
use_sort = On
|
|
||||||
use_force_selection = Off
|
use_force_selection = Off
|
||||||
use_thread = On
|
|
||||||
use_move = Off
|
|
||||||
use_expunge_all_on_delete = Off
|
use_expunge_all_on_delete = Off
|
||||||
body_text_limit = 555000
|
|
||||||
message_list_fast_simple_search = On
|
message_list_fast_simple_search = On
|
||||||
message_list_count_limit_trigger = 0
|
|
||||||
message_list_date_filter = 0
|
|
||||||
message_list_permanent_filter = ""
|
message_list_permanent_filter = ""
|
||||||
message_all_headers = Off
|
message_all_headers = Off
|
||||||
large_thread_limit = 50
|
|
||||||
folder_list_limit = 200
|
|
||||||
show_login_alert = On
|
show_login_alert = On
|
||||||
use_list_status = On
|
fetch_new_messages = On
|
||||||
timeout = 300
|
|
||||||
disable_metadata = Off
|
|
||||||
|
|
||||||
[labs]
|
[labs]
|
||||||
cache_system_data = On
|
; Display message RFC 2822 date and time header, instead of the arrival internal date.
|
||||||
date_from_headers = On
|
date_from_headers = On
|
||||||
autocreate_system_folders = Off
|
|
||||||
allow_message_append = Off
|
allow_message_append = Off
|
||||||
login_fault_delay = 1
|
|
||||||
|
; When login fails, wait N seconds before responding
|
||||||
|
login_fault_delay = 5
|
||||||
log_ajax_response_write_limit = 300
|
log_ajax_response_write_limit = 300
|
||||||
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
|
|
||||||
use_app_debug_js = Off
|
|
||||||
use_app_debug_css = Off
|
|
||||||
smtp_show_server_errors = Off
|
smtp_show_server_errors = Off
|
||||||
smtp_timeout = 60
|
|
||||||
sieve_auth_plain_initial = On
|
sieve_auth_plain_initial = On
|
||||||
sieve_allow_fileinto_inbox = Off
|
sieve_allow_fileinto_inbox = Off
|
||||||
sieve_timeout = 10
|
|
||||||
sasl_allow_plain = On
|
; PHP mail() remove To and Subject headers
|
||||||
sasl_allow_scram_sha = Off
|
|
||||||
sasl_allow_cram_md5 = Off
|
|
||||||
mail_func_clear_headers = On
|
mail_func_clear_headers = On
|
||||||
|
|
||||||
|
; PHP mail() set -f emailaddress
|
||||||
mail_func_additional_parameters = Off
|
mail_func_additional_parameters = Off
|
||||||
folders_spec_limit = 50
|
folders_spec_limit = 50
|
||||||
curl_proxy = ""
|
curl_proxy = ""
|
||||||
curl_proxy_auth = ""
|
curl_proxy_auth = ""
|
||||||
force_https = Off
|
|
||||||
custom_login_link = ""
|
custom_login_link = ""
|
||||||
custom_logout_link = ""
|
custom_logout_link = ""
|
||||||
allow_external_login = Off
|
|
||||||
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
|
||||||
|
|
@ -283,12 +310,11 @@ use_local_proxy_for_external_images = On
|
||||||
image_exif_auto_rotate = Off
|
image_exif_auto_rotate = Off
|
||||||
cookie_default_path = ""
|
cookie_default_path = ""
|
||||||
cookie_default_secure = Off
|
cookie_default_secure = Off
|
||||||
check_new_messages = On
|
|
||||||
replace_env_in_configuration = ""
|
replace_env_in_configuration = ""
|
||||||
boundary_prefix = ""
|
boundary_prefix = ""
|
||||||
dev_email = ""
|
dev_email = ""
|
||||||
dev_password = ""
|
dev_password = ""
|
||||||
|
|
||||||
[version]
|
[version]
|
||||||
current = "2.20.0"
|
current = "2.28.4"
|
||||||
saved = "Fri, 30 Sep 2022 12:34:56 +0000"
|
saved = "Sun, 18 Dec 2022 22:10:48 +0000"
|
||||||
|
|
|
||||||
1
.github/ISSUE_TEMPLATE/bug_report.md
vendored
1
.github/ISSUE_TEMPLATE/bug_report.md
vendored
|
|
@ -28,6 +28,7 @@ If applicable, add screenshots to help explain your problem.
|
||||||
- IMAP daemon: [e.g. courier, dovecot]
|
- IMAP daemon: [e.g. courier, dovecot]
|
||||||
- PHP version:
|
- PHP version:
|
||||||
- SnappyMail Version:
|
- SnappyMail Version:
|
||||||
|
- Mode: [e.g. standalone, nextcloud, cyberpanel, docker]
|
||||||
|
|
||||||
**[Debug/logging information](https://github.com/the-djmaze/snappymail/wiki/FAQ#how-do-i-enable-logging)**
|
**[Debug/logging information](https://github.com/the-djmaze/snappymail/wiki/FAQ#how-do-i-enable-logging)**
|
||||||
Place them here (few lines) or as attachments (many lines)
|
Place them here (few lines) or as attachments (many lines)
|
||||||
|
|
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -17,6 +17,7 @@
|
||||||
/data
|
/data
|
||||||
/MULTIPLY
|
/MULTIPLY
|
||||||
/include.php
|
/include.php
|
||||||
|
/issues
|
||||||
.idea/
|
.idea/
|
||||||
.env
|
.env
|
||||||
/test
|
/test
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,6 @@
|
||||||
# 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-XSS-Protection "1; mode=block"
|
# Header set X-XSS-Protection "1; mode=block"
|
||||||
Header set Service-Worker-Allowed "/"
|
Header set Service-Worker-Allowed "/"
|
||||||
|
|
||||||
|
|
|
||||||
1444
CHANGELOG.md
1444
CHANGELOG.md
File diff suppressed because it is too large
Load diff
|
|
@ -29,7 +29,7 @@
|
||||||
**Editing HTML Template Files**
|
**Editing HTML Template Files**
|
||||||
|
|
||||||
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**
|
**Release**
|
||||||
|
|
||||||
|
|
|
||||||
51
README.md
51
README.md
|
|
@ -34,14 +34,14 @@ 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 - 2022 SnappyMail
|
Copyright (c) 2020 - 2023 SnappyMail
|
||||||
Copyright (c) 2013 - 2022 RainLoop
|
Copyright (c) 2013 - 2022 RainLoop
|
||||||
|
|
||||||
## Modifications
|
## Modifications
|
||||||
|
|
||||||
This fork of RainLoop has the following changes:
|
This fork of RainLoop has the following changes:
|
||||||
|
|
||||||
* Privacy/GDPR friendly (no: Social, Gravatar, Facebook, Google, Twitter, DropBox, OwnCloud, X-Mailer)
|
* Privacy/GDPR friendly (no: Social, Gravatar, Facebook, Google, Twitter, DropBox, X-Mailer)
|
||||||
* 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
|
||||||
|
|
@ -57,10 +57,11 @@ This fork of RainLoop has the following changes:
|
||||||
* 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
|
* Removed Spyc yaml
|
||||||
|
* Removed OwnCloud
|
||||||
* 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)
|
* 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
|
||||||
* Updated [Sabre/VObject](https://github.com/sabre-io/vobject)
|
* Updated [Sabre/VObject](https://github.com/sabre-io/vobject)
|
||||||
* Split Admin specific JavaScript code from User code
|
* Split Admin specific JavaScript code from User code
|
||||||
|
|
@ -105,7 +106,6 @@ Supported are:
|
||||||
The result is faster and smaller download code (good for mobile networks).
|
The result is faster and smaller download code (good for mobile networks).
|
||||||
|
|
||||||
* Added dev/prototype.js for some additional features
|
* Added dev/prototype.js for some additional features
|
||||||
* boot.js without webpack overhead
|
|
||||||
* Modified Jua.js to be without jQuery
|
* Modified Jua.js to be without jQuery
|
||||||
* Replaced Autolinker with simple https/email detection
|
* Replaced Autolinker with simple https/email detection
|
||||||
* Replaced momentToNode with proper HTML5 `<time>`
|
* Replaced momentToNode with proper HTML5 `<time>`
|
||||||
|
|
@ -118,6 +118,7 @@ The result is faster and smaller download code (good for mobile networks).
|
||||||
* Replaced simplestatemanager with CSS @media
|
* Replaced simplestatemanager with CSS @media
|
||||||
* Replaced inputosaurus with own code
|
* Replaced inputosaurus with own code
|
||||||
* Replaced keymaster with own shortcuts handler
|
* Replaced keymaster with own shortcuts handler
|
||||||
|
* Replaced OpenPGP.js v2 with OpenPGP.js v5
|
||||||
* Removed ifvisible.js
|
* Removed ifvisible.js
|
||||||
* Removed pikaday
|
* Removed pikaday
|
||||||
* Removed underscore
|
* Removed underscore
|
||||||
|
|
@ -140,28 +141,28 @@ RainLoop 1.17 vs SnappyMail
|
||||||
|
|
||||||
|js/* |RainLoop |Snappy |
|
|js/* |RainLoop |Snappy |
|
||||||
|--------------- |--------: |--------: |
|
|--------------- |--------: |--------: |
|
||||||
|admin.js |2.170.153 | 81.545 |
|
|admin.js |2.170.153 | 80.102 |
|
||||||
|app.js |4.207.787 | 406.032 |
|
|app.js |4.207.787 | 407.874 |
|
||||||
|boot.js | 868.735 | 2.050 |
|
|boot.js | 868.735 | 4.142 |
|
||||||
|libs.js | 658.812 | 192.965 |
|
|libs.js | 658.812 | 187.076 |
|
||||||
|sieve.js | 0 | 86.121 |
|
|sieve.js | 0 | 85.141 |
|
||||||
|polyfills.js | 334.608 | 0 |
|
|polyfills.js | 334.608 | 0 |
|
||||||
|serviceworker.js | 0 | 285 |
|
|serviceworker.js | 0 | 285 |
|
||||||
|TOTAL |8.240.095 | 768.998 |
|
|TOTAL |8.240.095 | 764.620 |
|
||||||
|
|
||||||
|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 | 256.831 | 40.255 | 73.606 | 13.435 | 60.877 | 12.047 |
|
|admin.min.js | 256.831 | 39.350 | 73.606 | 13.163 | 60.877 | 11.805 |
|
||||||
|app.min.js | 515.367 | 188.046 |139.456 | 62.537 |110.485 | 53.693 |
|
|app.min.js | 515.367 | 186.311 |139.456 | 62.929 |110.485 | 54.076 |
|
||||||
|boot.min.js | 84.659 | 1.252 | 26.998 | 778 | 23.643 | 628 |
|
|boot.min.js | 84.659 | 2.084 | 26.998 | 1.202 | 23.643 | 1.003 |
|
||||||
|libs.min.js | 584.772 | 92.055 |180.901 | 34.113 |155.182 | 30.641 |
|
|libs.min.js | 584.772 | 90.808 |180.901 | 33.754 |155.182 | 30.224 |
|
||||||
|sieve.min.js | 0 | 41.926 | 0 | 10.484 | 0 | 9.451 |
|
|sieve.min.js | 0 | 41.399 | 0 | 10.394 | 0 | 9.356 |
|
||||||
|polyfills.min.js | 32.837 | 0 | 11.406 | 0 | 10.175 | 0 |
|
|polyfills.min.js | 32.837 | 0 | 11.406 | 0 | 10.175 | 0 |
|
||||||
|TOTAL user |1.217.635 | 281.353 |358.761 | 97.428 |299.485 | 84.962 |
|
|TOTAL user |1.217.635 | 279.203 |358.761 | 97.885 |299.485 | 85.303 |
|
||||||
|TOTAL user+sieve |1.217.635 | 323.279 |358.761 |107.912 |299.485 | 94.413 |
|
|TOTAL user+sieve |1.217.635 | 320.602 |358.761 |108.279 |299.485 | 94.659 |
|
||||||
|TOTAL admin | 959.099 | 133.562 |292.911 | 48.326 |249.877 | 43.316 |
|
|TOTAL admin | 959.099 | 132.242 |292.911 | 48.119 |249.877 | 43.032 |
|
||||||
|
|
||||||
For a user its around 70% smaller and faster than traditional RainLoop.
|
For a user it is around 72% smaller and faster than traditional RainLoop.
|
||||||
|
|
||||||
### CSS changes
|
### CSS changes
|
||||||
|
|
||||||
|
|
@ -169,6 +170,7 @@ For a user its around 70% 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
|
||||||
|
* Use border-box
|
||||||
* Split app.css to have separate admin.css
|
* Split app.css to have separate admin.css
|
||||||
* Remove oldschool 'float'
|
* Remove oldschool 'float'
|
||||||
* Remove unused css
|
* Remove unused css
|
||||||
|
|
@ -187,12 +189,12 @@ For a user its around 70% 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.331 | 82.870 | 46.946 | 17.215 | 14.727 |
|
|app.css | 340.331 | 84.390 | 46.946 | 17.605 | 15.084 |
|
||||||
|app.min.css | 274.947 | 66.646 | 39.647 | 15.260 | 13.322 |
|
|app.min.css | 274.947 | 67.774 | 39.647 | 15.487 | 13.527 |
|
||||||
|boot.css | | 1.326 | | 664 | 545 |
|
|boot.css | | 1.326 | | 664 | 545 |
|
||||||
|boot.min.css | | 1.071 | | 590 | 474 |
|
|boot.min.css | | 1.071 | | 590 | 474 |
|
||||||
|admin.css | | 30.506 | | 6.907 | 5.998 |
|
|admin.css | | 30.482 | | 6.988 | 6.092 |
|
||||||
|admin.min.css | | 24.677 | | 6.267 | 5.498 |
|
|admin.min.css | | 24.607 | | 6.315 | 5.579 |
|
||||||
|
|
||||||
### PGP
|
### PGP
|
||||||
RainLoop uses the old OpenPGP.js v2
|
RainLoop uses the old OpenPGP.js v2
|
||||||
|
|
@ -206,7 +208,7 @@ See https://github.com/the-djmaze/openpgpjs for development
|
||||||
|
|
||||||
|OpenPGP |RainLoop |Snappy |RL gzip |SM gzip |RL brotli |SM brotli |
|
|OpenPGP |RainLoop |Snappy |RL gzip |SM gzip |RL brotli |SM brotli |
|
||||||
|--------------- |--------: |--------: |------: |-------: |--------: |--------: |
|
|--------------- |--------: |--------: |------: |-------: |--------: |--------: |
|
||||||
|openpgp.min.js | 330.742 | 539.257 |102.388 | 166.998 | 84.241 | 137.186 |
|
|openpgp.min.js | 330.742 | 541.176 |102.388 | 168.266 | 84.241 | 138.278 |
|
||||||
|openpgp.worker | 1.499 | | 824 | | 695 | |
|
|openpgp.worker | 1.499 | | 824 | | 695 | |
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -216,7 +218,6 @@ The [Squire](https://github.com/neilj/Squire) implementation is not 100% compati
|
||||||
Still TODO:
|
Still TODO:
|
||||||
|
|
||||||
* support for tables (really needed?!?)
|
* support for tables (really needed?!?)
|
||||||
* support BIDI (really needed?!?)
|
|
||||||
|
|
||||||
| | normal | min | gzip | min gzip |
|
| | normal | min | gzip | min gzip |
|
||||||
|-------- |-------: |-------: |------: |--------: |
|
|-------- |-------: |-------: |------: |--------: |
|
||||||
|
|
|
||||||
15
_include.php
15
_include.php
|
|
@ -4,16 +4,6 @@
|
||||||
|
|
||||||
//header('Strict-Transport-Security: max-age=31536000');
|
//header('Strict-Transport-Security: max-age=31536000');
|
||||||
|
|
||||||
/**
|
|
||||||
* Uncomment to use gzip compressed output
|
|
||||||
*/
|
|
||||||
//define('USE_GZIP', 1);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Uncomment to use brotli compressed output
|
|
||||||
*/
|
|
||||||
//define('USE_BROTLI', 1);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Uncomment to enable multiple domain installation.
|
* Uncomment to enable multiple domain installation.
|
||||||
*/
|
*/
|
||||||
|
|
@ -34,3 +24,8 @@
|
||||||
* Additional configuration file name
|
* Additional configuration file name
|
||||||
*/
|
*/
|
||||||
//define('APP_CONFIGURATION_NAME', $_SERVER['HTTP_HOST'].'.ini');
|
//define('APP_CONFIGURATION_NAME', $_SERVER['HTTP_HOST'].'.ini');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Also update extensions on upgrade
|
||||||
|
*/
|
||||||
|
//define('SNAPPYMAIL_UPDATE_PLUGINS', 1);
|
||||||
|
|
|
||||||
|
|
@ -1,50 +0,0 @@
|
||||||
<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|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|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|gz)$">
|
|
||||||
Header set Content-Type "application/javascript; charset=utf-8"
|
|
||||||
ForceType application/javascript
|
|
||||||
</FilesMatch>
|
|
||||||
<FilesMatch "\.css\.(br|gz)$">
|
|
||||||
Header set Content-Type "text/css; charset=utf-8"
|
|
||||||
ForceType text/css
|
|
||||||
</FilesMatch>
|
|
||||||
</IfModule>
|
|
||||||
</IfModule>
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 1.8 KiB |
38
build/cpanel.php
Executable file
38
build/cpanel.php
Executable file
|
|
@ -0,0 +1,38 @@
|
||||||
|
<?php
|
||||||
|
echo "\x1b[33;1m === cPanel === \x1b[0m\n";
|
||||||
|
|
||||||
|
$cpanel_destination = "{$destPath}snappymail-{$package->version}-cpanel.tar";
|
||||||
|
|
||||||
|
@unlink($cpanel_destination);
|
||||||
|
@unlink("{$cpanel_destination}.gz");
|
||||||
|
|
||||||
|
$cpanel_tar = new PharData($cpanel_destination);
|
||||||
|
|
||||||
|
$cpanel_tar->buildFromDirectory('./integrations/cpanel', "@integrations/cpanel/@");
|
||||||
|
|
||||||
|
$cpanel_path = '/usr/local/cpanel/base/3rdparty/snappymail/';
|
||||||
|
|
||||||
|
$files = new RecursiveIteratorIterator(new RecursiveDirectoryIterator('snappymail/v'), RecursiveIteratorIterator::SELF_FIRST);
|
||||||
|
foreach ($files as $file) {
|
||||||
|
if (is_file($file)) {
|
||||||
|
$newFile = str_replace('\\', '/', $file);
|
||||||
|
$cpanel_tar->addFile($file, "{$cpanel_path}{$newFile}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$cpanel_tar->addFile('.htaccess', "{$cpanel_path}.htaccess");
|
||||||
|
$cpanel_tar->addFile('README.md', "{$cpanel_path}README.md");
|
||||||
|
$cpanel_tar->addFromString("{$cpanel_path}VERSION", $package->version);
|
||||||
|
|
||||||
|
$index = file_get_contents('index.php');
|
||||||
|
$index = str_replace('0.0.0', $package->version, $index);
|
||||||
|
$cpanel_tar->addFromString("{$cpanel_path}index.php", $index);
|
||||||
|
|
||||||
|
$data = file_get_contents('dev/serviceworker.js');
|
||||||
|
$cpanel_tar->addFromString("{$cpanel_path}serviceworker.js", $data);
|
||||||
|
|
||||||
|
$cpanel_tar->compress(Phar::GZ);
|
||||||
|
unlink($cpanel_destination);
|
||||||
|
$cpanel_destination .= '.gz';
|
||||||
|
|
||||||
|
echo "{$cpanel_destination} created\n";
|
||||||
|
|
@ -1,46 +1,92 @@
|
||||||
<?php
|
<?php
|
||||||
echo "\x1b[33;1m === Nextcloud === \x1b[0m\n";
|
echo "\x1b[33;1m === Nextcloud === \x1b[0m\n";
|
||||||
|
|
||||||
|
$cert_dir = $_SERVER['HOME'].'/.nextcloud/certificates';
|
||||||
|
|
||||||
$nc_destination = "{$destPath}snappymail-{$package->version}-nextcloud.tar";
|
$nc_destination = "{$destPath}snappymail-{$package->version}-nextcloud.tar";
|
||||||
|
|
||||||
@unlink($nc_destination);
|
@unlink($nc_destination);
|
||||||
@unlink("{$nc_destination}.gz");
|
@unlink("{$nc_destination}.gz");
|
||||||
|
|
||||||
$nc_tar = new PharData($nc_destination);
|
$nc_tar = new PharData($nc_destination);
|
||||||
|
$hashes = [];
|
||||||
|
|
||||||
|
file_put_contents(ROOT_DIR . '/integrations/nextcloud/snappymail/VERSION', $package->version);
|
||||||
|
$file = ROOT_DIR . '/integrations/nextcloud/snappymail/appinfo/info.xml';
|
||||||
|
file_put_contents($file, preg_replace('/<version>[^<]*</', "<version>{$package->version}<", file_get_contents($file)));
|
||||||
|
|
||||||
$nc_tar->buildFromDirectory('./integrations/nextcloud', "@integrations/nextcloud/snappymail/@");
|
$nc_tar->buildFromDirectory('./integrations/nextcloud', "@integrations/nextcloud/snappymail/@");
|
||||||
|
$files = new RecursiveIteratorIterator(new RecursiveDirectoryIterator('integrations/nextcloud/snappymail'));
|
||||||
|
foreach ($files as $file) {
|
||||||
|
if (is_file($file)) {
|
||||||
|
$name = str_replace('\\', '/', $file);
|
||||||
|
$name = str_replace('integrations/nextcloud/snappymail/', '', $name);
|
||||||
|
$hashes[$name] = hash_file('sha512', $file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$files = new RecursiveIteratorIterator(new RecursiveDirectoryIterator('snappymail/v'), RecursiveIteratorIterator::SELF_FIRST);
|
$files = new RecursiveIteratorIterator(new RecursiveDirectoryIterator('snappymail/v'), RecursiveIteratorIterator::SELF_FIRST);
|
||||||
foreach ($files as $file) {
|
foreach ($files as $file) {
|
||||||
if (is_file($file)) {
|
if (is_file($file)) {
|
||||||
$newFile = str_replace('\\', '/', $file);
|
$newFile = str_replace('\\', '/', $file);
|
||||||
$newFile = str_replace("'snappymail/v/'.", '', $newFile);
|
$newName = str_replace('/.htaccess', '/_htaccess', $newFile);
|
||||||
$nc_tar->addFile($file, "snappymail/app/{$newFile}");
|
$nc_tar->addFile($file, "snappymail/app/{$newName}");
|
||||||
|
$hashes["app/{$newFile}"] = hash_file('sha512', $file);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
$nc_tar->addFile('data/.htaccess');
|
$nc_tar->addFile('data/.htaccess');
|
||||||
$nc_tar->addFromString('data/VERSION', $package->version);
|
$nc_tar->addFromString('data/VERSION', $package->version);
|
||||||
$nc_tar->addFile('data/README.md');
|
$nc_tar->addFile('data/README.md');
|
||||||
$nc_tar->addFile('_include.php', 'snappymail/app/_include.php');
|
$nc_tar->addFile('_include.php', 'snappymail/app/_include.php');
|
||||||
*/
|
*/
|
||||||
$nc_tar->addFile('.htaccess', 'snappymail/app/.htaccess');
|
$nc_tar->addFile('.htaccess', 'snappymail/app/_htaccess');
|
||||||
|
$hashes['app/.htaccess'] = hash_file('sha512', '.htaccess');
|
||||||
|
|
||||||
$index = file_get_contents('index.php');
|
$index = file_get_contents('index.php');
|
||||||
$index = str_replace('0.0.0', $package->version, $index);
|
$index = str_replace('0.0.0', $package->version, $index);
|
||||||
$index = str_replace('snappymail/v/', '', $index);
|
//$index = str_replace('snappymail/v/', '', $index);
|
||||||
$nc_tar->addFromString('snappymail/app/index.php', $index);
|
$nc_tar->addFromString('snappymail/app/index.php', $index);
|
||||||
|
$hashes['app/index.php'] = hash('sha512', $index);
|
||||||
|
|
||||||
$nc_tar->addFile('README.md', 'snappymail/app/README.md');
|
$nc_tar->addFile('README.md', 'snappymail/app/README.md');
|
||||||
|
$hashes['app/README.md'] = hash_file('sha512', 'README.md');
|
||||||
|
|
||||||
$nc_tar->addFile('CHANGELOG.md', 'snappymail/CHANGELOG.md');
|
$nc_tar->addFile('CHANGELOG.md', 'snappymail/CHANGELOG.md');
|
||||||
|
$hashes['CHANGELOG.md'] = hash_file('sha512', 'CHANGELOG.md');
|
||||||
|
|
||||||
$data = file_get_contents('dev/serviceworker.js');
|
$data = file_get_contents('dev/serviceworker.js');
|
||||||
$nc_tar->addFromString('snappymail/app/serviceworker.js', $data);
|
$nc_tar->addFromString('snappymail/app/serviceworker.js', $data);
|
||||||
|
$hashes['app/serviceworker.js'] = hash('sha512', $data);
|
||||||
|
|
||||||
|
spl_autoload_register(function($name){
|
||||||
|
$file = __DIR__ . '/' . str_replace('\\', '/', $name) . '.php';
|
||||||
|
require $file;
|
||||||
|
});
|
||||||
|
|
||||||
|
ksort($hashes);
|
||||||
|
$cert = file_get_contents($cert_dir.'/snappymail.crt');
|
||||||
|
$rsa = new \phpseclib\Crypt\RSA();
|
||||||
|
$rsa->loadKey(file_get_contents($cert_dir.'/snappymail.key'));
|
||||||
|
$x509 = new \phpseclib\File\X509();
|
||||||
|
$x509->loadX509($cert);
|
||||||
|
$x509->setPrivateKey($rsa);
|
||||||
|
$rsa->setSignatureMode(\phpseclib\Crypt\RSA::SIGNATURE_PSS);
|
||||||
|
$rsa->setMGFHash('sha512');
|
||||||
|
$rsa->setSaltLength(0);
|
||||||
|
$signature = $rsa->sign(json_encode($hashes));
|
||||||
|
$nc_tar->addFromString('snappymail/appinfo/signature.json', json_encode([
|
||||||
|
'hashes' => $hashes,
|
||||||
|
'signature' => base64_encode($signature),
|
||||||
|
'certificate' => $cert
|
||||||
|
], JSON_PRETTY_PRINT));
|
||||||
|
|
||||||
$nc_tar->compress(Phar::GZ);
|
$nc_tar->compress(Phar::GZ);
|
||||||
unlink($nc_destination);
|
unlink($nc_destination);
|
||||||
$nc_destination .= '.gz';
|
$nc_destination .= '.gz';
|
||||||
|
|
||||||
$signature = shell_exec("openssl dgst -sha512 -sign ~/.nextcloud/certificates/snappymail.key {$nc_destination} | openssl base64");
|
$signature = shell_exec("openssl dgst -sha512 -sign {$cert_dir}/snappymail.key {$nc_destination} | openssl base64");
|
||||||
file_put_contents($nc_destination.'.sig', $signature);
|
file_put_contents($nc_destination.'.sig', $signature);
|
||||||
|
|
||||||
echo "{$nc_destination} created\n";
|
echo "{$nc_destination} created\n";
|
||||||
|
|
|
||||||
100
build/owncloud.php
Executable file
100
build/owncloud.php
Executable file
|
|
@ -0,0 +1,100 @@
|
||||||
|
<?php
|
||||||
|
echo "\x1b[33;1m === ownCloud === \x1b[0m\n";
|
||||||
|
|
||||||
|
$cert_dir = false; // $_SERVER['HOME'].'/.owncloud/certificates';
|
||||||
|
|
||||||
|
$nc_destination = "{$destPath}snappymail-{$package->version}-owncloud.tar";
|
||||||
|
|
||||||
|
@unlink($nc_destination);
|
||||||
|
@unlink("{$nc_destination}.gz");
|
||||||
|
|
||||||
|
$nc_tar = new PharData($nc_destination);
|
||||||
|
$hashes = [];
|
||||||
|
|
||||||
|
file_put_contents(ROOT_DIR . '/integrations/owncloud/snappymail/VERSION', $package->version);
|
||||||
|
$file = ROOT_DIR . '/integrations/owncloud/snappymail/appinfo/info.xml';
|
||||||
|
file_put_contents($file, preg_replace('/<version>[^<]*</', "<version>{$package->version}<", file_get_contents($file)));
|
||||||
|
|
||||||
|
$nc_tar->buildFromDirectory('./integrations/owncloud', "@integrations/owncloud/snappymail/@");
|
||||||
|
$files = new RecursiveIteratorIterator(new RecursiveDirectoryIterator('integrations/owncloud/snappymail'));
|
||||||
|
foreach ($files as $file) {
|
||||||
|
if (is_file($file)) {
|
||||||
|
$name = str_replace('\\', '/', $file);
|
||||||
|
$name = str_replace('integrations/owncloud/snappymail/', '', $name);
|
||||||
|
$hashes[$name] = hash_file('sha512', $file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$files = new RecursiveIteratorIterator(new RecursiveDirectoryIterator('snappymail/v'), RecursiveIteratorIterator::SELF_FIRST);
|
||||||
|
foreach ($files as $file) {
|
||||||
|
if (is_file($file)) {
|
||||||
|
$newFile = str_replace('\\', '/', $file);
|
||||||
|
// $newFile = str_replace("'snappymail/v/'.", '', $newFile);
|
||||||
|
$nc_tar->addFile($file, "snappymail/app/{$newFile}");
|
||||||
|
$hashes["app/{$newFile}"] = hash_file('sha512', $file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$nc_tar->delete("snappymail/app/snappymail/v/{$package->version}/app/.htaccess");
|
||||||
|
$nc_tar->addFile("snappymail/v/{$package->version}/app/.htaccess", "snappymail/app/snappymail/v/{$package->version}/app/_htaccess");
|
||||||
|
$nc_tar->delete("snappymail/app/snappymail/v/{$package->version}/static/.htaccess");
|
||||||
|
$nc_tar->addFile("snappymail/v/{$package->version}/static/.htaccess", "snappymail/app/snappymail/v/{$package->version}/static/_htaccess");
|
||||||
|
|
||||||
|
/*
|
||||||
|
$nc_tar->addFile('data/.htaccess');
|
||||||
|
$nc_tar->addFromString('data/VERSION', $package->version);
|
||||||
|
$nc_tar->addFile('data/README.md');
|
||||||
|
$nc_tar->addFile('_include.php', 'snappymail/app/_include.php');
|
||||||
|
*/
|
||||||
|
$nc_tar->addFile('.htaccess', 'snappymail/app/_htaccess');
|
||||||
|
$hashes['app/.htaccess'] = hash_file('sha512', '.htaccess');
|
||||||
|
|
||||||
|
$index = file_get_contents('index.php');
|
||||||
|
$index = str_replace('0.0.0', $package->version, $index);
|
||||||
|
//$index = str_replace('snappymail/v/', '', $index);
|
||||||
|
$nc_tar->addFromString('snappymail/app/index.php', $index);
|
||||||
|
$hashes['app/index.php'] = hash('sha512', $index);
|
||||||
|
|
||||||
|
$nc_tar->addFile('README.md', 'snappymail/app/README.md');
|
||||||
|
$hashes['app/README.md'] = hash_file('sha512', 'README.md');
|
||||||
|
|
||||||
|
$nc_tar->addFile('CHANGELOG.md', 'snappymail/CHANGELOG.md');
|
||||||
|
$hashes['CHANGELOG.md'] = hash_file('sha512', 'CHANGELOG.md');
|
||||||
|
|
||||||
|
$data = file_get_contents('dev/serviceworker.js');
|
||||||
|
$nc_tar->addFromString('snappymail/app/serviceworker.js', $data);
|
||||||
|
$hashes['app/serviceworker.js'] = hash('sha512', $data);
|
||||||
|
|
||||||
|
if ($cert_dir) {
|
||||||
|
spl_autoload_register(function($name){
|
||||||
|
$file = __DIR__ . '/' . str_replace('\\', '/', $name) . '.php';
|
||||||
|
require $file;
|
||||||
|
});
|
||||||
|
|
||||||
|
ksort($hashes);
|
||||||
|
$cert = file_get_contents($cert_dir.'/snappymail.crt');
|
||||||
|
$rsa = new \phpseclib\Crypt\RSA();
|
||||||
|
$rsa->loadKey(file_get_contents($cert_dir.'/snappymail.key'));
|
||||||
|
$x509 = new \phpseclib\File\X509();
|
||||||
|
$x509->loadX509($cert);
|
||||||
|
$x509->setPrivateKey($rsa);
|
||||||
|
$rsa->setSignatureMode(\phpseclib\Crypt\RSA::SIGNATURE_PSS);
|
||||||
|
$rsa->setMGFHash('sha512');
|
||||||
|
$rsa->setSaltLength(0);
|
||||||
|
$signature = $rsa->sign(json_encode($hashes));
|
||||||
|
$nc_tar->addFromString('snappymail/appinfo/signature.json', json_encode([
|
||||||
|
'hashes' => $hashes,
|
||||||
|
'signature' => base64_encode($signature),
|
||||||
|
'certificate' => $cert
|
||||||
|
], JSON_PRETTY_PRINT));
|
||||||
|
}
|
||||||
|
|
||||||
|
$nc_tar->compress(Phar::GZ);
|
||||||
|
unlink($nc_destination);
|
||||||
|
$nc_destination .= '.gz';
|
||||||
|
|
||||||
|
if ($cert_dir) {
|
||||||
|
$signature = shell_exec("openssl dgst -sha512 -sign {$cert_dir}/snappymail.key {$nc_destination} | openssl base64");
|
||||||
|
file_put_contents($nc_destination.'.sig', $signature);
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "{$nc_destination} created\n";
|
||||||
893
build/phpseclib/Crypt/Hash.php
Normal file
893
build/phpseclib/Crypt/Hash.php
Normal file
|
|
@ -0,0 +1,893 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pure-PHP implementations of keyed-hash message authentication codes (HMACs) and various cryptographic hashing functions.
|
||||||
|
*
|
||||||
|
* Uses hash() or mhash() if available and an internal implementation, otherwise. Currently supports the following:
|
||||||
|
*
|
||||||
|
* md2, md5, md5-96, sha1, sha1-96, sha256, sha256-96, sha384, and sha512, sha512-96
|
||||||
|
*
|
||||||
|
* If {@link self::setKey() setKey()} is called, {@link self::hash() hash()} will return the HMAC as opposed to
|
||||||
|
* the hash. If no valid algorithm is provided, sha1 will be used.
|
||||||
|
*
|
||||||
|
* PHP version 5
|
||||||
|
*
|
||||||
|
* {@internal The variable names are the same as those in
|
||||||
|
* {@link http://tools.ietf.org/html/rfc2104#section-2 RFC2104}.}}
|
||||||
|
*
|
||||||
|
* Here's a short example of how to use this library:
|
||||||
|
* <code>
|
||||||
|
* <?php
|
||||||
|
* include 'vendor/autoload.php';
|
||||||
|
*
|
||||||
|
* $hash = new \phpseclib\Crypt\Hash('sha1');
|
||||||
|
*
|
||||||
|
* $hash->setKey('abcdefg');
|
||||||
|
*
|
||||||
|
* echo base64_encode($hash->hash('abcdefg'));
|
||||||
|
* ?>
|
||||||
|
* </code>
|
||||||
|
*
|
||||||
|
* @category Crypt
|
||||||
|
* @package Hash
|
||||||
|
* @author Jim Wigginton <terrafrost@php.net>
|
||||||
|
* @copyright 2007 Jim Wigginton
|
||||||
|
* @license http://www.opensource.org/licenses/mit-license.html MIT License
|
||||||
|
* @link http://phpseclib.sourceforge.net
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace phpseclib\Crypt;
|
||||||
|
|
||||||
|
use phpseclib\Math\BigInteger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pure-PHP implementations of keyed-hash message authentication codes (HMACs) and various cryptographic hashing functions.
|
||||||
|
*
|
||||||
|
* @package Hash
|
||||||
|
* @author Jim Wigginton <terrafrost@php.net>
|
||||||
|
* @access public
|
||||||
|
*/
|
||||||
|
class Hash
|
||||||
|
{
|
||||||
|
/**#@+
|
||||||
|
* @access private
|
||||||
|
* @see \phpseclib\Crypt\Hash::__construct()
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* Toggles the internal implementation
|
||||||
|
*/
|
||||||
|
const MODE_INTERNAL = 1;
|
||||||
|
/**
|
||||||
|
* Toggles the mhash() implementation, which has been deprecated on PHP 5.3.0+.
|
||||||
|
*/
|
||||||
|
const MODE_MHASH = 2;
|
||||||
|
/**
|
||||||
|
* Toggles the hash() implementation, which works on PHP 5.1.2+.
|
||||||
|
*/
|
||||||
|
const MODE_HASH = 3;
|
||||||
|
/**#@-*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hash Parameter
|
||||||
|
*
|
||||||
|
* @see self::setHash()
|
||||||
|
* @var int
|
||||||
|
* @access private
|
||||||
|
*/
|
||||||
|
var $hashParam;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Byte-length of compression blocks / key (Internal HMAC)
|
||||||
|
*
|
||||||
|
* @see self::setAlgorithm()
|
||||||
|
* @var int
|
||||||
|
* @access private
|
||||||
|
*/
|
||||||
|
var $b;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Byte-length of hash output (Internal HMAC)
|
||||||
|
*
|
||||||
|
* @see self::setHash()
|
||||||
|
* @var int
|
||||||
|
* @access private
|
||||||
|
*/
|
||||||
|
var $l = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hash Algorithm
|
||||||
|
*
|
||||||
|
* @see self::setHash()
|
||||||
|
* @var string
|
||||||
|
* @access private
|
||||||
|
*/
|
||||||
|
var $hash;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Key
|
||||||
|
*
|
||||||
|
* @see self::setKey()
|
||||||
|
* @var string
|
||||||
|
* @access private
|
||||||
|
*/
|
||||||
|
var $key = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Computed Key
|
||||||
|
*
|
||||||
|
* @see self::_computeKey()
|
||||||
|
* @var string
|
||||||
|
* @access private
|
||||||
|
*/
|
||||||
|
var $computedKey = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Outer XOR (Internal HMAC)
|
||||||
|
*
|
||||||
|
* @see self::setKey()
|
||||||
|
* @var string
|
||||||
|
* @access private
|
||||||
|
*/
|
||||||
|
var $opad;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inner XOR (Internal HMAC)
|
||||||
|
*
|
||||||
|
* @see self::setKey()
|
||||||
|
* @var string
|
||||||
|
* @access private
|
||||||
|
*/
|
||||||
|
var $ipad;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Engine
|
||||||
|
*
|
||||||
|
* @see self::setHash()
|
||||||
|
* @var string
|
||||||
|
* @access private
|
||||||
|
*/
|
||||||
|
var $engine;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default Constructor.
|
||||||
|
*
|
||||||
|
* @param string $hash
|
||||||
|
* @return \phpseclib\Crypt\Hash
|
||||||
|
* @access public
|
||||||
|
*/
|
||||||
|
function __construct($hash = 'sha1')
|
||||||
|
{
|
||||||
|
if (!defined('CRYPT_HASH_MODE')) {
|
||||||
|
switch (true) {
|
||||||
|
case extension_loaded('hash'):
|
||||||
|
define('CRYPT_HASH_MODE', self::MODE_HASH);
|
||||||
|
break;
|
||||||
|
case extension_loaded('mhash'):
|
||||||
|
define('CRYPT_HASH_MODE', self::MODE_MHASH);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
define('CRYPT_HASH_MODE', self::MODE_INTERNAL);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->setHash($hash);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the key for HMACs
|
||||||
|
*
|
||||||
|
* Keys can be of any length.
|
||||||
|
*
|
||||||
|
* @access public
|
||||||
|
* @param string $key
|
||||||
|
*/
|
||||||
|
function setKey($key = false)
|
||||||
|
{
|
||||||
|
$this->key = $key;
|
||||||
|
$this->_computeKey();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pre-compute the key used by the HMAC
|
||||||
|
*
|
||||||
|
* Quoting http://tools.ietf.org/html/rfc2104#section-2, "Applications that use keys longer than B bytes
|
||||||
|
* will first hash the key using H and then use the resultant L byte string as the actual key to HMAC."
|
||||||
|
*
|
||||||
|
* As documented in https://www.reddit.com/r/PHP/comments/9nct2l/symfonypolyfill_hash_pbkdf2_correct_fix_for/
|
||||||
|
* when doing an HMAC multiple times it's faster to compute the hash once instead of computing it during
|
||||||
|
* every call
|
||||||
|
*
|
||||||
|
* @access private
|
||||||
|
*/
|
||||||
|
function _computeKey()
|
||||||
|
{
|
||||||
|
if ($this->key === false) {
|
||||||
|
$this->computedKey = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (strlen($this->key) <= $this->b) {
|
||||||
|
$this->computedKey = $this->key;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch ($this->engine) {
|
||||||
|
case self::MODE_MHASH:
|
||||||
|
$this->computedKey = mhash($this->hash, $this->key);
|
||||||
|
break;
|
||||||
|
case self::MODE_HASH:
|
||||||
|
$this->computedKey = hash($this->hash, $this->key, true);
|
||||||
|
break;
|
||||||
|
case self::MODE_INTERNAL:
|
||||||
|
$this->computedKey = call_user_func($this->hash, $this->key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the hash function.
|
||||||
|
*
|
||||||
|
* As set by the constructor or by the setHash() method.
|
||||||
|
*
|
||||||
|
* @access public
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
function getHash()
|
||||||
|
{
|
||||||
|
return $this->hashParam;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the hash function.
|
||||||
|
*
|
||||||
|
* @access public
|
||||||
|
* @param string $hash
|
||||||
|
*/
|
||||||
|
function setHash($hash)
|
||||||
|
{
|
||||||
|
$this->hashParam = $hash = strtolower($hash);
|
||||||
|
switch ($hash) {
|
||||||
|
case 'md5-96':
|
||||||
|
case 'sha1-96':
|
||||||
|
case 'sha256-96':
|
||||||
|
case 'sha512-96':
|
||||||
|
$hash = substr($hash, 0, -3);
|
||||||
|
$this->l = 12; // 96 / 8 = 12
|
||||||
|
break;
|
||||||
|
case 'md2':
|
||||||
|
case 'md5':
|
||||||
|
$this->l = 16;
|
||||||
|
break;
|
||||||
|
case 'sha1':
|
||||||
|
$this->l = 20;
|
||||||
|
break;
|
||||||
|
case 'sha256':
|
||||||
|
$this->l = 32;
|
||||||
|
break;
|
||||||
|
case 'sha384':
|
||||||
|
$this->l = 48;
|
||||||
|
break;
|
||||||
|
case 'sha512':
|
||||||
|
$this->l = 64;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch ($hash) {
|
||||||
|
case 'md2-96':
|
||||||
|
case 'md2':
|
||||||
|
$this->b = 16;
|
||||||
|
case 'md5-96':
|
||||||
|
case 'sha1-96':
|
||||||
|
case 'sha224-96':
|
||||||
|
case 'sha256-96':
|
||||||
|
case 'md2':
|
||||||
|
case 'md5':
|
||||||
|
case 'sha1':
|
||||||
|
case 'sha224':
|
||||||
|
case 'sha256':
|
||||||
|
$this->b = 64;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
$this->b = 128;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch ($hash) {
|
||||||
|
case 'md2':
|
||||||
|
$this->engine = CRYPT_HASH_MODE == self::MODE_HASH && in_array('md2', hash_algos()) ?
|
||||||
|
self::MODE_HASH : self::MODE_INTERNAL;
|
||||||
|
break;
|
||||||
|
case 'sha384':
|
||||||
|
case 'sha512':
|
||||||
|
$this->engine = CRYPT_HASH_MODE == self::MODE_MHASH ? self::MODE_INTERNAL : CRYPT_HASH_MODE;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
$this->engine = CRYPT_HASH_MODE;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch ($this->engine) {
|
||||||
|
case self::MODE_MHASH:
|
||||||
|
switch ($hash) {
|
||||||
|
case 'md5':
|
||||||
|
$this->hash = MHASH_MD5;
|
||||||
|
break;
|
||||||
|
case 'sha256':
|
||||||
|
$this->hash = MHASH_SHA256;
|
||||||
|
break;
|
||||||
|
case 'sha1':
|
||||||
|
default:
|
||||||
|
$this->hash = MHASH_SHA1;
|
||||||
|
}
|
||||||
|
$this->_computeKey(self::MODE_MHASH);
|
||||||
|
return;
|
||||||
|
case self::MODE_HASH:
|
||||||
|
switch ($hash) {
|
||||||
|
case 'md5':
|
||||||
|
$this->hash = 'md5';
|
||||||
|
return;
|
||||||
|
case 'md2':
|
||||||
|
case 'sha256':
|
||||||
|
case 'sha384':
|
||||||
|
case 'sha512':
|
||||||
|
$this->hash = $hash;
|
||||||
|
return;
|
||||||
|
case 'sha1':
|
||||||
|
default:
|
||||||
|
$this->hash = 'sha1';
|
||||||
|
}
|
||||||
|
$this->_computeKey(self::MODE_HASH);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch ($hash) {
|
||||||
|
case 'md2':
|
||||||
|
$this->hash = array($this, '_md2');
|
||||||
|
break;
|
||||||
|
case 'md5':
|
||||||
|
$this->hash = array($this, '_md5');
|
||||||
|
break;
|
||||||
|
case 'sha256':
|
||||||
|
$this->hash = array($this, '_sha256');
|
||||||
|
break;
|
||||||
|
case 'sha384':
|
||||||
|
case 'sha512':
|
||||||
|
$this->hash = array($this, '_sha512');
|
||||||
|
break;
|
||||||
|
case 'sha1':
|
||||||
|
default:
|
||||||
|
$this->hash = array($this, '_sha1');
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->ipad = str_repeat(chr(0x36), $this->b);
|
||||||
|
$this->opad = str_repeat(chr(0x5C), $this->b);
|
||||||
|
|
||||||
|
$this->_computeKey(self::MODE_INTERNAL);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compute the HMAC.
|
||||||
|
*
|
||||||
|
* @access public
|
||||||
|
* @param string $text
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
function hash($text)
|
||||||
|
{
|
||||||
|
if (!empty($this->key) || is_string($this->key)) {
|
||||||
|
switch ($this->engine) {
|
||||||
|
case self::MODE_MHASH:
|
||||||
|
$output = mhash($this->hash, $text, $this->computedKey);
|
||||||
|
break;
|
||||||
|
case self::MODE_HASH:
|
||||||
|
$output = hash_hmac($this->hash, $text, $this->computedKey, true);
|
||||||
|
break;
|
||||||
|
case self::MODE_INTERNAL:
|
||||||
|
$key = str_pad($this->computedKey, $this->b, chr(0)); // step 1
|
||||||
|
$temp = $this->ipad ^ $key; // step 2
|
||||||
|
$temp .= $text; // step 3
|
||||||
|
$temp = call_user_func($this->hash, $temp); // step 4
|
||||||
|
$output = $this->opad ^ $key; // step 5
|
||||||
|
$output.= $temp; // step 6
|
||||||
|
$output = call_user_func($this->hash, $output); // step 7
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
switch ($this->engine) {
|
||||||
|
case self::MODE_MHASH:
|
||||||
|
$output = mhash($this->hash, $text);
|
||||||
|
break;
|
||||||
|
case self::MODE_HASH:
|
||||||
|
$output = hash($this->hash, $text, true);
|
||||||
|
break;
|
||||||
|
case self::MODE_INTERNAL:
|
||||||
|
$output = call_user_func($this->hash, $text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return substr($output, 0, $this->l);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the hash length (in bytes)
|
||||||
|
*
|
||||||
|
* @access public
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
function getLength()
|
||||||
|
{
|
||||||
|
return $this->l;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrapper for MD5
|
||||||
|
*
|
||||||
|
* @access private
|
||||||
|
* @param string $m
|
||||||
|
*/
|
||||||
|
function _md5($m)
|
||||||
|
{
|
||||||
|
return pack('H*', md5($m));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrapper for SHA1
|
||||||
|
*
|
||||||
|
* @access private
|
||||||
|
* @param string $m
|
||||||
|
*/
|
||||||
|
function _sha1($m)
|
||||||
|
{
|
||||||
|
return pack('H*', sha1($m));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pure-PHP implementation of MD2
|
||||||
|
*
|
||||||
|
* See {@link http://tools.ietf.org/html/rfc1319 RFC1319}.
|
||||||
|
*
|
||||||
|
* @access private
|
||||||
|
* @param string $m
|
||||||
|
*/
|
||||||
|
function _md2($m)
|
||||||
|
{
|
||||||
|
static $s = array(
|
||||||
|
41, 46, 67, 201, 162, 216, 124, 1, 61, 54, 84, 161, 236, 240, 6,
|
||||||
|
19, 98, 167, 5, 243, 192, 199, 115, 140, 152, 147, 43, 217, 188,
|
||||||
|
76, 130, 202, 30, 155, 87, 60, 253, 212, 224, 22, 103, 66, 111, 24,
|
||||||
|
138, 23, 229, 18, 190, 78, 196, 214, 218, 158, 222, 73, 160, 251,
|
||||||
|
245, 142, 187, 47, 238, 122, 169, 104, 121, 145, 21, 178, 7, 63,
|
||||||
|
148, 194, 16, 137, 11, 34, 95, 33, 128, 127, 93, 154, 90, 144, 50,
|
||||||
|
39, 53, 62, 204, 231, 191, 247, 151, 3, 255, 25, 48, 179, 72, 165,
|
||||||
|
181, 209, 215, 94, 146, 42, 172, 86, 170, 198, 79, 184, 56, 210,
|
||||||
|
150, 164, 125, 182, 118, 252, 107, 226, 156, 116, 4, 241, 69, 157,
|
||||||
|
112, 89, 100, 113, 135, 32, 134, 91, 207, 101, 230, 45, 168, 2, 27,
|
||||||
|
96, 37, 173, 174, 176, 185, 246, 28, 70, 97, 105, 52, 64, 126, 15,
|
||||||
|
85, 71, 163, 35, 221, 81, 175, 58, 195, 92, 249, 206, 186, 197,
|
||||||
|
234, 38, 44, 83, 13, 110, 133, 40, 132, 9, 211, 223, 205, 244, 65,
|
||||||
|
129, 77, 82, 106, 220, 55, 200, 108, 193, 171, 250, 36, 225, 123,
|
||||||
|
8, 12, 189, 177, 74, 120, 136, 149, 139, 227, 99, 232, 109, 233,
|
||||||
|
203, 213, 254, 59, 0, 29, 57, 242, 239, 183, 14, 102, 88, 208, 228,
|
||||||
|
166, 119, 114, 248, 235, 117, 75, 10, 49, 68, 80, 180, 143, 237,
|
||||||
|
31, 26, 219, 153, 141, 51, 159, 17, 131, 20
|
||||||
|
);
|
||||||
|
|
||||||
|
// Step 1. Append Padding Bytes
|
||||||
|
$pad = 16 - (strlen($m) & 0xF);
|
||||||
|
$m.= str_repeat(chr($pad), $pad);
|
||||||
|
|
||||||
|
$length = strlen($m);
|
||||||
|
|
||||||
|
// Step 2. Append Checksum
|
||||||
|
$c = str_repeat(chr(0), 16);
|
||||||
|
$l = chr(0);
|
||||||
|
for ($i = 0; $i < $length; $i+= 16) {
|
||||||
|
for ($j = 0; $j < 16; $j++) {
|
||||||
|
// RFC1319 incorrectly states that C[j] should be set to S[c xor L]
|
||||||
|
//$c[$j] = chr($s[ord($m[$i + $j] ^ $l)]);
|
||||||
|
// per <http://www.rfc-editor.org/errata_search.php?rfc=1319>, however, C[j] should be set to S[c xor L] xor C[j]
|
||||||
|
$c[$j] = chr($s[ord($m[$i + $j] ^ $l)] ^ ord($c[$j]));
|
||||||
|
$l = $c[$j];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$m.= $c;
|
||||||
|
|
||||||
|
$length+= 16;
|
||||||
|
|
||||||
|
// Step 3. Initialize MD Buffer
|
||||||
|
$x = str_repeat(chr(0), 48);
|
||||||
|
|
||||||
|
// Step 4. Process Message in 16-Byte Blocks
|
||||||
|
for ($i = 0; $i < $length; $i+= 16) {
|
||||||
|
for ($j = 0; $j < 16; $j++) {
|
||||||
|
$x[$j + 16] = $m[$i + $j];
|
||||||
|
$x[$j + 32] = $x[$j + 16] ^ $x[$j];
|
||||||
|
}
|
||||||
|
$t = chr(0);
|
||||||
|
for ($j = 0; $j < 18; $j++) {
|
||||||
|
for ($k = 0; $k < 48; $k++) {
|
||||||
|
$x[$k] = $t = $x[$k] ^ chr($s[ord($t)]);
|
||||||
|
//$t = $x[$k] = $x[$k] ^ chr($s[ord($t)]);
|
||||||
|
}
|
||||||
|
$t = chr(ord($t) + $j);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 5. Output
|
||||||
|
return substr($x, 0, 16);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pure-PHP implementation of SHA256
|
||||||
|
*
|
||||||
|
* See {@link http://en.wikipedia.org/wiki/SHA_hash_functions#SHA-256_.28a_SHA-2_variant.29_pseudocode SHA-256 (a SHA-2 variant) pseudocode - Wikipedia}.
|
||||||
|
*
|
||||||
|
* @access private
|
||||||
|
* @param string $m
|
||||||
|
*/
|
||||||
|
function _sha256($m)
|
||||||
|
{
|
||||||
|
if (extension_loaded('suhosin')) {
|
||||||
|
return pack('H*', sha256($m));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize variables
|
||||||
|
$hash = array(
|
||||||
|
0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19
|
||||||
|
);
|
||||||
|
// Initialize table of round constants
|
||||||
|
// (first 32 bits of the fractional parts of the cube roots of the first 64 primes 2..311)
|
||||||
|
static $k = array(
|
||||||
|
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
|
||||||
|
0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
|
||||||
|
0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
|
||||||
|
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
|
||||||
|
0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
|
||||||
|
0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
|
||||||
|
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
|
||||||
|
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2
|
||||||
|
);
|
||||||
|
|
||||||
|
// Pre-processing
|
||||||
|
$length = strlen($m);
|
||||||
|
// to round to nearest 56 mod 64, we'll add 64 - (length + (64 - 56)) % 64
|
||||||
|
$m.= str_repeat(chr(0), 64 - (($length + 8) & 0x3F));
|
||||||
|
$m[$length] = chr(0x80);
|
||||||
|
// we don't support hashing strings 512MB long
|
||||||
|
$m.= pack('N2', 0, $length << 3);
|
||||||
|
|
||||||
|
// Process the message in successive 512-bit chunks
|
||||||
|
$chunks = str_split($m, 64);
|
||||||
|
foreach ($chunks as $chunk) {
|
||||||
|
$w = array();
|
||||||
|
for ($i = 0; $i < 16; $i++) {
|
||||||
|
extract(unpack('Ntemp', $this->_string_shift($chunk, 4)));
|
||||||
|
$w[] = $temp;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extend the sixteen 32-bit words into sixty-four 32-bit words
|
||||||
|
for ($i = 16; $i < 64; $i++) {
|
||||||
|
// @codingStandardsIgnoreStart
|
||||||
|
$s0 = $this->_rightRotate($w[$i - 15], 7) ^
|
||||||
|
$this->_rightRotate($w[$i - 15], 18) ^
|
||||||
|
$this->_rightShift( $w[$i - 15], 3);
|
||||||
|
$s1 = $this->_rightRotate($w[$i - 2], 17) ^
|
||||||
|
$this->_rightRotate($w[$i - 2], 19) ^
|
||||||
|
$this->_rightShift( $w[$i - 2], 10);
|
||||||
|
// @codingStandardsIgnoreEnd
|
||||||
|
$w[$i] = $this->_add($w[$i - 16], $s0, $w[$i - 7], $s1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize hash value for this chunk
|
||||||
|
list($a, $b, $c, $d, $e, $f, $g, $h) = $hash;
|
||||||
|
|
||||||
|
// Main loop
|
||||||
|
for ($i = 0; $i < 64; $i++) {
|
||||||
|
$s0 = $this->_rightRotate($a, 2) ^
|
||||||
|
$this->_rightRotate($a, 13) ^
|
||||||
|
$this->_rightRotate($a, 22);
|
||||||
|
$maj = ($a & $b) ^
|
||||||
|
($a & $c) ^
|
||||||
|
($b & $c);
|
||||||
|
$t2 = $this->_add($s0, $maj);
|
||||||
|
|
||||||
|
$s1 = $this->_rightRotate($e, 6) ^
|
||||||
|
$this->_rightRotate($e, 11) ^
|
||||||
|
$this->_rightRotate($e, 25);
|
||||||
|
$ch = ($e & $f) ^
|
||||||
|
($this->_not($e) & $g);
|
||||||
|
$t1 = $this->_add($h, $s1, $ch, $k[$i], $w[$i]);
|
||||||
|
|
||||||
|
$h = $g;
|
||||||
|
$g = $f;
|
||||||
|
$f = $e;
|
||||||
|
$e = $this->_add($d, $t1);
|
||||||
|
$d = $c;
|
||||||
|
$c = $b;
|
||||||
|
$b = $a;
|
||||||
|
$a = $this->_add($t1, $t2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add this chunk's hash to result so far
|
||||||
|
$hash = array(
|
||||||
|
$this->_add($hash[0], $a),
|
||||||
|
$this->_add($hash[1], $b),
|
||||||
|
$this->_add($hash[2], $c),
|
||||||
|
$this->_add($hash[3], $d),
|
||||||
|
$this->_add($hash[4], $e),
|
||||||
|
$this->_add($hash[5], $f),
|
||||||
|
$this->_add($hash[6], $g),
|
||||||
|
$this->_add($hash[7], $h)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Produce the final hash value (big-endian)
|
||||||
|
return pack('N8', $hash[0], $hash[1], $hash[2], $hash[3], $hash[4], $hash[5], $hash[6], $hash[7]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pure-PHP implementation of SHA384 and SHA512
|
||||||
|
*
|
||||||
|
* @access private
|
||||||
|
* @param string $m
|
||||||
|
*/
|
||||||
|
function _sha512($m)
|
||||||
|
{
|
||||||
|
static $init384, $init512, $k;
|
||||||
|
|
||||||
|
if (!isset($k)) {
|
||||||
|
// Initialize variables
|
||||||
|
$init384 = array( // initial values for SHA384
|
||||||
|
'cbbb9d5dc1059ed8', '629a292a367cd507', '9159015a3070dd17', '152fecd8f70e5939',
|
||||||
|
'67332667ffc00b31', '8eb44a8768581511', 'db0c2e0d64f98fa7', '47b5481dbefa4fa4'
|
||||||
|
);
|
||||||
|
$init512 = array( // initial values for SHA512
|
||||||
|
'6a09e667f3bcc908', 'bb67ae8584caa73b', '3c6ef372fe94f82b', 'a54ff53a5f1d36f1',
|
||||||
|
'510e527fade682d1', '9b05688c2b3e6c1f', '1f83d9abfb41bd6b', '5be0cd19137e2179'
|
||||||
|
);
|
||||||
|
|
||||||
|
for ($i = 0; $i < 8; $i++) {
|
||||||
|
$init384[$i] = new BigInteger($init384[$i], 16);
|
||||||
|
$init384[$i]->setPrecision(64);
|
||||||
|
$init512[$i] = new BigInteger($init512[$i], 16);
|
||||||
|
$init512[$i]->setPrecision(64);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize table of round constants
|
||||||
|
// (first 64 bits of the fractional parts of the cube roots of the first 80 primes 2..409)
|
||||||
|
$k = array(
|
||||||
|
'428a2f98d728ae22', '7137449123ef65cd', 'b5c0fbcfec4d3b2f', 'e9b5dba58189dbbc',
|
||||||
|
'3956c25bf348b538', '59f111f1b605d019', '923f82a4af194f9b', 'ab1c5ed5da6d8118',
|
||||||
|
'd807aa98a3030242', '12835b0145706fbe', '243185be4ee4b28c', '550c7dc3d5ffb4e2',
|
||||||
|
'72be5d74f27b896f', '80deb1fe3b1696b1', '9bdc06a725c71235', 'c19bf174cf692694',
|
||||||
|
'e49b69c19ef14ad2', 'efbe4786384f25e3', '0fc19dc68b8cd5b5', '240ca1cc77ac9c65',
|
||||||
|
'2de92c6f592b0275', '4a7484aa6ea6e483', '5cb0a9dcbd41fbd4', '76f988da831153b5',
|
||||||
|
'983e5152ee66dfab', 'a831c66d2db43210', 'b00327c898fb213f', 'bf597fc7beef0ee4',
|
||||||
|
'c6e00bf33da88fc2', 'd5a79147930aa725', '06ca6351e003826f', '142929670a0e6e70',
|
||||||
|
'27b70a8546d22ffc', '2e1b21385c26c926', '4d2c6dfc5ac42aed', '53380d139d95b3df',
|
||||||
|
'650a73548baf63de', '766a0abb3c77b2a8', '81c2c92e47edaee6', '92722c851482353b',
|
||||||
|
'a2bfe8a14cf10364', 'a81a664bbc423001', 'c24b8b70d0f89791', 'c76c51a30654be30',
|
||||||
|
'd192e819d6ef5218', 'd69906245565a910', 'f40e35855771202a', '106aa07032bbd1b8',
|
||||||
|
'19a4c116b8d2d0c8', '1e376c085141ab53', '2748774cdf8eeb99', '34b0bcb5e19b48a8',
|
||||||
|
'391c0cb3c5c95a63', '4ed8aa4ae3418acb', '5b9cca4f7763e373', '682e6ff3d6b2b8a3',
|
||||||
|
'748f82ee5defb2fc', '78a5636f43172f60', '84c87814a1f0ab72', '8cc702081a6439ec',
|
||||||
|
'90befffa23631e28', 'a4506cebde82bde9', 'bef9a3f7b2c67915', 'c67178f2e372532b',
|
||||||
|
'ca273eceea26619c', 'd186b8c721c0c207', 'eada7dd6cde0eb1e', 'f57d4f7fee6ed178',
|
||||||
|
'06f067aa72176fba', '0a637dc5a2c898a6', '113f9804bef90dae', '1b710b35131c471b',
|
||||||
|
'28db77f523047d84', '32caab7b40c72493', '3c9ebe0a15c9bebc', '431d67c49c100d4c',
|
||||||
|
'4cc5d4becb3e42b6', '597f299cfc657e2a', '5fcb6fab3ad6faec', '6c44198c4a475817'
|
||||||
|
);
|
||||||
|
|
||||||
|
for ($i = 0; $i < 80; $i++) {
|
||||||
|
$k[$i] = new BigInteger($k[$i], 16);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$hash = $this->l == 48 ? $init384 : $init512;
|
||||||
|
|
||||||
|
// Pre-processing
|
||||||
|
$length = strlen($m);
|
||||||
|
// to round to nearest 112 mod 128, we'll add 128 - (length + (128 - 112)) % 128
|
||||||
|
$m.= str_repeat(chr(0), 128 - (($length + 16) & 0x7F));
|
||||||
|
$m[$length] = chr(0x80);
|
||||||
|
// we don't support hashing strings 512MB long
|
||||||
|
$m.= pack('N4', 0, 0, 0, $length << 3);
|
||||||
|
|
||||||
|
// Process the message in successive 1024-bit chunks
|
||||||
|
$chunks = str_split($m, 128);
|
||||||
|
foreach ($chunks as $chunk) {
|
||||||
|
$w = array();
|
||||||
|
for ($i = 0; $i < 16; $i++) {
|
||||||
|
$temp = new BigInteger($this->_string_shift($chunk, 8), 256);
|
||||||
|
$temp->setPrecision(64);
|
||||||
|
$w[] = $temp;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extend the sixteen 32-bit words into eighty 32-bit words
|
||||||
|
for ($i = 16; $i < 80; $i++) {
|
||||||
|
$temp = array(
|
||||||
|
$w[$i - 15]->bitwise_rightRotate(1),
|
||||||
|
$w[$i - 15]->bitwise_rightRotate(8),
|
||||||
|
$w[$i - 15]->bitwise_rightShift(7)
|
||||||
|
);
|
||||||
|
$s0 = $temp[0]->bitwise_xor($temp[1]);
|
||||||
|
$s0 = $s0->bitwise_xor($temp[2]);
|
||||||
|
$temp = array(
|
||||||
|
$w[$i - 2]->bitwise_rightRotate(19),
|
||||||
|
$w[$i - 2]->bitwise_rightRotate(61),
|
||||||
|
$w[$i - 2]->bitwise_rightShift(6)
|
||||||
|
);
|
||||||
|
$s1 = $temp[0]->bitwise_xor($temp[1]);
|
||||||
|
$s1 = $s1->bitwise_xor($temp[2]);
|
||||||
|
$w[$i] = $w[$i - 16]->copy();
|
||||||
|
$w[$i] = $w[$i]->add($s0);
|
||||||
|
$w[$i] = $w[$i]->add($w[$i - 7]);
|
||||||
|
$w[$i] = $w[$i]->add($s1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize hash value for this chunk
|
||||||
|
$a = $hash[0]->copy();
|
||||||
|
$b = $hash[1]->copy();
|
||||||
|
$c = $hash[2]->copy();
|
||||||
|
$d = $hash[3]->copy();
|
||||||
|
$e = $hash[4]->copy();
|
||||||
|
$f = $hash[5]->copy();
|
||||||
|
$g = $hash[6]->copy();
|
||||||
|
$h = $hash[7]->copy();
|
||||||
|
|
||||||
|
// Main loop
|
||||||
|
for ($i = 0; $i < 80; $i++) {
|
||||||
|
$temp = array(
|
||||||
|
$a->bitwise_rightRotate(28),
|
||||||
|
$a->bitwise_rightRotate(34),
|
||||||
|
$a->bitwise_rightRotate(39)
|
||||||
|
);
|
||||||
|
$s0 = $temp[0]->bitwise_xor($temp[1]);
|
||||||
|
$s0 = $s0->bitwise_xor($temp[2]);
|
||||||
|
$temp = array(
|
||||||
|
$a->bitwise_and($b),
|
||||||
|
$a->bitwise_and($c),
|
||||||
|
$b->bitwise_and($c)
|
||||||
|
);
|
||||||
|
$maj = $temp[0]->bitwise_xor($temp[1]);
|
||||||
|
$maj = $maj->bitwise_xor($temp[2]);
|
||||||
|
$t2 = $s0->add($maj);
|
||||||
|
|
||||||
|
$temp = array(
|
||||||
|
$e->bitwise_rightRotate(14),
|
||||||
|
$e->bitwise_rightRotate(18),
|
||||||
|
$e->bitwise_rightRotate(41)
|
||||||
|
);
|
||||||
|
$s1 = $temp[0]->bitwise_xor($temp[1]);
|
||||||
|
$s1 = $s1->bitwise_xor($temp[2]);
|
||||||
|
$temp = array(
|
||||||
|
$e->bitwise_and($f),
|
||||||
|
$g->bitwise_and($e->bitwise_not())
|
||||||
|
);
|
||||||
|
$ch = $temp[0]->bitwise_xor($temp[1]);
|
||||||
|
$t1 = $h->add($s1);
|
||||||
|
$t1 = $t1->add($ch);
|
||||||
|
$t1 = $t1->add($k[$i]);
|
||||||
|
$t1 = $t1->add($w[$i]);
|
||||||
|
|
||||||
|
$h = $g->copy();
|
||||||
|
$g = $f->copy();
|
||||||
|
$f = $e->copy();
|
||||||
|
$e = $d->add($t1);
|
||||||
|
$d = $c->copy();
|
||||||
|
$c = $b->copy();
|
||||||
|
$b = $a->copy();
|
||||||
|
$a = $t1->add($t2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add this chunk's hash to result so far
|
||||||
|
$hash = array(
|
||||||
|
$hash[0]->add($a),
|
||||||
|
$hash[1]->add($b),
|
||||||
|
$hash[2]->add($c),
|
||||||
|
$hash[3]->add($d),
|
||||||
|
$hash[4]->add($e),
|
||||||
|
$hash[5]->add($f),
|
||||||
|
$hash[6]->add($g),
|
||||||
|
$hash[7]->add($h)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Produce the final hash value (big-endian)
|
||||||
|
// (\phpseclib\Crypt\Hash::hash() trims the output for hashes but not for HMACs. as such, we trim the output here)
|
||||||
|
$temp = $hash[0]->toBytes() . $hash[1]->toBytes() . $hash[2]->toBytes() . $hash[3]->toBytes() .
|
||||||
|
$hash[4]->toBytes() . $hash[5]->toBytes();
|
||||||
|
if ($this->l != 48) {
|
||||||
|
$temp.= $hash[6]->toBytes() . $hash[7]->toBytes();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $temp;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Right Rotate
|
||||||
|
*
|
||||||
|
* @access private
|
||||||
|
* @param int $int
|
||||||
|
* @param int $amt
|
||||||
|
* @see self::_sha256()
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
function _rightRotate($int, $amt)
|
||||||
|
{
|
||||||
|
$invamt = 32 - $amt;
|
||||||
|
$mask = (1 << $invamt) - 1;
|
||||||
|
return (($int << $invamt) & 0xFFFFFFFF) | (($int >> $amt) & $mask);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Right Shift
|
||||||
|
*
|
||||||
|
* @access private
|
||||||
|
* @param int $int
|
||||||
|
* @param int $amt
|
||||||
|
* @see self::_sha256()
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
function _rightShift($int, $amt)
|
||||||
|
{
|
||||||
|
$mask = (1 << (32 - $amt)) - 1;
|
||||||
|
return ($int >> $amt) & $mask;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Not
|
||||||
|
*
|
||||||
|
* @access private
|
||||||
|
* @param int $int
|
||||||
|
* @see self::_sha256()
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
function _not($int)
|
||||||
|
{
|
||||||
|
return ~$int & 0xFFFFFFFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add
|
||||||
|
*
|
||||||
|
* _sha256() adds multiple unsigned 32-bit integers. Since PHP doesn't support unsigned integers and since the
|
||||||
|
* possibility of overflow exists, care has to be taken. BigInteger could be used but this should be faster.
|
||||||
|
*
|
||||||
|
* @return int
|
||||||
|
* @see self::_sha256()
|
||||||
|
* @access private
|
||||||
|
*/
|
||||||
|
function _add()
|
||||||
|
{
|
||||||
|
static $mod;
|
||||||
|
if (!isset($mod)) {
|
||||||
|
$mod = pow(2, 32);
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = 0;
|
||||||
|
$arguments = func_get_args();
|
||||||
|
foreach ($arguments as $argument) {
|
||||||
|
$result+= $argument < 0 ? ($argument & 0x7FFFFFFF) + 0x80000000 : $argument;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((php_uname('m') & "\xDF\xDF\xDF") != 'ARM') {
|
||||||
|
return fmod($result, $mod);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (fmod($result, 0x80000000) & 0x7FFFFFFF) |
|
||||||
|
((fmod(floor($result / 0x80000000), 2) & 1) << 31);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* String Shift
|
||||||
|
*
|
||||||
|
* Inspired by array_shift
|
||||||
|
*
|
||||||
|
* @param string $string
|
||||||
|
* @param int $index
|
||||||
|
* @return string
|
||||||
|
* @access private
|
||||||
|
*/
|
||||||
|
function _string_shift(&$string, $index = 1)
|
||||||
|
{
|
||||||
|
$substr = substr($string, 0, $index);
|
||||||
|
$string = substr($string, $index);
|
||||||
|
return $substr;
|
||||||
|
}
|
||||||
|
}
|
||||||
3265
build/phpseclib/Crypt/RSA.php
Normal file
3265
build/phpseclib/Crypt/RSA.php
Normal file
File diff suppressed because it is too large
Load diff
277
build/phpseclib/Crypt/Random.php
Normal file
277
build/phpseclib/Crypt/Random.php
Normal file
|
|
@ -0,0 +1,277 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Random Number Generator
|
||||||
|
*
|
||||||
|
* PHP version 5
|
||||||
|
*
|
||||||
|
* Here's a short example of how to use this library:
|
||||||
|
* <code>
|
||||||
|
* <?php
|
||||||
|
* include 'vendor/autoload.php';
|
||||||
|
*
|
||||||
|
* echo bin2hex(\phpseclib\Crypt\Random::string(8));
|
||||||
|
* ?>
|
||||||
|
* </code>
|
||||||
|
*
|
||||||
|
* @category Crypt
|
||||||
|
* @package Random
|
||||||
|
* @author Jim Wigginton <terrafrost@php.net>
|
||||||
|
* @copyright 2007 Jim Wigginton
|
||||||
|
* @license http://www.opensource.org/licenses/mit-license.html MIT License
|
||||||
|
* @link http://phpseclib.sourceforge.net
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace phpseclib\Crypt;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pure-PHP Random Number Generator
|
||||||
|
*
|
||||||
|
* @package Random
|
||||||
|
* @author Jim Wigginton <terrafrost@php.net>
|
||||||
|
* @access public
|
||||||
|
*/
|
||||||
|
class Random
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Generate a random string.
|
||||||
|
*
|
||||||
|
* Although microoptimizations are generally discouraged as they impair readability this function is ripe with
|
||||||
|
* microoptimizations because this function has the potential of being called a huge number of times.
|
||||||
|
* eg. for RSA key generation.
|
||||||
|
*
|
||||||
|
* @param int $length
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
static function string($length)
|
||||||
|
{
|
||||||
|
if (!$length) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (version_compare(PHP_VERSION, '7.0.0', '>=')) {
|
||||||
|
try {
|
||||||
|
return \random_bytes($length);
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
// If a sufficient source of randomness is unavailable, random_bytes() will throw an
|
||||||
|
// object that implements the Throwable interface (Exception, TypeError, Error).
|
||||||
|
// We don't actually need to do anything here. The string() method should just continue
|
||||||
|
// as normal. Note, however, that if we don't have a sufficient source of randomness for
|
||||||
|
// random_bytes(), most of the other calls here will fail too, so we'll end up using
|
||||||
|
// the PHP implementation.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') {
|
||||||
|
// method 1. prior to PHP 5.3 this would call rand() on windows hence the function_exists('class_alias') call.
|
||||||
|
// ie. class_alias is a function that was introduced in PHP 5.3
|
||||||
|
if (extension_loaded('mcrypt') && function_exists('class_alias')) {
|
||||||
|
return @mcrypt_create_iv($length);
|
||||||
|
}
|
||||||
|
// method 2. openssl_random_pseudo_bytes was introduced in PHP 5.3.0 but prior to PHP 5.3.4 there was,
|
||||||
|
// to quote <http://php.net/ChangeLog-5.php#5.3.4>, "possible blocking behavior". as of 5.3.4
|
||||||
|
// openssl_random_pseudo_bytes and mcrypt_create_iv do the exact same thing on Windows. ie. they both
|
||||||
|
// call php_win32_get_random_bytes():
|
||||||
|
//
|
||||||
|
// https://github.com/php/php-src/blob/7014a0eb6d1611151a286c0ff4f2238f92c120d6/ext/openssl/openssl.c#L5008
|
||||||
|
// https://github.com/php/php-src/blob/7014a0eb6d1611151a286c0ff4f2238f92c120d6/ext/mcrypt/mcrypt.c#L1392
|
||||||
|
//
|
||||||
|
// php_win32_get_random_bytes() is defined thusly:
|
||||||
|
//
|
||||||
|
// https://github.com/php/php-src/blob/7014a0eb6d1611151a286c0ff4f2238f92c120d6/win32/winutil.c#L80
|
||||||
|
//
|
||||||
|
// we're calling it, all the same, in the off chance that the mcrypt extension is not available
|
||||||
|
if (extension_loaded('openssl') && version_compare(PHP_VERSION, '5.3.4', '>=')) {
|
||||||
|
return openssl_random_pseudo_bytes($length);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// method 1. the fastest
|
||||||
|
if (extension_loaded('openssl')) {
|
||||||
|
return openssl_random_pseudo_bytes($length);
|
||||||
|
}
|
||||||
|
// method 2
|
||||||
|
static $fp = true;
|
||||||
|
if ($fp === true) {
|
||||||
|
// warning's will be output unles the error suppression operator is used. errors such as
|
||||||
|
// "open_basedir restriction in effect", "Permission denied", "No such file or directory", etc.
|
||||||
|
$fp = @fopen('/dev/urandom', 'rb');
|
||||||
|
}
|
||||||
|
if ($fp !== true && $fp !== false) { // surprisingly faster than !is_bool() or is_resource()
|
||||||
|
$temp = fread($fp, $length);
|
||||||
|
if (strlen($temp) == $length) {
|
||||||
|
return $temp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// method 3. pretty much does the same thing as method 2 per the following url:
|
||||||
|
// https://github.com/php/php-src/blob/7014a0eb6d1611151a286c0ff4f2238f92c120d6/ext/mcrypt/mcrypt.c#L1391
|
||||||
|
// surprisingly slower than method 2. maybe that's because mcrypt_create_iv does a bunch of error checking that we're
|
||||||
|
// not doing. regardless, this'll only be called if this PHP script couldn't open /dev/urandom due to open_basedir
|
||||||
|
// restrictions or some such
|
||||||
|
if (extension_loaded('mcrypt')) {
|
||||||
|
return @mcrypt_create_iv($length, MCRYPT_DEV_URANDOM);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// at this point we have no choice but to use a pure-PHP CSPRNG
|
||||||
|
|
||||||
|
// cascade entropy across multiple PHP instances by fixing the session and collecting all
|
||||||
|
// environmental variables, including the previous session data and the current session
|
||||||
|
// data.
|
||||||
|
//
|
||||||
|
// mt_rand seeds itself by looking at the PID and the time, both of which are (relatively)
|
||||||
|
// easy to guess at. linux uses mouse clicks, keyboard timings, etc, as entropy sources, but
|
||||||
|
// PHP isn't low level to be able to use those as sources and on a web server there's not likely
|
||||||
|
// going to be a ton of keyboard or mouse action. web servers do have one thing that we can use
|
||||||
|
// however, a ton of people visiting the website. obviously you don't want to base your seeding
|
||||||
|
// soley on parameters a potential attacker sends but (1) not everything in $_SERVER is controlled
|
||||||
|
// by the user and (2) this isn't just looking at the data sent by the current user - it's based
|
||||||
|
// on the data sent by all users. one user requests the page and a hash of their info is saved.
|
||||||
|
// another user visits the page and the serialization of their data is utilized along with the
|
||||||
|
// server envirnment stuff and a hash of the previous http request data (which itself utilizes
|
||||||
|
// a hash of the session data before that). certainly an attacker should be assumed to have
|
||||||
|
// full control over his own http requests. he, however, is not going to have control over
|
||||||
|
// everyone's http requests.
|
||||||
|
static $crypto = false, $v;
|
||||||
|
if ($crypto === false) {
|
||||||
|
// save old session data
|
||||||
|
$old_session_id = session_id();
|
||||||
|
$old_use_cookies = ini_get('session.use_cookies');
|
||||||
|
$old_session_cache_limiter = session_cache_limiter();
|
||||||
|
$_OLD_SESSION = isset($_SESSION) ? $_SESSION : false;
|
||||||
|
if ($old_session_id != '') {
|
||||||
|
session_write_close();
|
||||||
|
}
|
||||||
|
|
||||||
|
session_id(1);
|
||||||
|
ini_set('session.use_cookies', 0);
|
||||||
|
session_cache_limiter('');
|
||||||
|
session_start();
|
||||||
|
|
||||||
|
$v = $seed = $_SESSION['seed'] = pack('H*', sha1(
|
||||||
|
(isset($_SERVER) ? phpseclib_safe_serialize($_SERVER) : '') .
|
||||||
|
(isset($_POST) ? phpseclib_safe_serialize($_POST) : '') .
|
||||||
|
(isset($_GET) ? phpseclib_safe_serialize($_GET) : '') .
|
||||||
|
(isset($_COOKIE) ? phpseclib_safe_serialize($_COOKIE) : '') .
|
||||||
|
phpseclib_safe_serialize($GLOBALS) .
|
||||||
|
phpseclib_safe_serialize($_SESSION) .
|
||||||
|
phpseclib_safe_serialize($_OLD_SESSION)
|
||||||
|
));
|
||||||
|
if (!isset($_SESSION['count'])) {
|
||||||
|
$_SESSION['count'] = 0;
|
||||||
|
}
|
||||||
|
$_SESSION['count']++;
|
||||||
|
|
||||||
|
session_write_close();
|
||||||
|
|
||||||
|
// restore old session data
|
||||||
|
if ($old_session_id != '') {
|
||||||
|
session_id($old_session_id);
|
||||||
|
session_start();
|
||||||
|
ini_set('session.use_cookies', $old_use_cookies);
|
||||||
|
session_cache_limiter($old_session_cache_limiter);
|
||||||
|
} else {
|
||||||
|
if ($_OLD_SESSION !== false) {
|
||||||
|
$_SESSION = $_OLD_SESSION;
|
||||||
|
unset($_OLD_SESSION);
|
||||||
|
} else {
|
||||||
|
unset($_SESSION);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// in SSH2 a shared secret and an exchange hash are generated through the key exchange process.
|
||||||
|
// the IV client to server is the hash of that "nonce" with the letter A and for the encryption key it's the letter C.
|
||||||
|
// if the hash doesn't produce enough a key or an IV that's long enough concat successive hashes of the
|
||||||
|
// original hash and the current hash. we'll be emulating that. for more info see the following URL:
|
||||||
|
//
|
||||||
|
// http://tools.ietf.org/html/rfc4253#section-7.2
|
||||||
|
//
|
||||||
|
// see the is_string($crypto) part for an example of how to expand the keys
|
||||||
|
$key = pack('H*', sha1($seed . 'A'));
|
||||||
|
$iv = pack('H*', sha1($seed . 'C'));
|
||||||
|
|
||||||
|
// ciphers are used as per the nist.gov link below. also, see this link:
|
||||||
|
//
|
||||||
|
// http://en.wikipedia.org/wiki/Cryptographically_secure_pseudorandom_number_generator#Designs_based_on_cryptographic_primitives
|
||||||
|
switch (true) {
|
||||||
|
case class_exists('\phpseclib\Crypt\AES'):
|
||||||
|
$crypto = new AES(Base::MODE_CTR);
|
||||||
|
break;
|
||||||
|
case class_exists('\phpseclib\Crypt\Twofish'):
|
||||||
|
$crypto = new Twofish(Base::MODE_CTR);
|
||||||
|
break;
|
||||||
|
case class_exists('\phpseclib\Crypt\Blowfish'):
|
||||||
|
$crypto = new Blowfish(Base::MODE_CTR);
|
||||||
|
break;
|
||||||
|
case class_exists('\phpseclib\Crypt\TripleDES'):
|
||||||
|
$crypto = new TripleDES(Base::MODE_CTR);
|
||||||
|
break;
|
||||||
|
case class_exists('\phpseclib\Crypt\DES'):
|
||||||
|
$crypto = new DES(Base::MODE_CTR);
|
||||||
|
break;
|
||||||
|
case class_exists('\phpseclib\Crypt\RC4'):
|
||||||
|
$crypto = new RC4();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
user_error(__CLASS__ . ' requires at least one symmetric cipher be loaded');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$crypto->setKey($key);
|
||||||
|
$crypto->setIV($iv);
|
||||||
|
$crypto->enableContinuousBuffer();
|
||||||
|
}
|
||||||
|
|
||||||
|
//return $crypto->encrypt(str_repeat("\0", $length));
|
||||||
|
|
||||||
|
// the following is based off of ANSI X9.31:
|
||||||
|
//
|
||||||
|
// http://csrc.nist.gov/groups/STM/cavp/documents/rng/931rngext.pdf
|
||||||
|
//
|
||||||
|
// OpenSSL uses that same standard for it's random numbers:
|
||||||
|
//
|
||||||
|
// http://www.opensource.apple.com/source/OpenSSL/OpenSSL-38/openssl/fips-1.0/rand/fips_rand.c
|
||||||
|
// (do a search for "ANS X9.31 A.2.4")
|
||||||
|
$result = '';
|
||||||
|
while (strlen($result) < $length) {
|
||||||
|
$i = $crypto->encrypt(microtime()); // strlen(microtime()) == 21
|
||||||
|
$r = $crypto->encrypt($i ^ $v); // strlen($v) == 20
|
||||||
|
$v = $crypto->encrypt($r ^ $i); // strlen($r) == 20
|
||||||
|
$result.= $r;
|
||||||
|
}
|
||||||
|
return substr($result, 0, $length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!function_exists('phpseclib_safe_serialize')) {
|
||||||
|
/**
|
||||||
|
* Safely serialize variables
|
||||||
|
*
|
||||||
|
* If a class has a private __sleep() method it'll give a fatal error on PHP 5.2 and earlier.
|
||||||
|
* PHP 5.3 will emit a warning.
|
||||||
|
*
|
||||||
|
* @param mixed $arr
|
||||||
|
* @access public
|
||||||
|
*/
|
||||||
|
function phpseclib_safe_serialize(&$arr)
|
||||||
|
{
|
||||||
|
if (is_object($arr)) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
if (!is_array($arr)) {
|
||||||
|
return serialize($arr);
|
||||||
|
}
|
||||||
|
// prevent circular array recursion
|
||||||
|
if (isset($arr['__phpseclib_marker'])) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
$safearr = array();
|
||||||
|
$arr['__phpseclib_marker'] = true;
|
||||||
|
foreach (array_keys($arr) as $key) {
|
||||||
|
// do not recurse on the '__phpseclib_marker' key itself, for smaller memory usage
|
||||||
|
if ($key !== '__phpseclib_marker') {
|
||||||
|
$safearr[$key] = phpseclib_safe_serialize($arr[$key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
unset($arr['__phpseclib_marker']);
|
||||||
|
return serialize($safearr);
|
||||||
|
}
|
||||||
|
}
|
||||||
577
build/phpseclib/File/ANSI.php
Normal file
577
build/phpseclib/File/ANSI.php
Normal file
|
|
@ -0,0 +1,577 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pure-PHP ANSI Decoder
|
||||||
|
*
|
||||||
|
* PHP version 5
|
||||||
|
*
|
||||||
|
* If you call read() in \phpseclib\Net\SSH2 you may get {@link http://en.wikipedia.org/wiki/ANSI_escape_code ANSI escape codes} back.
|
||||||
|
* They'd look like chr(0x1B) . '[00m' or whatever (0x1B = ESC). They tell a
|
||||||
|
* {@link http://en.wikipedia.org/wiki/Terminal_emulator terminal emulator} how to format the characters, what
|
||||||
|
* color to display them in, etc. \phpseclib\File\ANSI is a {@link http://en.wikipedia.org/wiki/VT100 VT100} terminal emulator.
|
||||||
|
*
|
||||||
|
* @category File
|
||||||
|
* @package ANSI
|
||||||
|
* @author Jim Wigginton <terrafrost@php.net>
|
||||||
|
* @copyright 2012 Jim Wigginton
|
||||||
|
* @license http://www.opensource.org/licenses/mit-license.html MIT License
|
||||||
|
* @link http://phpseclib.sourceforge.net
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace phpseclib\File;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pure-PHP ANSI Decoder
|
||||||
|
*
|
||||||
|
* @package ANSI
|
||||||
|
* @author Jim Wigginton <terrafrost@php.net>
|
||||||
|
* @access public
|
||||||
|
*/
|
||||||
|
class ANSI
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Max Width
|
||||||
|
*
|
||||||
|
* @var int
|
||||||
|
* @access private
|
||||||
|
*/
|
||||||
|
var $max_x;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Max Height
|
||||||
|
*
|
||||||
|
* @var int
|
||||||
|
* @access private
|
||||||
|
*/
|
||||||
|
var $max_y;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Max History
|
||||||
|
*
|
||||||
|
* @var int
|
||||||
|
* @access private
|
||||||
|
*/
|
||||||
|
var $max_history;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* History
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
* @access private
|
||||||
|
*/
|
||||||
|
var $history;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* History Attributes
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
* @access private
|
||||||
|
*/
|
||||||
|
var $history_attrs;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Current Column
|
||||||
|
*
|
||||||
|
* @var int
|
||||||
|
* @access private
|
||||||
|
*/
|
||||||
|
var $x;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Current Row
|
||||||
|
*
|
||||||
|
* @var int
|
||||||
|
* @access private
|
||||||
|
*/
|
||||||
|
var $y;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Old Column
|
||||||
|
*
|
||||||
|
* @var int
|
||||||
|
* @access private
|
||||||
|
*/
|
||||||
|
var $old_x;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Old Row
|
||||||
|
*
|
||||||
|
* @var int
|
||||||
|
* @access private
|
||||||
|
*/
|
||||||
|
var $old_y;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An empty attribute cell
|
||||||
|
*
|
||||||
|
* @var object
|
||||||
|
* @access private
|
||||||
|
*/
|
||||||
|
var $base_attr_cell;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The current attribute cell
|
||||||
|
*
|
||||||
|
* @var object
|
||||||
|
* @access private
|
||||||
|
*/
|
||||||
|
var $attr_cell;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An empty attribute row
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
* @access private
|
||||||
|
*/
|
||||||
|
var $attr_row;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The current screen text
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
* @access private
|
||||||
|
*/
|
||||||
|
var $screen;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The current screen attributes
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
* @access private
|
||||||
|
*/
|
||||||
|
var $attrs;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Current ANSI code
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
* @access private
|
||||||
|
*/
|
||||||
|
var $ansi;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tokenization
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
* @access private
|
||||||
|
*/
|
||||||
|
var $tokenization;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default Constructor.
|
||||||
|
*
|
||||||
|
* @return \phpseclib\File\ANSI
|
||||||
|
* @access public
|
||||||
|
*/
|
||||||
|
function __construct()
|
||||||
|
{
|
||||||
|
$attr_cell = new \stdClass();
|
||||||
|
$attr_cell->bold = false;
|
||||||
|
$attr_cell->underline = false;
|
||||||
|
$attr_cell->blink = false;
|
||||||
|
$attr_cell->background = 'black';
|
||||||
|
$attr_cell->foreground = 'white';
|
||||||
|
$attr_cell->reverse = false;
|
||||||
|
$this->base_attr_cell = clone $attr_cell;
|
||||||
|
$this->attr_cell = clone $attr_cell;
|
||||||
|
|
||||||
|
$this->setHistory(200);
|
||||||
|
$this->setDimensions(80, 24);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set terminal width and height
|
||||||
|
*
|
||||||
|
* Resets the screen as well
|
||||||
|
*
|
||||||
|
* @param int $x
|
||||||
|
* @param int $y
|
||||||
|
* @access public
|
||||||
|
*/
|
||||||
|
function setDimensions($x, $y)
|
||||||
|
{
|
||||||
|
$this->max_x = $x - 1;
|
||||||
|
$this->max_y = $y - 1;
|
||||||
|
$this->x = $this->y = 0;
|
||||||
|
$this->history = $this->history_attrs = array();
|
||||||
|
$this->attr_row = array_fill(0, $this->max_x + 2, $this->base_attr_cell);
|
||||||
|
$this->screen = array_fill(0, $this->max_y + 1, '');
|
||||||
|
$this->attrs = array_fill(0, $this->max_y + 1, $this->attr_row);
|
||||||
|
$this->ansi = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the number of lines that should be logged past the terminal height
|
||||||
|
*
|
||||||
|
* @param int $history
|
||||||
|
* @access public
|
||||||
|
*/
|
||||||
|
function setHistory($history)
|
||||||
|
{
|
||||||
|
$this->max_history = $history;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load a string
|
||||||
|
*
|
||||||
|
* @param string $source
|
||||||
|
* @access public
|
||||||
|
*/
|
||||||
|
function loadString($source)
|
||||||
|
{
|
||||||
|
$this->setDimensions($this->max_x + 1, $this->max_y + 1);
|
||||||
|
$this->appendString($source);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Appdend a string
|
||||||
|
*
|
||||||
|
* @param string $source
|
||||||
|
* @access public
|
||||||
|
*/
|
||||||
|
function appendString($source)
|
||||||
|
{
|
||||||
|
$this->tokenization = array('');
|
||||||
|
for ($i = 0; $i < strlen($source); $i++) {
|
||||||
|
if (strlen($this->ansi)) {
|
||||||
|
$this->ansi.= $source[$i];
|
||||||
|
$chr = ord($source[$i]);
|
||||||
|
// http://en.wikipedia.org/wiki/ANSI_escape_code#Sequence_elements
|
||||||
|
// single character CSI's not currently supported
|
||||||
|
switch (true) {
|
||||||
|
case $this->ansi == "\x1B=":
|
||||||
|
$this->ansi = '';
|
||||||
|
continue 2;
|
||||||
|
case strlen($this->ansi) == 2 && $chr >= 64 && $chr <= 95 && $chr != ord('['):
|
||||||
|
case strlen($this->ansi) > 2 && $chr >= 64 && $chr <= 126:
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
continue 2;
|
||||||
|
}
|
||||||
|
$this->tokenization[] = $this->ansi;
|
||||||
|
$this->tokenization[] = '';
|
||||||
|
// http://ascii-table.com/ansi-escape-sequences-vt-100.php
|
||||||
|
switch ($this->ansi) {
|
||||||
|
case "\x1B[H": // Move cursor to upper left corner
|
||||||
|
$this->old_x = $this->x;
|
||||||
|
$this->old_y = $this->y;
|
||||||
|
$this->x = $this->y = 0;
|
||||||
|
break;
|
||||||
|
case "\x1B[J": // Clear screen from cursor down
|
||||||
|
$this->history = array_merge($this->history, array_slice(array_splice($this->screen, $this->y + 1), 0, $this->old_y));
|
||||||
|
$this->screen = array_merge($this->screen, array_fill($this->y, $this->max_y, ''));
|
||||||
|
|
||||||
|
$this->history_attrs = array_merge($this->history_attrs, array_slice(array_splice($this->attrs, $this->y + 1), 0, $this->old_y));
|
||||||
|
$this->attrs = array_merge($this->attrs, array_fill($this->y, $this->max_y, $this->attr_row));
|
||||||
|
|
||||||
|
if (count($this->history) == $this->max_history) {
|
||||||
|
array_shift($this->history);
|
||||||
|
array_shift($this->history_attrs);
|
||||||
|
}
|
||||||
|
case "\x1B[K": // Clear screen from cursor right
|
||||||
|
$this->screen[$this->y] = substr($this->screen[$this->y], 0, $this->x);
|
||||||
|
|
||||||
|
array_splice($this->attrs[$this->y], $this->x + 1, $this->max_x - $this->x, array_fill($this->x, $this->max_x - ($this->x - 1), $this->base_attr_cell));
|
||||||
|
break;
|
||||||
|
case "\x1B[2K": // Clear entire line
|
||||||
|
$this->screen[$this->y] = str_repeat(' ', $this->x);
|
||||||
|
$this->attrs[$this->y] = $this->attr_row;
|
||||||
|
break;
|
||||||
|
case "\x1B[?1h": // set cursor key to application
|
||||||
|
case "\x1B[?25h": // show the cursor
|
||||||
|
case "\x1B(B": // set united states g0 character set
|
||||||
|
break;
|
||||||
|
case "\x1BE": // Move to next line
|
||||||
|
$this->_newLine();
|
||||||
|
$this->x = 0;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
switch (true) {
|
||||||
|
case preg_match('#\x1B\[(\d+)B#', $this->ansi, $match): // Move cursor down n lines
|
||||||
|
$this->old_y = $this->y;
|
||||||
|
$this->y+= $match[1];
|
||||||
|
break;
|
||||||
|
case preg_match('#\x1B\[(\d+);(\d+)H#', $this->ansi, $match): // Move cursor to screen location v,h
|
||||||
|
$this->old_x = $this->x;
|
||||||
|
$this->old_y = $this->y;
|
||||||
|
$this->x = $match[2] - 1;
|
||||||
|
$this->y = $match[1] - 1;
|
||||||
|
break;
|
||||||
|
case preg_match('#\x1B\[(\d+)C#', $this->ansi, $match): // Move cursor right n lines
|
||||||
|
$this->old_x = $this->x;
|
||||||
|
$this->x+= $match[1];
|
||||||
|
break;
|
||||||
|
case preg_match('#\x1B\[(\d+)D#', $this->ansi, $match): // Move cursor left n lines
|
||||||
|
$this->old_x = $this->x;
|
||||||
|
$this->x-= $match[1];
|
||||||
|
if ($this->x < 0) {
|
||||||
|
$this->x = 0;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case preg_match('#\x1B\[(\d+);(\d+)r#', $this->ansi, $match): // Set top and bottom lines of a window
|
||||||
|
break;
|
||||||
|
case preg_match('#\x1B\[(\d*(?:;\d*)*)m#', $this->ansi, $match): // character attributes
|
||||||
|
$attr_cell = &$this->attr_cell;
|
||||||
|
$mods = explode(';', $match[1]);
|
||||||
|
foreach ($mods as $mod) {
|
||||||
|
switch ($mod) {
|
||||||
|
case '':
|
||||||
|
case '0': // Turn off character attributes
|
||||||
|
$attr_cell = clone $this->base_attr_cell;
|
||||||
|
break;
|
||||||
|
case '1': // Turn bold mode on
|
||||||
|
$attr_cell->bold = true;
|
||||||
|
break;
|
||||||
|
case '4': // Turn underline mode on
|
||||||
|
$attr_cell->underline = true;
|
||||||
|
break;
|
||||||
|
case '5': // Turn blinking mode on
|
||||||
|
$attr_cell->blink = true;
|
||||||
|
break;
|
||||||
|
case '7': // Turn reverse video on
|
||||||
|
$attr_cell->reverse = !$attr_cell->reverse;
|
||||||
|
$temp = $attr_cell->background;
|
||||||
|
$attr_cell->background = $attr_cell->foreground;
|
||||||
|
$attr_cell->foreground = $temp;
|
||||||
|
break;
|
||||||
|
default: // set colors
|
||||||
|
//$front = $attr_cell->reverse ? &$attr_cell->background : &$attr_cell->foreground;
|
||||||
|
$front = &$attr_cell->{ $attr_cell->reverse ? 'background' : 'foreground' };
|
||||||
|
//$back = $attr_cell->reverse ? &$attr_cell->foreground : &$attr_cell->background;
|
||||||
|
$back = &$attr_cell->{ $attr_cell->reverse ? 'foreground' : 'background' };
|
||||||
|
switch ($mod) {
|
||||||
|
// @codingStandardsIgnoreStart
|
||||||
|
case '30': $front = 'black'; break;
|
||||||
|
case '31': $front = 'red'; break;
|
||||||
|
case '32': $front = 'green'; break;
|
||||||
|
case '33': $front = 'yellow'; break;
|
||||||
|
case '34': $front = 'blue'; break;
|
||||||
|
case '35': $front = 'magenta'; break;
|
||||||
|
case '36': $front = 'cyan'; break;
|
||||||
|
case '37': $front = 'white'; break;
|
||||||
|
|
||||||
|
case '40': $back = 'black'; break;
|
||||||
|
case '41': $back = 'red'; break;
|
||||||
|
case '42': $back = 'green'; break;
|
||||||
|
case '43': $back = 'yellow'; break;
|
||||||
|
case '44': $back = 'blue'; break;
|
||||||
|
case '45': $back = 'magenta'; break;
|
||||||
|
case '46': $back = 'cyan'; break;
|
||||||
|
case '47': $back = 'white'; break;
|
||||||
|
// @codingStandardsIgnoreEnd
|
||||||
|
|
||||||
|
default:
|
||||||
|
//user_error('Unsupported attribute: ' . $mod);
|
||||||
|
$this->ansi = '';
|
||||||
|
break 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
//user_error("{$this->ansi} is unsupported\r\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$this->ansi = '';
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->tokenization[count($this->tokenization) - 1].= $source[$i];
|
||||||
|
switch ($source[$i]) {
|
||||||
|
case "\r":
|
||||||
|
$this->x = 0;
|
||||||
|
break;
|
||||||
|
case "\n":
|
||||||
|
$this->_newLine();
|
||||||
|
break;
|
||||||
|
case "\x08": // backspace
|
||||||
|
if ($this->x) {
|
||||||
|
$this->x--;
|
||||||
|
$this->attrs[$this->y][$this->x] = clone $this->base_attr_cell;
|
||||||
|
$this->screen[$this->y] = substr_replace(
|
||||||
|
$this->screen[$this->y],
|
||||||
|
$source[$i],
|
||||||
|
$this->x,
|
||||||
|
1
|
||||||
|
);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "\x0F": // shift
|
||||||
|
break;
|
||||||
|
case "\x1B": // start ANSI escape code
|
||||||
|
$this->tokenization[count($this->tokenization) - 1] = substr($this->tokenization[count($this->tokenization) - 1], 0, -1);
|
||||||
|
//if (!strlen($this->tokenization[count($this->tokenization) - 1])) {
|
||||||
|
// array_pop($this->tokenization);
|
||||||
|
//}
|
||||||
|
$this->ansi.= "\x1B";
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
$this->attrs[$this->y][$this->x] = clone $this->attr_cell;
|
||||||
|
if ($this->x > strlen($this->screen[$this->y])) {
|
||||||
|
$this->screen[$this->y] = str_repeat(' ', $this->x);
|
||||||
|
}
|
||||||
|
$this->screen[$this->y] = substr_replace(
|
||||||
|
$this->screen[$this->y],
|
||||||
|
$source[$i],
|
||||||
|
$this->x,
|
||||||
|
1
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($this->x > $this->max_x) {
|
||||||
|
$this->x = 0;
|
||||||
|
$this->_newLine();
|
||||||
|
} else {
|
||||||
|
$this->x++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a new line
|
||||||
|
*
|
||||||
|
* Also update the $this->screen and $this->history buffers
|
||||||
|
*
|
||||||
|
* @access private
|
||||||
|
*/
|
||||||
|
function _newLine()
|
||||||
|
{
|
||||||
|
//if ($this->y < $this->max_y) {
|
||||||
|
// $this->y++;
|
||||||
|
//}
|
||||||
|
|
||||||
|
while ($this->y >= $this->max_y) {
|
||||||
|
$this->history = array_merge($this->history, array(array_shift($this->screen)));
|
||||||
|
$this->screen[] = '';
|
||||||
|
|
||||||
|
$this->history_attrs = array_merge($this->history_attrs, array(array_shift($this->attrs)));
|
||||||
|
$this->attrs[] = $this->attr_row;
|
||||||
|
|
||||||
|
if (count($this->history) >= $this->max_history) {
|
||||||
|
array_shift($this->history);
|
||||||
|
array_shift($this->history_attrs);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->y--;
|
||||||
|
}
|
||||||
|
$this->y++;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the current coordinate without preformating
|
||||||
|
*
|
||||||
|
* @access private
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
function _processCoordinate($last_attr, $cur_attr, $char)
|
||||||
|
{
|
||||||
|
$output = '';
|
||||||
|
|
||||||
|
if ($last_attr != $cur_attr) {
|
||||||
|
$close = $open = '';
|
||||||
|
if ($last_attr->foreground != $cur_attr->foreground) {
|
||||||
|
if ($cur_attr->foreground != 'white') {
|
||||||
|
$open.= '<span style="color: ' . $cur_attr->foreground . '">';
|
||||||
|
}
|
||||||
|
if ($last_attr->foreground != 'white') {
|
||||||
|
$close = '</span>' . $close;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($last_attr->background != $cur_attr->background) {
|
||||||
|
if ($cur_attr->background != 'black') {
|
||||||
|
$open.= '<span style="background: ' . $cur_attr->background . '">';
|
||||||
|
}
|
||||||
|
if ($last_attr->background != 'black') {
|
||||||
|
$close = '</span>' . $close;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($last_attr->bold != $cur_attr->bold) {
|
||||||
|
if ($cur_attr->bold) {
|
||||||
|
$open.= '<b>';
|
||||||
|
} else {
|
||||||
|
$close = '</b>' . $close;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($last_attr->underline != $cur_attr->underline) {
|
||||||
|
if ($cur_attr->underline) {
|
||||||
|
$open.= '<u>';
|
||||||
|
} else {
|
||||||
|
$close = '</u>' . $close;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($last_attr->blink != $cur_attr->blink) {
|
||||||
|
if ($cur_attr->blink) {
|
||||||
|
$open.= '<blink>';
|
||||||
|
} else {
|
||||||
|
$close = '</blink>' . $close;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$output.= $close . $open;
|
||||||
|
}
|
||||||
|
|
||||||
|
$output.= htmlspecialchars($char);
|
||||||
|
|
||||||
|
return $output;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the current screen without preformating
|
||||||
|
*
|
||||||
|
* @access private
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
function _getScreen()
|
||||||
|
{
|
||||||
|
$output = '';
|
||||||
|
$last_attr = $this->base_attr_cell;
|
||||||
|
for ($i = 0; $i <= $this->max_y; $i++) {
|
||||||
|
for ($j = 0; $j <= $this->max_x; $j++) {
|
||||||
|
$cur_attr = $this->attrs[$i][$j];
|
||||||
|
$output.= $this->_processCoordinate($last_attr, $cur_attr, isset($this->screen[$i][$j]) ? $this->screen[$i][$j] : '');
|
||||||
|
$last_attr = $this->attrs[$i][$j];
|
||||||
|
}
|
||||||
|
$output.= "\r\n";
|
||||||
|
}
|
||||||
|
$output = substr($output, 0, -2);
|
||||||
|
// close any remaining open tags
|
||||||
|
$output.= $this->_processCoordinate($last_attr, $this->base_attr_cell, '');
|
||||||
|
return rtrim($output);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the current screen
|
||||||
|
*
|
||||||
|
* @access public
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
function getScreen()
|
||||||
|
{
|
||||||
|
return '<pre width="' . ($this->max_x + 1) . '" style="color: white; background: black">' . $this->_getScreen() . '</pre>';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the current screen and the x previous lines
|
||||||
|
*
|
||||||
|
* @access public
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
function getHistory()
|
||||||
|
{
|
||||||
|
$scrollback = '';
|
||||||
|
$last_attr = $this->base_attr_cell;
|
||||||
|
for ($i = 0; $i < count($this->history); $i++) {
|
||||||
|
for ($j = 0; $j <= $this->max_x + 1; $j++) {
|
||||||
|
$cur_attr = $this->history_attrs[$i][$j];
|
||||||
|
$scrollback.= $this->_processCoordinate($last_attr, $cur_attr, isset($this->history[$i][$j]) ? $this->history[$i][$j] : '');
|
||||||
|
$last_attr = $this->history_attrs[$i][$j];
|
||||||
|
}
|
||||||
|
$scrollback.= "\r\n";
|
||||||
|
}
|
||||||
|
$base_attr_cell = $this->base_attr_cell;
|
||||||
|
$this->base_attr_cell = $last_attr;
|
||||||
|
$scrollback.= $this->_getScreen();
|
||||||
|
$this->base_attr_cell = $base_attr_cell;
|
||||||
|
|
||||||
|
return '<pre width="' . ($this->max_x + 1) . '" style="color: white; background: black">' . $scrollback . '</span></pre>';
|
||||||
|
}
|
||||||
|
}
|
||||||
1450
build/phpseclib/File/ASN1.php
Normal file
1450
build/phpseclib/File/ASN1.php
Normal file
File diff suppressed because it is too large
Load diff
47
build/phpseclib/File/ASN1/Element.php
Normal file
47
build/phpseclib/File/ASN1/Element.php
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Pure-PHP ASN.1 Parser
|
||||||
|
*
|
||||||
|
* PHP version 5
|
||||||
|
*
|
||||||
|
* @category File
|
||||||
|
* @package ASN1
|
||||||
|
* @author Jim Wigginton <terrafrost@php.net>
|
||||||
|
* @copyright 2012 Jim Wigginton
|
||||||
|
* @license http://www.opensource.org/licenses/mit-license.html MIT License
|
||||||
|
* @link http://phpseclib.sourceforge.net
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace phpseclib\File\ASN1;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ASN.1 Element
|
||||||
|
*
|
||||||
|
* Bypass normal encoding rules in phpseclib\File\ASN1::encodeDER()
|
||||||
|
*
|
||||||
|
* @package ASN1
|
||||||
|
* @author Jim Wigginton <terrafrost@php.net>
|
||||||
|
* @access public
|
||||||
|
*/
|
||||||
|
class Element
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Raw element value
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
* @access private
|
||||||
|
*/
|
||||||
|
var $element;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor
|
||||||
|
*
|
||||||
|
* @param string $encoded
|
||||||
|
* @return \phpseclib\File\ASN1\Element
|
||||||
|
* @access public
|
||||||
|
*/
|
||||||
|
function __construct($encoded)
|
||||||
|
{
|
||||||
|
$this->element = $encoded;
|
||||||
|
}
|
||||||
|
}
|
||||||
5097
build/phpseclib/File/X509.php
Normal file
5097
build/phpseclib/File/X509.php
Normal file
File diff suppressed because it is too large
Load diff
3787
build/phpseclib/Math/BigInteger.php
Normal file
3787
build/phpseclib/Math/BigInteger.php
Normal file
File diff suppressed because it is too large
Load diff
6
build/phpseclib/openssl.cnf
Normal file
6
build/phpseclib/openssl.cnf
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
# minimalist openssl.cnf file for use with phpseclib
|
||||||
|
|
||||||
|
HOME = .
|
||||||
|
RANDFILE = $ENV::HOME/.rnd
|
||||||
|
|
||||||
|
[ v3_ca ]
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
<?php
|
<?php
|
||||||
define('ROOT_DIR', dirname(__DIR__));
|
|
||||||
define('PLUGINS_DEST_DIR', __DIR__ . '/dist/releases/plugins');
|
define('PLUGINS_DEST_DIR', __DIR__ . '/dist/releases/plugins');
|
||||||
|
|
||||||
is_dir(PLUGINS_DEST_DIR) || mkdir(PLUGINS_DEST_DIR, 0777, true);
|
is_dir(PLUGINS_DEST_DIR) || mkdir(PLUGINS_DEST_DIR, 0777, true);
|
||||||
|
|
@ -11,6 +10,8 @@ foreach ($files as $fileinfo) {
|
||||||
$fileinfo->isDir() || unlink($fileinfo->getRealPath());
|
$fileinfo->isDir() || unlink($fileinfo->getRealPath());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$terser = ROOT_DIR . '/node_modules/terser/bin/terser';
|
||||||
|
|
||||||
$manifest = [];
|
$manifest = [];
|
||||||
require ROOT_DIR . '/snappymail/v/0.0.0/app/libraries/RainLoop/Plugins/AbstractPlugin.php';
|
require ROOT_DIR . '/snappymail/v/0.0.0/app/libraries/RainLoop/Plugins/AbstractPlugin.php';
|
||||||
$keys = [
|
$keys = [
|
||||||
|
|
@ -42,6 +43,21 @@ foreach (glob(ROOT_DIR . '/plugins/*', GLOB_NOSORT | GLOB_ONLYDIR) as $dir) {
|
||||||
$version = $manifest_item['version'];
|
$version = $manifest_item['version'];
|
||||||
if (0 < floatval($version)) {
|
if (0 < floatval($version)) {
|
||||||
echo "+ {$name} {$version}\n";
|
echo "+ {$name} {$version}\n";
|
||||||
|
|
||||||
|
// Minify JavaScript
|
||||||
|
foreach (glob("{$dir}/*.js") as $file) {
|
||||||
|
if (!strpos($file,'.min')) {
|
||||||
|
$mfile = str_replace('.js', '.min.js', $file);
|
||||||
|
passthru("{$terser} {$file} --output {$mfile} --compress 'drop_console' --ecma 6 --mangle");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
foreach (glob("{$dir}/js/*.js") as $file) {
|
||||||
|
if (!strpos($file,'.min')) {
|
||||||
|
$mfile = str_replace('.js', '.min.js', $file);
|
||||||
|
passthru("{$terser} {$file} --output {$mfile} --compress 'drop_console' --ecma 6 --mangle");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$manifest_item['type'] = 'plugin';
|
$manifest_item['type'] = 'plugin';
|
||||||
$manifest_item['id'] = $name;
|
$manifest_item['id'] = $name;
|
||||||
$manifest_item['file'] = "plugins/{$name}-{$version}.tgz";
|
$manifest_item['file'] = "plugins/{$name}-{$version}.tgz";
|
||||||
|
|
@ -54,6 +70,7 @@ foreach (glob(ROOT_DIR . '/plugins/*', GLOB_NOSORT | GLOB_ONLYDIR) as $dir) {
|
||||||
$tar->compress(Phar::GZ);
|
$tar->compress(Phar::GZ);
|
||||||
unlink($tar_destination);
|
unlink($tar_destination);
|
||||||
rename("{$tar_destination}.gz", $tgz_destination);
|
rename("{$tar_destination}.gz", $tgz_destination);
|
||||||
|
/*
|
||||||
if (Phar::canWrite()) {
|
if (Phar::canWrite()) {
|
||||||
$phar_destination = PLUGINS_DEST_DIR . "/{$name}.phar";
|
$phar_destination = PLUGINS_DEST_DIR . "/{$name}.phar";
|
||||||
@unlink($phar_destination);
|
@unlink($phar_destination);
|
||||||
|
|
@ -63,6 +80,7 @@ foreach (glob(ROOT_DIR . '/plugins/*', GLOB_NOSORT | GLOB_ONLYDIR) as $dir) {
|
||||||
unlink($phar_destination);
|
unlink($phar_destination);
|
||||||
rename("{$phar_destination}.gz", $phar_destination);
|
rename("{$phar_destination}.gz", $phar_destination);
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
if (isset($options['sign'])) {
|
if (isset($options['sign'])) {
|
||||||
passthru('gpg --local-user 1016E47079145542F8BA133548208BA13290F3EB --armor --detach-sign '.escapeshellarg($tgz_destination), $return_var);
|
passthru('gpg --local-user 1016E47079145542F8BA133548208BA13290F3EB --armor --detach-sign '.escapeshellarg($tgz_destination), $return_var);
|
||||||
$manifest_item['pgp_sig'] = trim(preg_replace('/-----(BEGIN|END) PGP SIGNATURE-----/', '', file_get_contents($tgz_destination.'.asc')));
|
$manifest_item['pgp_sig'] = trim(preg_replace('/-----(BEGIN|END) PGP SIGNATURE-----/', '', file_get_contents($tgz_destination.'.asc')));
|
||||||
|
|
@ -85,6 +103,6 @@ $manifest = str_replace('"}', "\"\n\t}", $manifest);
|
||||||
$manifest = str_replace('}]', "}\n]", $manifest);
|
$manifest = str_replace('}]', "}\n]", $manifest);
|
||||||
$manifest = str_replace('","', "\",\n\t\t\"", $manifest);
|
$manifest = str_replace('","', "\",\n\t\t\"", $manifest);
|
||||||
$manifest = str_replace('\/', '/', $manifest);
|
$manifest = str_replace('\/', '/', $manifest);
|
||||||
file_put_contents(PLUGINS_DEST_DIR . "/packages.json", $manifest);
|
file_put_contents(dirname(PLUGINS_DEST_DIR) . "/packages.json", $manifest);
|
||||||
|
|
||||||
exit;
|
exit;
|
||||||
|
|
|
||||||
283
cli/release.php
Executable file
283
cli/release.php
Executable file
|
|
@ -0,0 +1,283 @@
|
||||||
|
#!/usr/bin/php
|
||||||
|
<?php
|
||||||
|
define('ROOT_DIR', dirname(__DIR__));
|
||||||
|
chdir(ROOT_DIR);
|
||||||
|
|
||||||
|
$options = getopt('', ['aur','docker','plugins','skip-gulp','debian','nextcloud','owncloud','cpanel','sign']);
|
||||||
|
|
||||||
|
if (isset($options['plugins'])) {
|
||||||
|
require(ROOT_DIR . '/build/plugins.php');
|
||||||
|
}
|
||||||
|
|
||||||
|
$gulp = trim(`which gulp`);
|
||||||
|
if (!$gulp) {
|
||||||
|
exit('gulp not installed, run as root: npm install --global gulp-cli');
|
||||||
|
}
|
||||||
|
|
||||||
|
$package = json_decode(file_get_contents('package.json'));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update files that contain version
|
||||||
|
*/
|
||||||
|
// cloudron
|
||||||
|
$file = ROOT_DIR . '/integrations/cloudron/Dockerfile';
|
||||||
|
file_put_contents($file, preg_replace('/VERSION=[0-9.]+/', "VERSION={$package->version}", file_get_contents($file)));
|
||||||
|
$file = ROOT_DIR . '/integrations/cloudron/DESCRIPTION.md';
|
||||||
|
file_put_contents($file, preg_replace('/<upstream>[^<]*</', "<upstream>{$package->version}<", file_get_contents($file)));
|
||||||
|
// docker
|
||||||
|
$file = ROOT_DIR . '/.docker/release/files/usr/local/include/application.ini';
|
||||||
|
file_put_contents($file, preg_replace('/current = "[0-9.]+"/', "current = \"{$package->version}\"", file_get_contents($file)));
|
||||||
|
// virtualmin
|
||||||
|
$file = ROOT_DIR . '/integrations/virtualmin/snappymail.pl';
|
||||||
|
file_put_contents($file, preg_replace('/return \\( "[0-9]+\\.[0-9]+\\.[0-9]+" \\)/', "return ( \"{$package->version}\" )", file_get_contents($file)));
|
||||||
|
|
||||||
|
// Arch User Repository
|
||||||
|
// https://aur.archlinux.org/packages/snappymail/
|
||||||
|
$options['aur'] = isset($options['aur']);
|
||||||
|
|
||||||
|
// Docker build
|
||||||
|
$options['docker'] = isset($options['docker']);
|
||||||
|
if ($options['docker'] && !($docker = trim(`which docker`))) {
|
||||||
|
exit('docker not found');
|
||||||
|
}
|
||||||
|
if ($options['docker'] && $options['aur']) {
|
||||||
|
exit('Conflict between docker and aur');
|
||||||
|
}
|
||||||
|
|
||||||
|
$destPath = "build/dist/releases/webmail/{$package->version}/";
|
||||||
|
is_dir($destPath) || mkdir($destPath, 0777, true);
|
||||||
|
|
||||||
|
$zip_destination = "{$destPath}snappymail-{$package->version}.zip";
|
||||||
|
$tar_destination = "{$destPath}snappymail-{$package->version}.tar";
|
||||||
|
|
||||||
|
@unlink($zip_destination);
|
||||||
|
@unlink($tar_destination);
|
||||||
|
@unlink("{$tar_destination}.gz");
|
||||||
|
|
||||||
|
if (!isset($options['skip-gulp'])) {
|
||||||
|
echo "\x1b[33;1m === Gulp === \x1b[0m\n";
|
||||||
|
passthru($gulp, $return_var);
|
||||||
|
if ($return_var) {
|
||||||
|
exit("gulp failed with error code {$return_var}\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
$cmddir = escapeshellcmd(ROOT_DIR) . '/snappymail/v/0.0.0/static';
|
||||||
|
|
||||||
|
if ($gzip = trim(`which gzip`)) {
|
||||||
|
echo "\x1b[33;1m === Gzip *.js and *.css === \x1b[0m\n";
|
||||||
|
passthru("{$gzip} -k --best {$cmddir}/js/*.js");
|
||||||
|
passthru("{$gzip} -k --best {$cmddir}/js/min/*.js");
|
||||||
|
passthru("{$gzip} -k --best {$cmddir}/css/admin*.css");
|
||||||
|
passthru("{$gzip} -k --best {$cmddir}/css/app*.css");
|
||||||
|
unlink(ROOT_DIR . '/snappymail/v/0.0.0/static/js/boot.js.gz');
|
||||||
|
unlink(ROOT_DIR . '/snappymail/v/0.0.0/static/js/min/boot.min.js.gz');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($brotli = trim(`which brotli`)) {
|
||||||
|
echo "\x1b[33;1m === Brotli *.js and *.css === \x1b[0m\n";
|
||||||
|
passthru("{$brotli} -k --best {$cmddir}/js/*.js");
|
||||||
|
passthru("{$brotli} -k --best {$cmddir}/js/min/*.js");
|
||||||
|
passthru("{$brotli} -k --best {$cmddir}/css/admin*.css");
|
||||||
|
passthru("{$brotli} -k --best {$cmddir}/css/app*.css");
|
||||||
|
unlink(ROOT_DIR . '/snappymail/v/0.0.0/static/js/boot.js.br');
|
||||||
|
unlink(ROOT_DIR . '/snappymail/v/0.0.0/static/js/min/boot.min.js.br');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Temporary rename folder to speed up PharData
|
||||||
|
//if (!rename('snappymail/v/0.0.0', "snappymail/v/{$package->version}")){
|
||||||
|
if (!rename('snappymail/v/0.0.0', "snappymail/v/{$package->version}")) {
|
||||||
|
exit('Failed to temporary rename snappymail/v/0.0.0');
|
||||||
|
}
|
||||||
|
register_shutdown_function(function(){
|
||||||
|
// Rename folder back to original
|
||||||
|
@rename("snappymail/v/{$GLOBALS['package']->version}", 'snappymail/v/0.0.0');
|
||||||
|
});
|
||||||
|
|
||||||
|
echo "\x1b[33;1m === Zip/Tar === \x1b[0m\n";
|
||||||
|
|
||||||
|
$zip = new ZipArchive();
|
||||||
|
if (!$zip->open($zip_destination, ZIPARCHIVE::CREATE)) {
|
||||||
|
exit("Failed to create {$zip_destination}");
|
||||||
|
}
|
||||||
|
|
||||||
|
$tar = new PharData($tar_destination);
|
||||||
|
|
||||||
|
$files = new RecursiveIteratorIterator(new RecursiveDirectoryIterator('snappymail/v'), RecursiveIteratorIterator::SELF_FIRST);
|
||||||
|
foreach ($files as $file) {
|
||||||
|
$file = str_replace('\\', '/', $file);
|
||||||
|
//echo "{$file}\n";
|
||||||
|
// Ignore "." and ".." folders
|
||||||
|
if (!in_array(substr($file, strrpos($file, '/')+1), array('.', '..'))) {
|
||||||
|
if (is_dir($file)) {
|
||||||
|
$zip->addEmptyDir($file);
|
||||||
|
} else if (is_file($file)) {
|
||||||
|
$zip->addFile($file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($options['docker']) {
|
||||||
|
$tar->buildFromDirectory('./snappymail/', "@v/{$package->version}@");
|
||||||
|
} else {
|
||||||
|
$tar->buildFromDirectory('./', "@snappymail/v/{$package->version}@");
|
||||||
|
}
|
||||||
|
|
||||||
|
//$zip->addFile('cli/upgrade.sh');
|
||||||
|
//$tar->addFile('cli/upgrade.sh');
|
||||||
|
|
||||||
|
$zip->addFile('data/.htaccess');
|
||||||
|
$tar->addFile('data/.htaccess');
|
||||||
|
|
||||||
|
$zip->addFromString('data/VERSION', $package->version);
|
||||||
|
$tar->addFromString('data/VERSION', $package->version);
|
||||||
|
|
||||||
|
$zip->addFile('data/README.md');
|
||||||
|
$tar->addFile('data/README.md');
|
||||||
|
|
||||||
|
if ($options['aur']) {
|
||||||
|
$data = '<?php
|
||||||
|
function __get_custom_data_full_path()
|
||||||
|
{
|
||||||
|
return \'/var/lib/snappymail\';
|
||||||
|
}
|
||||||
|
';
|
||||||
|
$zip->addFromString('include.php', $data);
|
||||||
|
$tar->addFromString('include.php', $data);
|
||||||
|
} else {
|
||||||
|
$zip->addFile('_include.php');
|
||||||
|
$tar->addFile('_include.php');
|
||||||
|
}
|
||||||
|
|
||||||
|
$zip->addFile('.htaccess');
|
||||||
|
$tar->addFile('.htaccess');
|
||||||
|
|
||||||
|
$index = file_get_contents('index.php');
|
||||||
|
$index = str_replace('0.0.0', $package->version, $index);
|
||||||
|
$zip->addFromString('index.php', $index);
|
||||||
|
$tar->addFromString('index.php', $index);
|
||||||
|
|
||||||
|
$zip->addFile('README.md');
|
||||||
|
$tar->addFile('README.md');
|
||||||
|
|
||||||
|
$zip->close();
|
||||||
|
|
||||||
|
$tar->compress(Phar::GZ);
|
||||||
|
unlink($tar_destination);
|
||||||
|
$tar_destination .= '.gz';
|
||||||
|
|
||||||
|
echo "{$zip_destination} created\n{$tar_destination} created\n";
|
||||||
|
|
||||||
|
if (isset($options['nextcloud'])) {
|
||||||
|
require(ROOT_DIR . '/build/nextcloud.php');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($options['owncloud'])) {
|
||||||
|
require(ROOT_DIR . '/build/owncloud.php');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($options['cpanel'])) {
|
||||||
|
require(ROOT_DIR . '/build/cpanel.php');
|
||||||
|
}
|
||||||
|
|
||||||
|
rename("snappymail/v/{$package->version}", 'snappymail/v/0.0.0');
|
||||||
|
|
||||||
|
file_put_contents("{$destPath}core.json", '{
|
||||||
|
"version": "'.$package->version.'",
|
||||||
|
"file": "../latest.tar.gz",
|
||||||
|
"warnings": []
|
||||||
|
}');
|
||||||
|
|
||||||
|
// Arch User Repository
|
||||||
|
if ($options['aur']) {
|
||||||
|
// extension_loaded('blake2')
|
||||||
|
if (!function_exists('b2sum') && $b2sum = trim(`which b2sum`)) {
|
||||||
|
function b2sum($file) {
|
||||||
|
$file = escapeshellarg($file);
|
||||||
|
exec("b2sum --binary {$file} 2>&1", $output, $exitcode);
|
||||||
|
$output = explode(' ', implode("\n", $output));
|
||||||
|
return $output[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$b2sums = function_exists('b2sum') ? [
|
||||||
|
b2sum($tar_destination),
|
||||||
|
b2sum(ROOT_DIR . '/build/arch/snappymail.sysusers'),
|
||||||
|
b2sum(ROOT_DIR . '/build/arch/snappymail.tmpfiles')
|
||||||
|
] : [];
|
||||||
|
|
||||||
|
file_put_contents('build/arch/.SRCINFO', 'pkgbase = snappymail
|
||||||
|
pkgdesc = modern PHP webmail client
|
||||||
|
pkgver = '.$package->version.'
|
||||||
|
pkgrel = 1
|
||||||
|
url = https://github.com/the-djmaze/snappymail
|
||||||
|
arch = any
|
||||||
|
license = AGPL3
|
||||||
|
makedepends = php
|
||||||
|
makedepends = nodejs
|
||||||
|
makedepends = yarn
|
||||||
|
makedepends = gulp
|
||||||
|
depends = php-fpm
|
||||||
|
optdepends = mariadb: storage backend for contacts
|
||||||
|
optdepends = php-pgsql: storage backend for contacts
|
||||||
|
optdepends = php-sqlite: storage backend for contacts
|
||||||
|
source = snappymail-'.$package->version.'.tar.gz::https://github.com/the-djmaze/snappymail/archive/v'.$package->version.'.tar.gz
|
||||||
|
source = snappymail.sysusers
|
||||||
|
source = snappymail.tmpfiles
|
||||||
|
b2sums = '.implode("\n b2sums = ", $b2sums).'
|
||||||
|
|
||||||
|
pkgname = snappymail
|
||||||
|
');
|
||||||
|
|
||||||
|
$file = ROOT_DIR . '/build/arch/PKGBUILD';
|
||||||
|
if (is_file($file)) {
|
||||||
|
$PKGBUILD = file_get_contents($file);
|
||||||
|
$PKGBUILD = preg_replace('/pkgver=[0-9.]+/', "pkgver={$package->version}", $PKGBUILD);
|
||||||
|
$PKGBUILD = preg_replace('/b2sums=\\([^)]+\\)/s', "b2sums=('".implode("'\n '", $b2sums)."')", $PKGBUILD);
|
||||||
|
file_put_contents($file, $PKGBUILD);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Debian Repository
|
||||||
|
else if (isset($options['debian'])) {
|
||||||
|
require(ROOT_DIR . '/build/deb.php');
|
||||||
|
}
|
||||||
|
// Docker build
|
||||||
|
else if ($options['docker']) {
|
||||||
|
echo "\x1b[33;1m === Docker === \x1b[0m\n";
|
||||||
|
$zip_filename = "snappymail-{$package->version}.zip";
|
||||||
|
copy($zip_destination, "./.docker/release/{$zip_filename}");
|
||||||
|
if ($docker) {
|
||||||
|
passthru("{$docker} build --pull " . ROOT_DIR . "/.docker/release/ --build-arg FILES_ZIP={$zip_filename} -t snappymail:{$package->version}");
|
||||||
|
} else {
|
||||||
|
echo "Docker not installed!\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($options['sign'])) {
|
||||||
|
echo "\x1b[33;1m === PGP Sign === \x1b[0m\n";
|
||||||
|
passthru('gpg --local-user 1016E47079145542F8BA133548208BA13290F3EB --armor --detach-sign '.escapeshellarg($tar_destination), $return_var);
|
||||||
|
passthru('gpg --local-user 1016E47079145542F8BA133548208BA13290F3EB --armor --detach-sign '.escapeshellarg($zip_destination), $return_var);
|
||||||
|
if (isset($options['nextcloud'])) {
|
||||||
|
passthru('gpg --local-user 1016E47079145542F8BA133548208BA13290F3EB --armor --detach-sign '
|
||||||
|
.escapeshellarg("{$destPath}snappymail-{$package->version}-nextcloud.tar.gz"), $return_var);
|
||||||
|
}
|
||||||
|
if (isset($options['owncloud'])) {
|
||||||
|
passthru('gpg --local-user 1016E47079145542F8BA133548208BA13290F3EB --armor --detach-sign '
|
||||||
|
.escapeshellarg("{$destPath}snappymail-{$package->version}-owncloud.tar.gz"), $return_var);
|
||||||
|
}
|
||||||
|
if (isset($options['cpanel'])) {
|
||||||
|
passthru('gpg --local-user 1016E47079145542F8BA133548208BA13290F3EB --armor --detach-sign '
|
||||||
|
.escapeshellarg("{$destPath}snappymail-{$package->version}-cpanel.tar.gz"), $return_var);
|
||||||
|
}
|
||||||
|
if (isset($options['debian'])) {
|
||||||
|
passthru('gpg --local-user 1016E47079145542F8BA133548208BA13290F3EB --armor --detach-sign '
|
||||||
|
. escapeshellarg(ROOT_DIR . "/build/dist/releases/webmail/{$package->version}/" . basename(DEB_DEST_DIR.'.deb')), $return_var);
|
||||||
|
// https://github.com/the-djmaze/snappymail/issues/185#issuecomment-1059420588
|
||||||
|
passthru('gpg --local-user 1016E47079145542F8BA133548208BA13290F3EB --digest-algo SHA512 --clearsign --output '
|
||||||
|
. escapeshellarg(ROOT_DIR . "/build/dist/releases/webmail/{$package->version}/InRelease") . ' '
|
||||||
|
. escapeshellarg(ROOT_DIR . "/build/dist/releases/webmail/{$package->version}/Release"), $return_var);
|
||||||
|
passthru('gpg --local-user 1016E47079145542F8BA133548208BA13290F3EB --digest-algo SHA512 -abs --output '
|
||||||
|
. escapeshellarg(ROOT_DIR . "/build/dist/releases/webmail/{$package->version}/Release.gpg") . ' '
|
||||||
|
. escapeshellarg(ROOT_DIR . "/build/dist/releases/webmail/{$package->version}/Release"), $return_var);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -10,36 +10,42 @@
|
||||||
# By Uwe Bieling <pychi@gmx.de>
|
# By Uwe Bieling <pychi@gmx.de>
|
||||||
# and Jordan S (https://github.com/jas8522)
|
# and Jordan S (https://github.com/jas8522)
|
||||||
|
|
||||||
if [[ $(id -u) -ne 0 ]] ; then echo -e "\033[1;31mPlease run as root\033[0m" ; exit 1 ; fi
|
|
||||||
|
|
||||||
SCRIPT_DIR="$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
|
SCRIPT_DIR="$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
|
||||||
cd "${SCRIPT_DIR}/.."
|
|
||||||
|
|
||||||
if [ ! -d snappymail/v ] ; then echo -e "\033[1;31mThis script can only be run from the SnappyMail install directory\033[0m" ; exit 1 ; fi
|
|
||||||
|
|
||||||
LATEST_URL="https://snappymail.eu/repository/latest.tar.gz"
|
LATEST_URL="https://snappymail.eu/repository/latest.tar.gz"
|
||||||
|
cd "${SCRIPT_DIR}"
|
||||||
OWNERGROUP=`stat -c "%U:%G" snappymail`
|
OWNERGROUP=`stat -c "%U:%G" snappymail`
|
||||||
OLD_VERSION=`grep "define('APP_VERSION" index.php | awk -F\' '{print $4}'`
|
OLD_VERSION=`grep "define('APP_VERSION" index.php | awk -F\' '{print $4}'`
|
||||||
|
|
||||||
# Safety First ... make a backup
|
# check if we are root
|
||||||
DSTAMP=`date +%Y-%m-%d`
|
if [[ $(id -u) -ne 0 ]] ; then echo -e "\033[1;31mPlease run as root\033[0m" ; exit 1 ; fi
|
||||||
echo -e "\033[1;33mBacking up snappymail $OLD_VERSION to ../backup_snappymail_${DSTAMP}.tar.gz\033[0m"
|
|
||||||
tar -czf ../backup_snappymail_${DSTAMP}.tar.gz .
|
# check if we are in the correct dir
|
||||||
|
if [ ! -d snappymail/v ] ; then echo -e "\033[1;31mThis script can only be run from the SnappyMail install directory\033[0m" ; exit 1 ; fi
|
||||||
|
|
||||||
# Download last release to /tmp
|
# Download last release to /tmp
|
||||||
echo -e "\033[1;33mDownloading last release\033[0m"
|
echo -e "\033[1;33mDownloading last release\033[0m"
|
||||||
wget $LATEST_URL -O /tmp/snappymail_latest.tar.gz
|
wget $LATEST_URL -O /tmp/snappymail_latest.tar.gz >/dev/null 2>&1
|
||||||
NEW_VERSION=`tar -tf /tmp/snappymail_latest.tar.gz | grep "snappymail/v/.*/index.php" | awk -F/ '{print $3}'`
|
NEW_VERSION=`tar -tf /tmp/snappymail_latest.tar.gz | grep "snappymail/v/.*/index.php" | awk -F/ '{print $3}'`
|
||||||
|
|
||||||
echo -e "\033[1;33mInstalling $NEW_VERSION\033[0m"
|
if [ $OLD_VERSION != $NEW_VERSION ] ;
|
||||||
tar -xzf /tmp/snappymail_latest.tar.gz
|
then
|
||||||
|
# Safety First ... make a backup
|
||||||
|
DSTAMP=`date +%Y-%m-%d`
|
||||||
|
echo -e "\033[1;33mBacking up snappymail $OLD_VERSION to ../backup_snappymail_${DSTAMP}.tar.gz\033[0m"
|
||||||
|
tar -czf ../backup_snappymail_${DSTAMP}.tar.gz .
|
||||||
|
|
||||||
# set permissions
|
# installing
|
||||||
echo -e "\033[1;33mSet permissions\033[0m"
|
echo -e "\033[1;33mInstalling $NEW_VERSION\033[0m";
|
||||||
find . -type d -exec chmod 755 {} \;
|
tar -xzf /tmp/snappymail_latest.tar.gz;
|
||||||
find . -type f -exec chmod 644 {} \;
|
|
||||||
chmod u+x "${SCRIPT_DIR}/upgrade.sh"
|
|
||||||
chown -R $OWNERGROUP *
|
|
||||||
|
|
||||||
echo -e "\033[1;32mFinished with snappymail upgrade from $OLD_VERSION to $NEW_VERSION... \033[0m"
|
# set permissions
|
||||||
|
echo -e "\033[1;33mSet permissions\033[0m"
|
||||||
|
find . -type d -exec chmod 755 {} \;
|
||||||
|
find . -type f -exec chmod 644 {} \;
|
||||||
|
chmod u+x "${SCRIPT_DIR}/upgrade.sh"
|
||||||
|
chown -R $OWNERGROUP $SCRIPT_DIR
|
||||||
|
echo -e "\033[1;32mFinished with snappymail upgrade from $OLD_VERSION to $NEW_VERSION... \033[0m"
|
||||||
|
|
||||||
|
else
|
||||||
|
echo -e "\033[1;31msame Version skipping.. \033[0m";
|
||||||
|
fi
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import { logoutLink } from 'Common/Links';
|
||||||
import { i18nToNodes, initOnStartOrLangChange } from 'Common/Translator';
|
import { i18nToNodes, initOnStartOrLangChange } from 'Common/Translator';
|
||||||
|
|
||||||
import { LanguageStore } from 'Stores/Language';
|
import { LanguageStore } from 'Stores/Language';
|
||||||
import { ThemeStore } from 'Stores/Theme';
|
import { initThemes } from 'Stores/Theme';
|
||||||
|
|
||||||
import { SelectComponent } from 'Component/Select';
|
import { SelectComponent } from 'Component/Select';
|
||||||
import { CheckboxComponent } from 'Component/Checkbox';
|
import { CheckboxComponent } from 'Component/Checkbox';
|
||||||
|
|
@ -36,9 +36,6 @@ export class AbstractApp {
|
||||||
createViewModel: (params, componentInfo) => {
|
createViewModel: (params, componentInfo) => {
|
||||||
params = params || {};
|
params = params || {};
|
||||||
i18nToNodes(componentInfo.element);
|
i18nToNodes(componentInfo.element);
|
||||||
if (params.inline) {
|
|
||||||
componentInfo.element.style.display = 'inline-block';
|
|
||||||
}
|
|
||||||
return new ClassObject(params);
|
return new ClassObject(params);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -49,7 +46,7 @@ export class AbstractApp {
|
||||||
initOnStartOrLangChange();
|
initOnStartOrLangChange();
|
||||||
|
|
||||||
LanguageStore.populate();
|
LanguageStore.populate();
|
||||||
ThemeStore.populate();
|
initThemes();
|
||||||
|
|
||||||
this.start();
|
this.start();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import 'External/ko';
|
import 'External/ko';
|
||||||
|
|
||||||
import { Settings, SettingsGet } from 'Common/Globals';
|
import { Settings, SettingsGet } from 'Common/Globals';
|
||||||
import { ThemeStore } from 'Stores/Theme';
|
import { initThemes } from 'Stores/Theme';
|
||||||
|
|
||||||
import Remote from 'Remote/Admin/Fetch';
|
import Remote from 'Remote/Admin/Fetch';
|
||||||
|
|
||||||
|
|
@ -18,7 +18,7 @@ export class AdminApp extends AbstractApp {
|
||||||
}
|
}
|
||||||
|
|
||||||
refresh() {
|
refresh() {
|
||||||
ThemeStore.populate();
|
initThemes();
|
||||||
this.start();
|
this.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -27,7 +27,7 @@ export class AdminApp extends AbstractApp {
|
||||||
rl.route.root();
|
rl.route.root();
|
||||||
setTimeout(() => location.href = '/', 1);
|
setTimeout(() => location.href = '/', 1);
|
||||||
} else if (SettingsGet('Auth')) {
|
} else if (SettingsGet('Auth')) {
|
||||||
this.weakPassword(SettingsGet('WeakPassword'));
|
this.weakPassword(SettingsGet('weakPassword'));
|
||||||
startScreens([SettingsAdminScreen]);
|
startScreens([SettingsAdminScreen]);
|
||||||
} else {
|
} else {
|
||||||
startScreens([LoginAdminScreen]);
|
startScreens([LoginAdminScreen]);
|
||||||
|
|
|
||||||
101
dev/App/User.js
101
dev/App/User.js
|
|
@ -1,12 +1,11 @@
|
||||||
import 'External/User/ko';
|
import 'External/User/ko';
|
||||||
|
|
||||||
import { SMAudio } from 'Common/Audio';
|
import { SMAudio } from 'Common/Audio';
|
||||||
import { isArray, pString, changeTheme } from 'Common/Utils';
|
import { isArray, pInt } from 'Common/Utils';
|
||||||
import { mailToHelper, setLayoutResizer, dropdownsDetectVisibility } from 'Common/UtilsUser';
|
import { mailToHelper, setLayoutResizer, dropdownsDetectVisibility } from 'Common/UtilsUser';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
FolderType,
|
FolderType,
|
||||||
SetSystemFoldersNotification,
|
|
||||||
ClientSideKeyNameFolderListSize
|
ClientSideKeyNameFolderListSize
|
||||||
} from 'Common/EnumsUser';
|
} from 'Common/EnumsUser';
|
||||||
|
|
||||||
|
|
@ -38,7 +37,7 @@ import { IdentityUserStore } from 'Stores/User/Identity';
|
||||||
import { FolderUserStore } from 'Stores/User/Folder';
|
import { FolderUserStore } from 'Stores/User/Folder';
|
||||||
import { PgpUserStore } from 'Stores/User/Pgp';
|
import { PgpUserStore } from 'Stores/User/Pgp';
|
||||||
import { MessagelistUserStore } from 'Stores/User/Messagelist';
|
import { MessagelistUserStore } from 'Stores/User/Messagelist';
|
||||||
import { ThemeStore } from 'Stores/Theme';
|
import { ThemeStore, initThemes } from 'Stores/Theme';
|
||||||
import { LanguageStore } from 'Stores/Language';
|
import { LanguageStore } from 'Stores/Language';
|
||||||
import { MessageUserStore } from 'Stores/User/Message';
|
import { MessageUserStore } from 'Stores/User/Message';
|
||||||
|
|
||||||
|
|
@ -62,7 +61,7 @@ import { AskPopupView } from 'View/Popup/Ask';
|
||||||
import {
|
import {
|
||||||
folderInformation,
|
folderInformation,
|
||||||
folderInformationMultiply,
|
folderInformationMultiply,
|
||||||
refreshFoldersInterval,
|
setRefreshFoldersInterval,
|
||||||
messagesMoveHelper,
|
messagesMoveHelper,
|
||||||
messagesDeleteHelper
|
messagesDeleteHelper
|
||||||
} from 'Common/Folders';
|
} from 'Common/Folders';
|
||||||
|
|
@ -80,7 +79,7 @@ export class AppUser extends AbstractApp {
|
||||||
(currentTime > (lastTime + interval + 1000))
|
(currentTime > (lastTime + interval + 1000))
|
||||||
&& Remote.request('Version',
|
&& Remote.request('Version',
|
||||||
iError => (100 < iError) && location.reload(),
|
iError => (100 < iError) && location.reload(),
|
||||||
{ Version: Settings.app('version') }
|
{ version: Settings.app('version') }
|
||||||
);
|
);
|
||||||
lastTime = currentTime;
|
lastTime = currentTime;
|
||||||
}, interval);
|
}, interval);
|
||||||
|
|
@ -91,6 +90,7 @@ export class AppUser extends AbstractApp {
|
||||||
addEventListener('click', dropdownsDetectVisibility);
|
addEventListener('click', dropdownsDetectVisibility);
|
||||||
|
|
||||||
this.folderList = FolderUserStore.folderList;
|
this.folderList = FolderUserStore.folderList;
|
||||||
|
this.messageList = MessagelistUserStore;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -101,27 +101,27 @@ export class AppUser extends AbstractApp {
|
||||||
*/
|
*/
|
||||||
moveMessagesToFolderType(iFolderType, sFromFolderFullName, oUids, bDelete) {
|
moveMessagesToFolderType(iFolderType, sFromFolderFullName, oUids, bDelete) {
|
||||||
let oMoveFolder = null,
|
let oMoveFolder = null,
|
||||||
nSetSystemFoldersNotification = null;
|
nSetSystemFoldersNotification = 0;
|
||||||
|
|
||||||
switch (iFolderType) {
|
switch (iFolderType) {
|
||||||
case FolderType.Spam:
|
case FolderType.Junk:
|
||||||
oMoveFolder = getFolderFromCacheList(FolderUserStore.spamFolder());
|
oMoveFolder = getFolderFromCacheList(FolderUserStore.spamFolder());
|
||||||
nSetSystemFoldersNotification = SetSystemFoldersNotification.Spam;
|
nSetSystemFoldersNotification = iFolderType;
|
||||||
bDelete = bDelete || UNUSED_OPTION_VALUE === FolderUserStore.spamFolder();
|
bDelete = bDelete || UNUSED_OPTION_VALUE === FolderUserStore.spamFolder();
|
||||||
break;
|
break;
|
||||||
case FolderType.NotSpam:
|
case FolderType.Inbox:
|
||||||
oMoveFolder = getFolderFromCacheList(getFolderInboxName());
|
oMoveFolder = getFolderFromCacheList(getFolderInboxName());
|
||||||
break;
|
break;
|
||||||
case FolderType.Trash:
|
case FolderType.Trash:
|
||||||
oMoveFolder = getFolderFromCacheList(FolderUserStore.trashFolder());
|
oMoveFolder = getFolderFromCacheList(FolderUserStore.trashFolder());
|
||||||
nSetSystemFoldersNotification = SetSystemFoldersNotification.Trash;
|
nSetSystemFoldersNotification = iFolderType;
|
||||||
bDelete = bDelete || UNUSED_OPTION_VALUE === FolderUserStore.trashFolder()
|
bDelete = bDelete || UNUSED_OPTION_VALUE === FolderUserStore.trashFolder()
|
||||||
|| sFromFolderFullName === FolderUserStore.spamFolder()
|
|| sFromFolderFullName === FolderUserStore.spamFolder()
|
||||||
|| sFromFolderFullName === FolderUserStore.trashFolder();
|
|| sFromFolderFullName === FolderUserStore.trashFolder();
|
||||||
break;
|
break;
|
||||||
case FolderType.Archive:
|
case FolderType.Archive:
|
||||||
oMoveFolder = getFolderFromCacheList(FolderUserStore.archiveFolder());
|
oMoveFolder = getFolderFromCacheList(FolderUserStore.archiveFolder());
|
||||||
nSetSystemFoldersNotification = SetSystemFoldersNotification.Archive;
|
nSetSystemFoldersNotification = iFolderType;
|
||||||
bDelete = bDelete || UNUSED_OPTION_VALUE === FolderUserStore.archiveFolder();
|
bDelete = bDelete || UNUSED_OPTION_VALUE === FolderUserStore.archiveFolder();
|
||||||
break;
|
break;
|
||||||
// no default
|
// no default
|
||||||
|
|
@ -152,41 +152,18 @@ export class AppUser extends AbstractApp {
|
||||||
IdentityUserStore.loading(false);
|
IdentityUserStore.loading(false);
|
||||||
|
|
||||||
if (!iError) {
|
if (!iError) {
|
||||||
const
|
let items = oData.Result.Accounts;
|
||||||
// counts = {},
|
AccountUserStore(isArray(items)
|
||||||
accounts = oData.Result.Accounts,
|
? items.map(oValue => new AccountModel(oValue.email, oValue.name))
|
||||||
mainEmail = SettingsGet('MainEmail');
|
: []
|
||||||
|
|
||||||
if (isArray(accounts)) {
|
|
||||||
// AccountUserStore.accounts.forEach(oAccount => counts[oAccount.email] = oAccount.count());
|
|
||||||
|
|
||||||
AccountUserStore.accounts(
|
|
||||||
accounts.map(
|
|
||||||
sValue => new AccountModel(sValue/*, counts[sValue]*/)
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
// accounts.length &&
|
AccountUserStore.unshift(new AccountModel(SettingsGet('mainEmail'), '', false));
|
||||||
AccountUserStore.accounts.unshift(new AccountModel(mainEmail/*, counts[mainEmail]*/, false));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isArray(oData.Result.Identities)) {
|
items = oData.Result.Identities;
|
||||||
IdentityUserStore(
|
IdentityUserStore(isArray(items)
|
||||||
oData.Result.Identities.map(identityData => {
|
? items.map(identityData => IdentityModel.reviveFromJson(identityData))
|
||||||
const identity = new IdentityModel(
|
: []
|
||||||
pString(identityData.Id),
|
|
||||||
pString(identityData.Email)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
identity.name(pString(identityData.Name));
|
|
||||||
identity.replyTo(pString(identityData.ReplyTo));
|
|
||||||
identity.bcc(pString(identityData.Bcc));
|
|
||||||
identity.signature(pString(identityData.Signature));
|
|
||||||
identity.signatureInsertBefore(!!identityData.SignatureInsertBefore);
|
|
||||||
|
|
||||||
return identity;
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -200,31 +177,30 @@ export class AppUser extends AbstractApp {
|
||||||
}
|
}
|
||||||
|
|
||||||
logout() {
|
logout() {
|
||||||
|
localStorage.removeItem('register_protocol_offered');
|
||||||
Remote.request('Logout', () => rl.logoutReload(Settings.app('customLogoutLink')));
|
Remote.request('Logout', () => rl.logoutReload(Settings.app('customLogoutLink')));
|
||||||
}
|
}
|
||||||
|
|
||||||
bootstart() {
|
bootstart() {
|
||||||
super.bootstart();
|
super.bootstart();
|
||||||
|
|
||||||
addEventListener('resize', () => leftPanelDisabled(ThemeStore.isMobile() || 1000 > innerWidth));
|
|
||||||
addEventListener('beforeunload', event => {
|
addEventListener('beforeunload', event => {
|
||||||
if (arePopupsVisible() || (ThemeStore.isMobile() && MessageUserStore.message())) {
|
if (arePopupsVisible() || (!SettingsUserStore.usePreviewPane() && MessageUserStore.message())) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
return event.returnValue = "Are you sure you want to exit?";
|
return event.returnValue = i18n('POPUPS_ASK/EXIT_ARE_YOU_SURE');
|
||||||
}
|
}
|
||||||
}, {capture: true});
|
}, {capture: true});
|
||||||
}
|
}
|
||||||
|
|
||||||
refresh() {
|
refresh() {
|
||||||
ThemeStore.populate();
|
initThemes();
|
||||||
LanguageStore.language(SettingsGet('Language'));
|
LanguageStore.language(SettingsGet('language'));
|
||||||
changeTheme(SettingsGet('Theme'));
|
|
||||||
this.start();
|
this.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
start() {
|
start() {
|
||||||
if (SettingsGet('Auth')) {
|
if (SettingsGet('Auth')) {
|
||||||
rl.setWindowTitle(i18n('GLOBAL/LOADING'));
|
rl.setTitle(i18n('GLOBAL/LOADING'));
|
||||||
|
|
||||||
SMAudio.notifications(!!SettingsGet('SoundNotification'));
|
SMAudio.notifications(!!SettingsGet('SoundNotification'));
|
||||||
NotificationUserStore.enabled(!!SettingsGet('DesktopNotifications'));
|
NotificationUserStore.enabled(!!SettingsGet('DesktopNotifications'));
|
||||||
|
|
@ -242,13 +218,7 @@ export class AppUser extends AbstractApp {
|
||||||
SettingsUserScreen
|
SettingsUserScreen
|
||||||
]);
|
]);
|
||||||
|
|
||||||
setInterval(() => {
|
setRefreshFoldersInterval(pInt(SettingsGet('CheckMailInterval')));
|
||||||
const cF = FolderUserStore.currentFolderFullName(),
|
|
||||||
iF = getFolderInboxName();
|
|
||||||
folderInformation(iF);
|
|
||||||
iF === cF || folderInformation(cF);
|
|
||||||
folderInformationMultiply();
|
|
||||||
}, refreshFoldersInterval);
|
|
||||||
|
|
||||||
ContactUserStore.init();
|
ContactUserStore.init();
|
||||||
|
|
||||||
|
|
@ -269,30 +239,31 @@ export class AppUser extends AbstractApp {
|
||||||
// initLeftSideLayoutResizer
|
// initLeftSideLayoutResizer
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const left = elementById('rl-left'),
|
const left = elementById('rl-left'),
|
||||||
right = elementById('rl-right'),
|
|
||||||
fToggle = () =>
|
fToggle = () =>
|
||||||
setLayoutResizer(left, right, ClientSideKeyNameFolderListSize,
|
setLayoutResizer(left, ClientSideKeyNameFolderListSize,
|
||||||
(ThemeStore.isMobile() || leftPanelDisabled()) ? 0 : 'Width');
|
(ThemeStore.isMobile() || leftPanelDisabled()) ? 0 : 'Width');
|
||||||
if (left && right) {
|
if (left) {
|
||||||
fToggle();
|
fToggle();
|
||||||
leftPanelDisabled.subscribe(fToggle);
|
leftPanelDisabled.subscribe(fToggle);
|
||||||
}
|
}
|
||||||
}, 1);
|
}, 1);
|
||||||
|
|
||||||
setInterval(reloadTime(), 60000);
|
setInterval(reloadTime, 60000);
|
||||||
|
|
||||||
PgpUserStore.init();
|
PgpUserStore.init();
|
||||||
|
|
||||||
|
setTimeout(() => mailToHelper(SettingsGet('mailToEmail')), 500);
|
||||||
|
|
||||||
|
if (!localStorage.getItem('register_protocol_offered')) {
|
||||||
// When auto-login is active
|
// When auto-login is active
|
||||||
try {
|
|
||||||
navigator.registerProtocolHandler?.(
|
navigator.registerProtocolHandler?.(
|
||||||
'mailto',
|
'mailto',
|
||||||
location.protocol + '//' + location.host + location.pathname + '?mailto&to=%s',
|
location.protocol + '//' + location.host + location.pathname + '?mailto&to=%s',
|
||||||
(SettingsGet('Title') || 'SnappyMail')
|
(SettingsGet('title') || 'SnappyMail')
|
||||||
);
|
);
|
||||||
} catch (e) {} // eslint-disable-line no-empty
|
localStorage.setItem('register_protocol_offered', '1');
|
||||||
|
}
|
||||||
|
|
||||||
setTimeout(() => mailToHelper(SettingsGet('MailToEmail')), 500);
|
|
||||||
} else {
|
} else {
|
||||||
this.logout();
|
this.logout();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,5 @@
|
||||||
import { MessageSetAction } from 'Common/EnumsUser';
|
let FOLDERS_CACHE = new Map,
|
||||||
import { isArray } from 'Common/Utils';
|
FOLDERS_HASH_MAP = new Map,
|
||||||
|
|
||||||
let FOLDERS_CACHE = {},
|
|
||||||
FOLDERS_NAME_CACHE = {},
|
|
||||||
MESSAGE_FLAGS_CACHE = {},
|
|
||||||
inboxFolderName = 'INBOX';
|
inboxFolderName = 'INBOX';
|
||||||
|
|
||||||
export const
|
export const
|
||||||
|
|
@ -11,18 +7,10 @@ export const
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
clearCache = () => {
|
clearCache = () => {
|
||||||
FOLDERS_CACHE = {};
|
FOLDERS_CACHE.clear();
|
||||||
FOLDERS_NAME_CACHE = {};
|
FOLDERS_HASH_MAP.clear();
|
||||||
MESSAGE_FLAGS_CACHE = {};
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {string} folderFullName
|
|
||||||
* @param {string} uid
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
getMessageKey = (folderFullName, uid) => folderFullName + '#' + uid,
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
|
|
@ -34,143 +22,35 @@ export const
|
||||||
setFolderInboxName = name => inboxFolderName = name,
|
setFolderInboxName = name => inboxFolderName = name,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} folderHash
|
* @param {string} fullNameHash
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
getFolderFullName = folderHash => (folderHash && FOLDERS_NAME_CACHE[folderHash]) || '',
|
getFolderFromHashMap = fullNameHash => getFolderFromCacheList(FOLDERS_HASH_MAP.get(fullNameHash)),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} folderHash
|
|
||||||
* @param {string} folderFullName
|
|
||||||
* @param {?FolderModel} folder
|
* @param {?FolderModel} folder
|
||||||
*/
|
*/
|
||||||
setFolder = folder => {
|
setFolder = folder => {
|
||||||
folder.hash = '';
|
folder.etag = '';
|
||||||
FOLDERS_CACHE[folder.fullName] = folder;
|
FOLDERS_CACHE.set(folder.fullName, folder);
|
||||||
FOLDERS_NAME_CACHE[folder.fullNameHash] = folder.fullName;
|
FOLDERS_HASH_MAP.set(folder.fullNameHash, folder.fullName);
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} folderFullName
|
* @param {string} folderFullName
|
||||||
* @param {string} folderHash
|
* @param {string} folderETag
|
||||||
*/
|
*/
|
||||||
setFolderHash = (folderFullName, folderHash) =>
|
setFolderETag = (folderFullName, folderETag) =>
|
||||||
FOLDERS_CACHE[folderFullName] && (FOLDERS_CACHE[folderFullName].hash = folderHash),
|
FOLDERS_CACHE.has(folderFullName) && (FOLDERS_CACHE.get(folderFullName).etag = folderETag),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} folderFullName
|
* @param {string} folderFullName
|
||||||
* @returns {?FolderModel}
|
* @returns {?FolderModel}
|
||||||
*/
|
*/
|
||||||
getFolderFromCacheList = folderFullName =>
|
getFolderFromCacheList = folderFullName =>
|
||||||
FOLDERS_CACHE[folderFullName] ? FOLDERS_CACHE[folderFullName] : null,
|
FOLDERS_CACHE.get(folderFullName),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} folderFullName
|
* @param {string} folderFullName
|
||||||
*/
|
*/
|
||||||
removeFolderFromCacheList = folderFullName => delete FOLDERS_CACHE[folderFullName];
|
removeFolderFromCacheList = folderFullName => FOLDERS_CACHE.delete(folderFullName);
|
||||||
|
|
||||||
export class MessageFlagsCache
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* @param {string} folderFullName
|
|
||||||
* @param {string} uid
|
|
||||||
* @param {string} flag
|
|
||||||
* @returns {bool}
|
|
||||||
*/
|
|
||||||
static hasFlag(folderFullName, uid, flag) {
|
|
||||||
return MESSAGE_FLAGS_CACHE[folderFullName]?.[uid]?.includes(flag);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {string} folderFullName
|
|
||||||
* @param {string} uid
|
|
||||||
* @returns {?Array}
|
|
||||||
*/
|
|
||||||
static getFor(folderFullName, uid) {
|
|
||||||
return MESSAGE_FLAGS_CACHE[folderFullName]?.[uid];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {string} folderFullName
|
|
||||||
* @param {string} uid
|
|
||||||
* @param {Array} flagsCache
|
|
||||||
*/
|
|
||||||
static setFor(folderFullName, uid, flags) {
|
|
||||||
if (isArray(flags)) {
|
|
||||||
if (!MESSAGE_FLAGS_CACHE[folderFullName]) {
|
|
||||||
MESSAGE_FLAGS_CACHE[folderFullName] = {};
|
|
||||||
}
|
|
||||||
MESSAGE_FLAGS_CACHE[folderFullName][uid] = flags;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {string} folderFullName
|
|
||||||
*/
|
|
||||||
static clearFolder(folderFullName) {
|
|
||||||
MESSAGE_FLAGS_CACHE[folderFullName] = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {(MessageModel|null)} message
|
|
||||||
*/
|
|
||||||
static initMessage(message) {
|
|
||||||
if (message) {
|
|
||||||
const uid = message.uid,
|
|
||||||
flags = this.getFor(message.folder, uid),
|
|
||||||
thread = message.threads();
|
|
||||||
|
|
||||||
isArray(flags) && message.flags(flags);
|
|
||||||
|
|
||||||
if (thread.length) {
|
|
||||||
message.hasUnseenSubMessage(!!thread.find(iSubUid =>
|
|
||||||
(uid !== iSubUid) && !this.hasFlag(message.folder, iSubUid, '\\seen')
|
|
||||||
));
|
|
||||||
message.hasFlaggedSubMessage(!!thread.find(iSubUid =>
|
|
||||||
(uid !== iSubUid) && this.hasFlag(message.folder, iSubUid, '\\flagged')
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {(MessageModel|null)} message
|
|
||||||
*/
|
|
||||||
static store(message) {
|
|
||||||
message && this.setFor(message.folder, message.uid, message.flags());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {string} folder
|
|
||||||
* @param {string} uid
|
|
||||||
* @param {number} setAction
|
|
||||||
*/
|
|
||||||
static storeBySetAction(folder, uid, setAction) {
|
|
||||||
let flags = this.getFor(folder, uid) || [];
|
|
||||||
const
|
|
||||||
unread = flags.includes('\\seen') ? 0 : 1,
|
|
||||||
add = item => flags.includes(item) || flags.push(item),
|
|
||||||
remove = item => flags = flags.filter(flag => flag != item);
|
|
||||||
|
|
||||||
switch (setAction) {
|
|
||||||
case MessageSetAction.SetSeen:
|
|
||||||
add('\\seen');
|
|
||||||
break;
|
|
||||||
case MessageSetAction.UnsetSeen:
|
|
||||||
remove('\\seen');
|
|
||||||
break;
|
|
||||||
case MessageSetAction.SetFlag:
|
|
||||||
add('\\flagged');
|
|
||||||
break;
|
|
||||||
case MessageSetAction.UnsetFlag:
|
|
||||||
remove('\\flagged');
|
|
||||||
break;
|
|
||||||
// no default
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setFor(folder, uid, flags);
|
|
||||||
|
|
||||||
return unread;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -5,12 +5,10 @@ export const
|
||||||
/**
|
/**
|
||||||
* @enum {string}
|
* @enum {string}
|
||||||
*/
|
*/
|
||||||
Scope = {
|
ScopeMessageList = 'MessageList',
|
||||||
MessageList: 'MessageList',
|
ScopeFolderList = 'FolderList',
|
||||||
FolderList: 'FolderList',
|
ScopeMessageView = 'MessageView',
|
||||||
MessageView: 'MessageView',
|
ScopeSettings = 'Settings',
|
||||||
Settings: 'Settings'
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @enum {number}
|
* @enum {number}
|
||||||
|
|
@ -39,7 +37,7 @@ SaveSettingStatus = {
|
||||||
/**
|
/**
|
||||||
* @enum {number}
|
* @enum {number}
|
||||||
*/
|
*/
|
||||||
Notification = {
|
Notifications = {
|
||||||
RequestError: 1,
|
RequestError: 1,
|
||||||
RequestAborted: 2,
|
RequestAborted: 2,
|
||||||
RequestTimeout: 3,
|
RequestTimeout: 3,
|
||||||
|
|
|
||||||
|
|
@ -4,20 +4,33 @@
|
||||||
* @enum {number}
|
* @enum {number}
|
||||||
*/
|
*/
|
||||||
export const FolderType = {
|
export const FolderType = {
|
||||||
User: 0,
|
|
||||||
Inbox: 1,
|
Inbox: 1,
|
||||||
Sent: 2,
|
Sent: 2,
|
||||||
Drafts: 3,
|
Drafts: 3,
|
||||||
Spam: 4, // JUNK
|
Junk: 4, // Spam
|
||||||
Trash: 5,
|
Trash: 5,
|
||||||
Archive: 6,
|
Archive: 6
|
||||||
NotSpam: 80
|
/*
|
||||||
};
|
IMPORTANT : 10;
|
||||||
|
FLAGGED : 11;
|
||||||
|
ALL : 13;
|
||||||
|
// TODO: SnappyMail
|
||||||
|
TEMPLATES : 19;
|
||||||
|
// Kolab
|
||||||
|
CONFIGURATION : 20;
|
||||||
|
CALENDAR : 21;
|
||||||
|
CONTACTS : 22;
|
||||||
|
TASKS : 23;
|
||||||
|
NOTES : 24;
|
||||||
|
FILES : 25;
|
||||||
|
JOURNAL : 26;
|
||||||
|
*/
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @enum {string}
|
* @enum {string}
|
||||||
*/
|
*/
|
||||||
export const FolderMetadataKeys = {
|
FolderMetadataKeys = {
|
||||||
// RFC 5464
|
// RFC 5464
|
||||||
Comment: '/private/comment',
|
Comment: '/private/comment',
|
||||||
CommentShared: '/shared/comment',
|
CommentShared: '/shared/comment',
|
||||||
|
|
@ -26,12 +39,12 @@ export const FolderMetadataKeys = {
|
||||||
// Kolab
|
// Kolab
|
||||||
KolabFolderType: '/private/vendor/kolab/folder-type',
|
KolabFolderType: '/private/vendor/kolab/folder-type',
|
||||||
KolabFolderTypeShared: '/shared/vendor/kolab/folder-type'
|
KolabFolderTypeShared: '/shared/vendor/kolab/folder-type'
|
||||||
};
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @enum {string}
|
* @enum {string}
|
||||||
*/
|
*/
|
||||||
export const ComposeType = {
|
ComposeType = {
|
||||||
Empty: 0,
|
Empty: 0,
|
||||||
Reply: 1,
|
Reply: 1,
|
||||||
ReplyAll: 2,
|
ReplyAll: 2,
|
||||||
|
|
@ -39,63 +52,32 @@ export const ComposeType = {
|
||||||
ForwardAsAttachment: 4,
|
ForwardAsAttachment: 4,
|
||||||
Draft: 5,
|
Draft: 5,
|
||||||
EditAsNew: 6
|
EditAsNew: 6
|
||||||
};
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @enum {number}
|
* @enum {number}
|
||||||
*/
|
*/
|
||||||
export const SetSystemFoldersNotification = {
|
ClientSideKeyNameExpandedFolders = 3,
|
||||||
None: 0,
|
ClientSideKeyNameFolderListSize = 4,
|
||||||
Sent: 1,
|
ClientSideKeyNameMessageListSize = 5,
|
||||||
Draft: 2,
|
ClientSideKeyNameLastSignMe = 7,
|
||||||
Spam: 3,
|
ClientSideKeyNameMessageHeaderFullInfo = 9,
|
||||||
Trash: 4,
|
ClientSideKeyNameMessageAttachmentControls = 10,
|
||||||
Archive: 5
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @enum {number}
|
* @enum {number}
|
||||||
*/
|
*/
|
||||||
export const
|
MessageSetAction = {
|
||||||
ClientSideKeyNameExpandedFolders = 3,
|
|
||||||
ClientSideKeyNameFolderListSize = 4,
|
|
||||||
ClientSideKeyNameMessageListSize = 5,
|
|
||||||
ClientSideKeyNameLastSignMe = 7,
|
|
||||||
ClientSideKeyNameMessageHeaderFullInfo = 9,
|
|
||||||
ClientSideKeyNameMessageAttachmentControls = 10;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @enum {number}
|
|
||||||
*/
|
|
||||||
export const MessageSetAction = {
|
|
||||||
SetSeen: 0,
|
SetSeen: 0,
|
||||||
UnsetSeen: 1,
|
UnsetSeen: 1,
|
||||||
SetFlag: 2,
|
SetFlag: 2,
|
||||||
UnsetFlag: 3
|
UnsetFlag: 3
|
||||||
};
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @enum {number}
|
* @enum {number}
|
||||||
*/
|
*/
|
||||||
export const MessagePriority = {
|
//LayoutNoView = 0,
|
||||||
Low: 5,
|
LayoutSideView = 1,
|
||||||
Normal: 3,
|
LayoutBottomView = 2
|
||||||
High: 1
|
;
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @enum {string}
|
|
||||||
*/
|
|
||||||
export const EditorDefaultType = {
|
|
||||||
Html: 'Html',
|
|
||||||
Plain: 'Plain'
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @enum {number}
|
|
||||||
*/
|
|
||||||
export const Layout = {
|
|
||||||
NoPreview: 0,
|
|
||||||
SidePreview: 1,
|
|
||||||
BottomPreview: 2
|
|
||||||
};
|
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,8 @@ const
|
||||||
ics: 'text/calendar',
|
ics: 'text/calendar',
|
||||||
xml: 'text/xml',
|
xml: 'text/xml',
|
||||||
json: app+'json',
|
json: app+'json',
|
||||||
asc: app+'pgp-signature',
|
// asc: app+'pgp-signature',
|
||||||
|
// asc: app+'pgp-keys',
|
||||||
p10: app+'pkcs10',
|
p10: app+'pkcs10',
|
||||||
p7c: app+'pkcs7-mime',
|
p7c: app+'pkcs7-mime',
|
||||||
p7m: app+'pkcs7-mime',
|
p7m: app+'pkcs7-mime',
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import { isArray, arrayLength } from 'Common/Utils';
|
import { isArray, arrayLength } from 'Common/Utils';
|
||||||
import {
|
import {
|
||||||
MessageFlagsCache,
|
setFolderETag,
|
||||||
setFolderHash,
|
|
||||||
getFolderInboxName,
|
getFolderInboxName,
|
||||||
getFolderFromCacheList
|
getFolderFromCacheList
|
||||||
} from 'Common/Cache';
|
} from 'Common/Cache';
|
||||||
|
|
@ -9,13 +8,27 @@ import { SettingsUserStore } from 'Stores/User/Settings';
|
||||||
import { FolderUserStore } from 'Stores/User/Folder';
|
import { FolderUserStore } from 'Stores/User/Folder';
|
||||||
import { MessagelistUserStore } from 'Stores/User/Messagelist';
|
import { MessagelistUserStore } from 'Stores/User/Messagelist';
|
||||||
import { getNotification } from 'Common/Translator';
|
import { getNotification } from 'Common/Translator';
|
||||||
import { Settings, } from 'Common/Globals';
|
|
||||||
import { serverRequest } from 'Common/Links';
|
|
||||||
|
|
||||||
import Remote from 'Remote/User/Fetch';
|
import Remote from 'Remote/User/Fetch';
|
||||||
|
|
||||||
|
let refreshInterval,
|
||||||
|
// Default every 5 minutes
|
||||||
|
refreshFoldersInterval = 300000;
|
||||||
|
|
||||||
export const
|
export const
|
||||||
|
|
||||||
|
setRefreshFoldersInterval = minutes => {
|
||||||
|
refreshFoldersInterval = Math.max(5, minutes) * 60000;
|
||||||
|
clearInterval(refreshInterval);
|
||||||
|
refreshInterval = setInterval(() => {
|
||||||
|
const cF = FolderUserStore.currentFolderFullName(),
|
||||||
|
iF = getFolderInboxName();
|
||||||
|
folderInformation(iF);
|
||||||
|
iF === cF || folderInformation(cF);
|
||||||
|
folderInformationMultiply();
|
||||||
|
}, refreshFoldersInterval);
|
||||||
|
},
|
||||||
|
|
||||||
sortFolders = folders => {
|
sortFolders = folders => {
|
||||||
try {
|
try {
|
||||||
let collator = new Intl.Collator(undefined, {numeric: true, sensitivity: 'base'});
|
let collator = new Intl.Collator(undefined, {numeric: true, sensitivity: 'base'});
|
||||||
|
|
@ -27,37 +40,6 @@ sortFolders = folders => {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {?Function} fCallback
|
|
||||||
* @param {string} folder
|
|
||||||
* @param {Array=} list = []
|
|
||||||
*/
|
|
||||||
fetchFolderInformation = (fCallback, folder, list = []) => {
|
|
||||||
let fetch = !arrayLength(list);
|
|
||||||
const uids = [],
|
|
||||||
folderFromCache = getFolderFromCacheList(folder);
|
|
||||||
|
|
||||||
if (!fetch) {
|
|
||||||
list.forEach(messageListItem => {
|
|
||||||
MessageFlagsCache.getFor(folder, messageListItem.uid) || uids.push(messageListItem.uid);
|
|
||||||
messageListItem.threads.forEach(uid => {
|
|
||||||
MessageFlagsCache.getFor(folder, uid) || uids.push(uid);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
fetch = uids.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (fetch) {
|
|
||||||
Remote.request('FolderInformation', fCallback, {
|
|
||||||
Folder: folder,
|
|
||||||
FlagsUids: uids,
|
|
||||||
UidNext: folderFromCache?.uidNext || 0 // Used to check for new messages
|
|
||||||
});
|
|
||||||
} else if (SettingsUserStore.useThreads()) {
|
|
||||||
MessagelistUserStore.reloadFlagsAndCachedMessage();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Array=} aDisabled
|
* @param {Array=} aDisabled
|
||||||
* @param {Array=} aHeaderLines
|
* @param {Array=} aHeaderLines
|
||||||
|
|
@ -118,56 +100,59 @@ folderListOptionsBuilder = (
|
||||||
return aResult;
|
return aResult;
|
||||||
},
|
},
|
||||||
|
|
||||||
// Every 5 minutes
|
|
||||||
refreshFoldersInterval = 300000,
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} folder
|
* @param {string} folder
|
||||||
* @param {Array=} list = []
|
* @param {Array=} list = []
|
||||||
*/
|
*/
|
||||||
folderInformation = (folder, list) => {
|
folderInformation = (folder, list) => {
|
||||||
if (folder?.trim()) {
|
if (folder?.trim()) {
|
||||||
fetchFolderInformation(
|
let count = 1;
|
||||||
(iError, data) => {
|
const uids = [];
|
||||||
|
|
||||||
|
if (arrayLength(list)) {
|
||||||
|
list.forEach(messageListItem => {
|
||||||
|
uids.push(messageListItem.uid);
|
||||||
|
messageListItem.threads.forEach(uid => uids.push(uid));
|
||||||
|
});
|
||||||
|
count = uids.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (count) {
|
||||||
|
Remote.request('FolderInformation', (iError, data) => {
|
||||||
if (!iError && data.Result) {
|
if (!iError && data.Result) {
|
||||||
const result = data.Result,
|
const result = data.Result,
|
||||||
folderFromCache = getFolderFromCacheList(result.Folder);
|
folderFromCache = getFolderFromCacheList(result.name);
|
||||||
if (folderFromCache) {
|
if (folderFromCache) {
|
||||||
const oldHash = folderFromCache.hash,
|
const oldHash = folderFromCache.etag,
|
||||||
unreadCountChange = (folderFromCache.unreadEmails() !== result.unreadEmails);
|
unreadCountChange = (folderFromCache.unreadEmails() !== result.unreadEmails);
|
||||||
|
|
||||||
// folderFromCache.revivePropertiesFromJson(result);
|
// folderFromCache.revivePropertiesFromJson(result);
|
||||||
folderFromCache.expires = Date.now();
|
folderFromCache.expires = Date.now();
|
||||||
folderFromCache.uidNext = result.UidNext;
|
folderFromCache.uidNext = result.uidNext;
|
||||||
folderFromCache.hash = result.Hash;
|
folderFromCache.etag = result.etag;
|
||||||
folderFromCache.totalEmails(result.totalEmails);
|
folderFromCache.totalEmails(result.totalEmails);
|
||||||
folderFromCache.unreadEmails(result.unreadEmails);
|
folderFromCache.unreadEmails(result.unreadEmails);
|
||||||
|
|
||||||
unreadCountChange && MessageFlagsCache.clearFolder(folderFromCache.fullName);
|
MessagelistUserStore.notifyNewMessages(folderFromCache.fullName, result.newMessages);
|
||||||
|
|
||||||
if (result.MessagesFlags.length) {
|
if (!oldHash || unreadCountChange || result.etag !== oldHash) {
|
||||||
result.MessagesFlags.forEach(message =>
|
|
||||||
MessageFlagsCache.setFor(folderFromCache.fullName, message.Uid.toString(), message.Flags)
|
|
||||||
);
|
|
||||||
|
|
||||||
MessagelistUserStore.reloadFlagsAndCachedMessage();
|
|
||||||
}
|
|
||||||
|
|
||||||
MessagelistUserStore.notifyNewMessages(folderFromCache.fullName, result.NewMessages);
|
|
||||||
|
|
||||||
if (!oldHash || unreadCountChange || result.Hash !== oldHash) {
|
|
||||||
if (folderFromCache.fullName === FolderUserStore.currentFolderFullName()) {
|
if (folderFromCache.fullName === FolderUserStore.currentFolderFullName()) {
|
||||||
MessagelistUserStore.reload();
|
MessagelistUserStore.reload();
|
||||||
|
/*
|
||||||
} else if (getFolderInboxName() === folderFromCache.fullName) {
|
} else if (getFolderInboxName() === folderFromCache.fullName) {
|
||||||
Remote.messageList(null, {Folder: getFolderInboxName()}, true);
|
// Remote.messageList(null, {folder: getFolderFromCacheList(getFolderInboxName())}, true);
|
||||||
|
Remote.messageList(null, {folder: getFolderInboxName()}, true);
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
}, {
|
||||||
folder,
|
folder: folder,
|
||||||
list
|
flagsUids: uids,
|
||||||
);
|
uidNext: getFolderFromCacheList(folder)?.uidNext || 0 // Used to check for new messages
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -181,21 +166,18 @@ folderInformationMultiply = (boot = false) => {
|
||||||
if (!iError && arrayLength(oData.Result)) {
|
if (!iError && arrayLength(oData.Result)) {
|
||||||
const utc = Date.now();
|
const utc = Date.now();
|
||||||
oData.Result.forEach(item => {
|
oData.Result.forEach(item => {
|
||||||
const folder = getFolderFromCacheList(item.Folder);
|
const folder = getFolderFromCacheList(item.name);
|
||||||
|
|
||||||
if (folder) {
|
if (folder) {
|
||||||
const oldHash = folder.hash,
|
const oldHash = folder.etag,
|
||||||
unreadCountChange = folder.unreadEmails() !== item.unreadEmails;
|
unreadCountChange = folder.unreadEmails() !== item.unreadEmails;
|
||||||
|
|
||||||
// folder.revivePropertiesFromJson(item);
|
// folder.revivePropertiesFromJson(item);
|
||||||
folder.expires = utc;
|
folder.expires = utc;
|
||||||
folder.hash = item.Hash;
|
folder.etag = item.etag;
|
||||||
folder.totalEmails(item.totalEmails);
|
folder.totalEmails(item.totalEmails);
|
||||||
folder.unreadEmails(item.unreadEmails);
|
folder.unreadEmails(item.unreadEmails);
|
||||||
|
|
||||||
unreadCountChange && MessageFlagsCache.clearFolder(folder.fullName);
|
if (!oldHash || item.etag !== oldHash) {
|
||||||
|
|
||||||
if (!oldHash || item.Hash !== oldHash) {
|
|
||||||
if (folder.fullName === FolderUserStore.currentFolderFullName()) {
|
if (folder.fullName === FolderUserStore.currentFolderFullName()) {
|
||||||
MessagelistUserStore.reload();
|
MessagelistUserStore.reload();
|
||||||
}
|
}
|
||||||
|
|
@ -210,20 +192,20 @@ folderInformationMultiply = (boot = false) => {
|
||||||
boot && setTimeout(() => folderInformationMultiply(true), 2000);
|
boot && setTimeout(() => folderInformationMultiply(true), 2000);
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
Folders: folders
|
folders: folders
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
moveOrDeleteResponseHelper = (iError, oData) => {
|
moveOrDeleteResponseHelper = (iError, oData) => {
|
||||||
if (iError) {
|
if (iError) {
|
||||||
setFolderHash(FolderUserStore.currentFolderFullName(), '');
|
setFolderETag(FolderUserStore.currentFolderFullName(), '');
|
||||||
alert(getNotification(iError));
|
alert(getNotification(iError));
|
||||||
} else if (FolderUserStore.currentFolder()) {
|
} else if (FolderUserStore.currentFolder()) {
|
||||||
if (2 === arrayLength(oData.Result)) {
|
if (2 === arrayLength(oData.Result)) {
|
||||||
setFolderHash(oData.Result[0], oData.Result[1]);
|
setFolderETag(oData.Result[0], oData.Result[1]);
|
||||||
} else {
|
} else {
|
||||||
setFolderHash(FolderUserStore.currentFolderFullName(), '');
|
setFolderETag(FolderUserStore.currentFolderFullName(), '');
|
||||||
}
|
}
|
||||||
MessagelistUserStore.reload(!MessagelistUserStore.length);
|
MessagelistUserStore.reload(!MessagelistUserStore.length);
|
||||||
}
|
}
|
||||||
|
|
@ -235,24 +217,24 @@ messagesMoveHelper = (fromFolderFullName, toFolderFullName, uidsForMove) => {
|
||||||
isSpam = sSpamFolder === toFolderFullName,
|
isSpam = sSpamFolder === toFolderFullName,
|
||||||
isHam = !isSpam && sSpamFolder === fromFolderFullName && getFolderInboxName() === toFolderFullName;
|
isHam = !isSpam && sSpamFolder === fromFolderFullName && getFolderInboxName() === toFolderFullName;
|
||||||
|
|
||||||
Remote.abort('MessageList').request('MessageMove',
|
Remote.abort('MessageList', 'reload').request('MessageMove',
|
||||||
moveOrDeleteResponseHelper,
|
moveOrDeleteResponseHelper,
|
||||||
{
|
{
|
||||||
FromFolder: fromFolderFullName,
|
fromFolder: fromFolderFullName,
|
||||||
ToFolder: toFolderFullName,
|
toFolder: toFolderFullName,
|
||||||
Uids: [...uidsForMove].join(','),
|
uids: [...uidsForMove].join(','),
|
||||||
MarkAsRead: (isSpam || FolderUserStore.trashFolder() === toFolderFullName) ? 1 : 0,
|
markAsRead: (isSpam || FolderUserStore.trashFolder() === toFolderFullName) ? 1 : 0,
|
||||||
Learning: isSpam ? 'SPAM' : isHam ? 'HAM' : ''
|
learning: isSpam ? 'SPAM' : isHam ? 'HAM' : ''
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
messagesDeleteHelper = (sFromFolderFullName, aUidForRemove) => {
|
messagesDeleteHelper = (sFromFolderFullName, aUidForRemove) => {
|
||||||
Remote.abort('MessageList').request('MessageDelete',
|
Remote.abort('MessageList', 'reload').request('MessageDelete',
|
||||||
moveOrDeleteResponseHelper,
|
moveOrDeleteResponseHelper,
|
||||||
{
|
{
|
||||||
Folder: sFromFolderFullName,
|
folder: sFromFolderFullName,
|
||||||
Uids: [...aUidForRemove].join(',')
|
uids: [...aUidForRemove].join(',')
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
@ -271,9 +253,9 @@ moveMessagesToFolder = (sFromFolderFullName, oUids, sToFolderFullName, bCopy) =>
|
||||||
if (oFromFolder && oToFolder) {
|
if (oFromFolder && oToFolder) {
|
||||||
bCopy
|
bCopy
|
||||||
? Remote.request('MessageCopy', null, {
|
? Remote.request('MessageCopy', null, {
|
||||||
FromFolder: oFromFolder.fullName,
|
fromFolder: oFromFolder.fullName,
|
||||||
ToFolder: oToFolder.fullName,
|
toFolder: oToFolder.fullName,
|
||||||
Uids: [...oUids].join(',')
|
uids: [...oUids].join(',')
|
||||||
})
|
})
|
||||||
: messagesMoveHelper(oFromFolder.fullName, oToFolder.fullName, oUids);
|
: messagesMoveHelper(oFromFolder.fullName, oToFolder.fullName, oUids);
|
||||||
|
|
||||||
|
|
@ -286,28 +268,20 @@ moveMessagesToFolder = (sFromFolderFullName, oUids, sToFolderFullName, bCopy) =>
|
||||||
},
|
},
|
||||||
|
|
||||||
dropFilesInFolder = (sFolderFullName, files) => {
|
dropFilesInFolder = (sFolderFullName, files) => {
|
||||||
let count = 0,
|
let count = files.length;
|
||||||
fn = () => 0 == --count
|
|
||||||
&& FolderUserStore.currentFolderFullName() == sFolderFullName
|
|
||||||
&& MessagelistUserStore.reload(true, true);
|
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
if ('message/rfc822' === file.type) {
|
if ('message/rfc822' === file.type) {
|
||||||
++count;
|
|
||||||
let data = new FormData;
|
let data = new FormData;
|
||||||
data.append('Folder', sFolderFullName);
|
data.append('folder', sFolderFullName);
|
||||||
data.append('AppendFile', file);
|
data.append('appendFile', file);
|
||||||
data.XToken = Settings.app('token');
|
Remote.request('FolderAppend', (iError, data)=>{
|
||||||
fetch(serverRequest('Append'), {
|
iError && console.error(data.ErrorMessage);
|
||||||
method: 'POST',
|
0 == --count
|
||||||
mode: 'same-origin',
|
&& FolderUserStore.currentFolderFullName() == sFolderFullName
|
||||||
cache: 'no-cache',
|
&& MessagelistUserStore.reload(true, true);
|
||||||
redirect: 'error',
|
}, data);
|
||||||
referrerPolicy: 'no-referrer',
|
} else {
|
||||||
credentials: 'same-origin',
|
--count;
|
||||||
body: data
|
|
||||||
})
|
|
||||||
.then(fn)
|
|
||||||
.catch(fn);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,19 @@
|
||||||
import { doc, elementById, $htmlCL } from 'Common/Globals';
|
import { doc, appEl, $htmlCL } from 'Common/Globals';
|
||||||
|
|
||||||
// Fullscreen must be on app, else other popups fail
|
// Fullscreen must be on app, else other popups fail
|
||||||
export const
|
export const
|
||||||
app = elementById('rl-app'),
|
appFullscreen = () => (doc.fullscreenElement || doc.webkitFullscreenElement) === appEl,
|
||||||
appFullscreen = () => (doc.fullscreenElement || doc.webkitFullscreenElement) === app,
|
|
||||||
exitFullscreen = () => appFullscreen() && (doc.exitFullscreen || doc.webkitExitFullscreen).call(doc),
|
exitFullscreen = () => appFullscreen() && (doc.exitFullscreen || doc.webkitExitFullscreen).call(doc),
|
||||||
isFullscreen = ko.observable(false),
|
isFullscreen = ko.observable(false),
|
||||||
toggleFullscreen = () => isFullscreen() ? exitFullscreen() : app.requestFullscreen();
|
toggleFullscreen = () => isFullscreen() ? exitFullscreen() : appEl.requestFullscreen();
|
||||||
|
|
||||||
if (app) {
|
if (appEl) {
|
||||||
let event = 'fullscreenchange';
|
let event = 'fullscreenchange';
|
||||||
if (!app.requestFullscreen && app.webkitRequestFullscreen) {
|
if (!appEl.requestFullscreen && appEl.webkitRequestFullscreen) {
|
||||||
app.requestFullscreen = app.webkitRequestFullscreen;
|
appEl.requestFullscreen = appEl.webkitRequestFullscreen;
|
||||||
event = 'webkit'+event;
|
event = 'webkit'+event;
|
||||||
}
|
}
|
||||||
if (app.requestFullscreen) {
|
if (appEl.requestFullscreen) {
|
||||||
doc.addEventListener(event, () => {
|
doc.addEventListener(event, () => {
|
||||||
isFullscreen(appFullscreen());
|
isFullscreen(appFullscreen());
|
||||||
$htmlCL.toggle('rl-fullscreen', appFullscreen());
|
$htmlCL.toggle('rl-fullscreen', appFullscreen());
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,8 @@ export const
|
||||||
|
|
||||||
elementById = id => doc.getElementById(id),
|
elementById = id => doc.getElementById(id),
|
||||||
|
|
||||||
|
appEl = elementById('rl-app'),
|
||||||
|
|
||||||
Settings = rl.settings,
|
Settings = rl.settings,
|
||||||
SettingsGet = Settings.get,
|
SettingsGet = Settings.get,
|
||||||
SettingsCapa = name => name && !!(SettingsGet('Capa') || {})[name],
|
SettingsCapa = name => name && !!(SettingsGet('Capa') || {})[name],
|
||||||
|
|
@ -18,8 +20,8 @@ export const
|
||||||
dropdowns = [],
|
dropdowns = [],
|
||||||
dropdownVisibility = ko.observable(false).extend({ rateLimit: 0 }),
|
dropdownVisibility = ko.observable(false).extend({ rateLimit: 0 }),
|
||||||
|
|
||||||
moveAction = ko.observable(false),
|
|
||||||
leftPanelDisabled = ko.observable(false),
|
leftPanelDisabled = ko.observable(false),
|
||||||
|
toggleLeftPanel = () => leftPanelDisabled(!leftPanelDisabled()),
|
||||||
|
|
||||||
createElement = (name, attr) => {
|
createElement = (name, attr) => {
|
||||||
let el = doc.createElement(name);
|
let el = doc.createElement(name);
|
||||||
|
|
@ -31,6 +33,11 @@ export const
|
||||||
new CustomEvent(name, {detail:detail, cancelable: !!cancelable})
|
new CustomEvent(name, {detail:detail, cancelable: !!cancelable})
|
||||||
),
|
),
|
||||||
|
|
||||||
|
stopEvent = event => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
},
|
||||||
|
|
||||||
formFieldFocused = () => doc.activeElement?.matches('input,textarea'),
|
formFieldFocused = () => doc.activeElement?.matches('input,textarea'),
|
||||||
|
|
||||||
addShortcut = (...args) => shortcuts.add(...args),
|
addShortcut = (...args) => shortcuts.add(...args),
|
||||||
|
|
@ -68,10 +75,4 @@ dropdownVisibility.subscribe(value => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
leftPanelDisabled.toggle = () => leftPanelDisabled(!leftPanelDisabled());
|
leftPanelDisabled.subscribe(value => $htmlCL.toggle('rl-left-panel-disabled', value));
|
||||||
leftPanelDisabled.subscribe(value => {
|
|
||||||
value && moveAction() && moveAction(false);
|
|
||||||
$htmlCL.toggle('rl-left-panel-disabled', value);
|
|
||||||
});
|
|
||||||
|
|
||||||
moveAction.subscribe(value => value && leftPanelDisabled() && leftPanelDisabled(false));
|
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import { forEachObjectEntry, pInt } from 'Common/Utils';
|
||||||
import { SettingsUserStore } from 'Stores/User/Settings';
|
import { SettingsUserStore } from 'Stores/User/Settings';
|
||||||
|
|
||||||
const
|
const
|
||||||
tpl = createElement('template'),
|
tmpl = createElement('template'),
|
||||||
htmlre = /[&<>"']/g,
|
htmlre = /[&<>"']/g,
|
||||||
httpre = /^(https?:)?\/\//i,
|
httpre = /^(https?:)?\/\//i,
|
||||||
htmlmap = {
|
htmlmap = {
|
||||||
|
|
@ -14,13 +14,32 @@ const
|
||||||
"'": '''
|
"'": '''
|
||||||
},
|
},
|
||||||
|
|
||||||
|
disallowedTags = [
|
||||||
|
'svg','script','title','link','base','meta',
|
||||||
|
'input','output','select','button','textarea',
|
||||||
|
'bgsound','keygen','source','object','embed','applet','iframe','frame','frameset','video','audio','area','map'
|
||||||
|
// not supported by <template> element
|
||||||
|
// ,'html','head','body'
|
||||||
|
].join(','),
|
||||||
|
|
||||||
|
blockquoteSwitcher = () => {
|
||||||
|
SettingsUserStore.collapseBlockquotes() &&
|
||||||
|
// tmpl.content.querySelectorAll('blockquote').forEach(node => {
|
||||||
|
[...tmpl.content.querySelectorAll('blockquote')].reverse().forEach(node => {
|
||||||
|
const el = createElement('details', {class:'sm-bq-switcher'});
|
||||||
|
el.innerHTML = '<summary>•••</summary>';
|
||||||
|
node.replaceWith(el);
|
||||||
|
el.append(node);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
replaceWithChildren = node => node.replaceWith(...[...node.childNodes]),
|
replaceWithChildren = node => node.replaceWith(...[...node.childNodes]),
|
||||||
|
|
||||||
url = /(^|\s|\n|\/?>)(https?:\/\/[-A-Z0-9+&#/%?=()~_|!:,.;]*[-A-Z0-9+&#/%=~()_|])/gi,
|
urlRegExp = /https?:\/\/[^\p{C}\p{Z}]+[^\p{C}\p{Z}.]/gu,
|
||||||
// eslint-disable-next-line max-len
|
// eslint-disable-next-line max-len
|
||||||
email = /(^|\s|\n|\/?>)((?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x21\x23-\x5b\x5d-\x7f]|\\[\x21\x23-\x5b\x5d-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\.){3}(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9])|[a-z0-9-]*[a-z0-9]:(?:[\x21-\x5a\x53-\x7f]|\\[\x21\x23-\x5b\x5d-\x7f])+)\]))/gi,
|
email = /(^|\r|\n|\p{C}\p{Z})((?:[^"(),.:;<>@[\]\\\p{C}\p{Z}]+(?:\.[^"(),.:;<>@[\]\\\p{C}\p{Z}]+)*|"(?:\\?[^"\\\p{C}\p{Z}])*")@[^@\p{C}\p{Z}]+[^@\p{C}\p{Z}.])/gui,
|
||||||
// rfc3966
|
// rfc3966
|
||||||
tel = /(^|\s|\n|\/?>)(tel:(\+[0-9().-]+|[0-9*#().-]+(;phone-context=\+[0-9+().-]+)?))/g,
|
tel = /(tel:(\+[0-9().-]+|[0-9*#().-]+(;phone-context=\+[0-9+().-]+)?))/g,
|
||||||
|
|
||||||
// Strip tracking
|
// Strip tracking
|
||||||
/** TODO: implement other url strippers like from
|
/** TODO: implement other url strippers like from
|
||||||
|
|
@ -31,17 +50,21 @@ const
|
||||||
* https://github.com/M66B/FairEmail/blob/master/app/src/main/java/eu/faircode/email/UriHelper.java
|
* https://github.com/M66B/FairEmail/blob/master/app/src/main/java/eu/faircode/email/UriHelper.java
|
||||||
*/
|
*/
|
||||||
// eslint-disable-next-line max-len
|
// eslint-disable-next-line max-len
|
||||||
stripParams = /^(utm_|ec_|fbclid|mc_eid|mkt_tok|_hsenc|vero_id|oly_enc_id|oly_anon_id|__s|Referrer|mailing|elq|bch|trc|ref|correlation_id|pd_|pf_)/i,
|
stripParams = /^(utm_|ec_|fbclid|mc_eid|mkt_tok|_hsenc|vero_id|oly_enc_id|oly_anon_id|__s|Referrer|mailing|elq|bch|trc|ref|correlation_id|pd_|pf_|email_hash)/i,
|
||||||
urlGetParam = (url, name) => new URL(url).searchParams.get(name) || url,
|
urlGetParam = (url, name) => new URL(url).searchParams.get(name) || url,
|
||||||
base64Url = data => atob(data.replace(/_/g,'/').replace(/-/g,'+')),
|
base64Url = data => atob(data.replace(/_/g,'/').replace(/-/g,'+')),
|
||||||
|
decode = decodeURIComponent,
|
||||||
stripTracking = url => {
|
stripTracking = url => {
|
||||||
try {
|
try {
|
||||||
let nurl = url
|
let nurl = url
|
||||||
|
// Copernica
|
||||||
|
.replace(/^.+\/(https%253A[^/?&]+).*$/i, (...m) => decode(decode(m[1])))
|
||||||
.replace(/tracking\.(printabout\.nl[^?]+)\?.*/i, (...m) => m[1])
|
.replace(/tracking\.(printabout\.nl[^?]+)\?.*/i, (...m) => m[1])
|
||||||
.replace(/(zalando\.nl[^?]+)\?.*/i, (...m) => m[1])
|
.replace(/(zalando\.nl[^?]+)\?.*/i, (...m) => m[1])
|
||||||
.replace(/^.+(awstrack\.me|redditmail\.com)\/.+(https:%2F%2F[^/]+).*/i, (...m) => decodeURIComponent(m[2]))
|
.replace(/^.+(awstrack\.me|redditmail\.com)\/.+(https:%2F%2F[^/]+).*/i, (...m) => decode(m[2]))
|
||||||
.replace(/^.+(www\.google|safelinks\.protection\.outlook\.com|mailchimp\.com).+url=.+$/i,
|
.replace(/^.+(www\.google|safelinks\.protection\.outlook\.com|mailchimp\.com).+url=.+$/i,
|
||||||
() => urlGetParam(url, 'url'))
|
() => urlGetParam(url, 'url'))
|
||||||
|
.replace(/^.+click\.godaddy\.com.+$/i, () => urlGetParam(url, 'redir'))
|
||||||
.replace(/^.+delivery-status\.com.+$/i, () => urlGetParam(url, 'fb'))
|
.replace(/^.+delivery-status\.com.+$/i, () => urlGetParam(url, 'fb'))
|
||||||
.replace(/^.+go\.dhlparcel\.nl.+\/([A-Za-z0-9_-]+)$/i, (...m) => base64Url(m[1]))
|
.replace(/^.+go\.dhlparcel\.nl.+\/([A-Za-z0-9_-]+)$/i, (...m) => base64Url(m[1]))
|
||||||
.replace(/^(.+mopinion\.com.+)\?.*$/i, (...m) => m[1])
|
.replace(/^(.+mopinion\.com.+)\?.*$/i, (...m) => m[1])
|
||||||
|
|
@ -73,6 +96,93 @@ const
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return url;
|
return url;
|
||||||
|
},
|
||||||
|
|
||||||
|
cleanCSS = source =>
|
||||||
|
source.trim().replace(/-(ms|webkit)-[^;]+(;|$)/g, '')
|
||||||
|
.replace(/white-space[^;]+(;|$)/g, '')
|
||||||
|
// Drop Microsoft Office style properties
|
||||||
|
// .replace(/mso-[^:;]+:[^;]+/gi, '')
|
||||||
|
,
|
||||||
|
|
||||||
|
/*
|
||||||
|
Parses given css string, and returns css object
|
||||||
|
keys as selectors and values are css rules
|
||||||
|
eliminates all css comments before parsing
|
||||||
|
|
||||||
|
@param source css string to be parsed
|
||||||
|
|
||||||
|
@return object css
|
||||||
|
*/
|
||||||
|
parseCSS = source => {
|
||||||
|
const css = [];
|
||||||
|
css.toString = () => css.reduce(
|
||||||
|
(ret, tmp) =>
|
||||||
|
ret + tmp.selector + ' {\n'
|
||||||
|
+ (tmp.type === 'media' ? tmp.subStyles.toString() : tmp.rules)
|
||||||
|
+ '}\n'
|
||||||
|
,
|
||||||
|
''
|
||||||
|
);
|
||||||
|
/**
|
||||||
|
* Given css array, parses it and then for every selector,
|
||||||
|
* prepends namespace to prevent css collision issues
|
||||||
|
*/
|
||||||
|
css.applyNamespace = namespace => css.forEach(obj => {
|
||||||
|
if (obj.type === 'media') {
|
||||||
|
obj.subStyles.applyNamespace(namespace);
|
||||||
|
} else {
|
||||||
|
obj.selector = obj.selector.split(',').map(selector =>
|
||||||
|
(namespace + ' .mail-body ' + selector.replace(/\./g, '.msg-'))
|
||||||
|
.replace(/\sbody/gi, '')
|
||||||
|
).join(',');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (source) {
|
||||||
|
source = source
|
||||||
|
// strip comments
|
||||||
|
.replace(/\/\*[\s\S]*?\*\/|<!--|-->/gi, '')
|
||||||
|
// strip import statements
|
||||||
|
.replace(/@import .*?;/gi , '')
|
||||||
|
// strip keyframe statements
|
||||||
|
.replace(/((@.*?keyframes [\s\S]*?){([\s\S]*?}\s*?)})/gi, '');
|
||||||
|
|
||||||
|
// unified regex to match css & media queries together
|
||||||
|
let unified = /((\s*?(?:\/\*[\s\S]*?\*\/)?\s*?@media[\s\S]*?){([\s\S]*?)}\s*?})|(([\s\S]*?){([\s\S]*?)})/gi,
|
||||||
|
arr;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
arr = unified.exec(source);
|
||||||
|
if (arr === null) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
let selector = arr[arr[2] === undefined ? 5 : 2].split('\r\n').join('\n').trim()
|
||||||
|
// Never have more than a single line break in a row
|
||||||
|
.replace(/\n+/, "\n")
|
||||||
|
// Remove :root and html
|
||||||
|
.split(/\s+/g).map(item => item.replace(/^(:root|html)$/, '')).join(' ').trim();
|
||||||
|
|
||||||
|
// determine the type
|
||||||
|
if (selector.includes('@media')) {
|
||||||
|
// we have a media query
|
||||||
|
css.push({
|
||||||
|
selector: selector,
|
||||||
|
type: 'media',
|
||||||
|
subStyles: parseCSS(arr[3] + '\n}') //recursively parse media query inner css
|
||||||
|
});
|
||||||
|
} else if (selector && !selector.includes('@')) {
|
||||||
|
// we have standard css
|
||||||
|
css.push({
|
||||||
|
selector: selector,
|
||||||
|
rules: cleanCSS(arr[6])
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return css;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const
|
export const
|
||||||
|
|
@ -88,28 +198,27 @@ export const
|
||||||
* @param {string} text
|
* @param {string} text
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
cleanHtml = (html, oAttachments) => {
|
cleanHtml = (html, oAttachments, msgId) => {
|
||||||
|
let aColor;
|
||||||
const
|
const
|
||||||
debug = false, // Config()->Get('debug', 'enable', false);
|
debug = false, // Config()->Get('debug', 'enable', false);
|
||||||
detectHiddenImages = true, // !!SettingsGet('try_to_detect_hidden_images'),
|
detectHiddenImages = true, // !!SettingsGet('try_to_detect_hidden_images'),
|
||||||
|
|
||||||
|
bqLevel = parseInt(SettingsUserStore.maxBlockquotesLevel()),
|
||||||
|
|
||||||
result = {
|
result = {
|
||||||
hasExternals: false
|
hasExternals: false
|
||||||
},
|
},
|
||||||
|
|
||||||
findAttachmentByCid = cid => oAttachments.findByCid(cid),
|
findAttachmentByCid = cId => oAttachments.findByCid(cId),
|
||||||
findLocationByCid = cid => {
|
findLocationByCid = cId => {
|
||||||
const attachment = findAttachmentByCid(cid);
|
const attachment = findAttachmentByCid(cId);
|
||||||
return attachment?.contentLocation ? attachment : 0;
|
return attachment?.contentLocation ? attachment : 0;
|
||||||
},
|
},
|
||||||
|
|
||||||
// convert body attributes to CSS
|
// convert body attributes to CSS
|
||||||
tasks = {
|
tasks = {
|
||||||
link: value => {
|
link: value => aColor = value,
|
||||||
if (/^#[a-fA-Z0-9]{3,6}$/.test(value)) {
|
|
||||||
tpl.content.querySelectorAll('a').forEach(node => node.style.color || (node.style.color = value))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
text: (value, node) => node.style.color = value,
|
text: (value, node) => node.style.color = value,
|
||||||
topmargin: (value, node) => node.style.marginTop = pInt(value) + 'px',
|
topmargin: (value, node) => node.style.marginTop = pInt(value) + 'px',
|
||||||
leftmargin: (value, node) => node.style.marginLeft = pInt(value) + 'px',
|
leftmargin: (value, node) => node.style.marginLeft = pInt(value) + 'px',
|
||||||
|
|
@ -145,41 +254,65 @@ export const
|
||||||
// td
|
// td
|
||||||
'colspan', 'rowspan', 'headers'
|
'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 = [
|
nonEmptyTags = [
|
||||||
'A','B','EM','I','SPAN','STRONG'
|
'A','B','EM','I','SPAN','STRONG'
|
||||||
];
|
];
|
||||||
|
|
||||||
tpl.innerHTML = html
|
if (SettingsUserStore.allowStyles()) {
|
||||||
|
allowedAttributes.push('class');
|
||||||
|
} else {
|
||||||
|
msgId = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
tmpl.innerHTML = html
|
||||||
// .replace(/<pre[^>]*>[\s\S]*?<\/pre>/gi, pre => pre.replace(/\n/g, '\n<br>'))
|
// .replace(/<pre[^>]*>[\s\S]*?<\/pre>/gi, pre => pre.replace(/\n/g, '\n<br>'))
|
||||||
.replace(/<!doctype[^>]*>/gi, '')
|
|
||||||
.replace(/<\?xml[^>]*\?>/gi, '')
|
|
||||||
// Not supported by <template> element
|
// Not supported by <template> element
|
||||||
|
// .replace(/<!doctype[^>]*>/gi, '')
|
||||||
|
// .replace(/<\?xml[^>]*\?>/gi, '')
|
||||||
.replace(/<(\/?)body(\s[^>]*)?>/gi, '<$1div class="mail-body"$2>')
|
.replace(/<(\/?)body(\s[^>]*)?>/gi, '<$1div class="mail-body"$2>')
|
||||||
.replace(/<\/?(html|head)[^>]*>/gi, '')
|
// .replace(/<\/?(html|head)[^>]*>/gi, '')
|
||||||
// Fix Reddit https://github.com/the-djmaze/snappymail/issues/540
|
// Fix Reddit https://github.com/the-djmaze/snappymail/issues/540
|
||||||
.replace(/<span class="preview-text"[\s\S]+?<\/span>/, '')
|
.replace(/<span class="preview-text"[\s\S]+?<\/span>/, '')
|
||||||
|
// https://github.com/the-djmaze/snappymail/issues/900
|
||||||
|
.replace(/\u2028/g,' ')
|
||||||
.trim();
|
.trim();
|
||||||
html = '';
|
html = '';
|
||||||
|
|
||||||
// \MailSo\Base\HtmlUtils::ClearComments()
|
// Strip all comments
|
||||||
// https://github.com/the-djmaze/snappymail/issues/187
|
const nodeIterator = document.createNodeIterator(tmpl.content, NodeFilter.SHOW_COMMENT);
|
||||||
const nodeIterator = document.createNodeIterator(tpl.content, NodeFilter.SHOW_COMMENT);
|
|
||||||
while (nodeIterator.nextNode()) {
|
while (nodeIterator.nextNode()) {
|
||||||
nodeIterator.referenceNode.remove();
|
nodeIterator.referenceNode.remove();
|
||||||
}
|
}
|
||||||
|
|
||||||
tpl.content.querySelectorAll('*').forEach(oElement => {
|
tmpl.content.querySelectorAll(
|
||||||
|
disallowedTags
|
||||||
|
+ (0 < bqLevel ? ',' + (new Array(1 + bqLevel).fill('blockquote').join(' ')) : '')
|
||||||
|
).forEach(oElement => oElement.remove());
|
||||||
|
|
||||||
|
// https://github.com/the-djmaze/snappymail/issues/1125
|
||||||
|
tmpl.content.querySelectorAll('form,button').forEach(oElement => replaceWithChildren(oElement));
|
||||||
|
|
||||||
|
[...tmpl.content.querySelectorAll('*')].forEach(oElement => {
|
||||||
const name = oElement.tagName,
|
const name = oElement.tagName,
|
||||||
oStyle = oElement.style;
|
oStyle = oElement.style;
|
||||||
|
|
||||||
|
if ('STYLE' === name) {
|
||||||
|
let css = msgId ? parseCSS(oElement.textContent) : [];
|
||||||
|
if (css.length) {
|
||||||
|
css.applyNamespace(msgId);
|
||||||
|
css = css.toString();
|
||||||
|
if (SettingsUserStore.removeColors()) {
|
||||||
|
css = css.replace(/(background-)?color:[^};]+;?/g, '');
|
||||||
|
}
|
||||||
|
oElement.textContent = css;
|
||||||
|
} else {
|
||||||
|
oElement.remove();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// \MailSo\Base\HtmlUtils::ClearTags()
|
// \MailSo\Base\HtmlUtils::ClearTags()
|
||||||
if (disallowedTags.includes(name)
|
if ('none' == oStyle.display
|
||||||
|| 'none' == oStyle.display
|
|
||||||
|| 'hidden' == oStyle.visibility
|
|| 'hidden' == oStyle.visibility
|
||||||
// || (oStyle.lineHeight && 1 > parseFloat(oStyle.lineHeight)
|
// || (oStyle.lineHeight && 1 > parseFloat(oStyle.lineHeight)
|
||||||
// || (oStyle.maxHeight && 1 > parseFloat(oStyle.maxHeight)
|
// || (oStyle.maxHeight && 1 > parseFloat(oStyle.maxHeight)
|
||||||
|
|
@ -189,29 +322,24 @@ export const
|
||||||
oElement.remove();
|
oElement.remove();
|
||||||
return;
|
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 = [],
|
const aAttrsForRemove = [],
|
||||||
|
className = oElement.className,
|
||||||
hasAttribute = name => oElement.hasAttribute(name),
|
hasAttribute = name => oElement.hasAttribute(name),
|
||||||
getAttribute = name => hasAttribute(name) ? oElement.getAttribute(name).trim() : '',
|
getAttribute = name => hasAttribute(name) ? oElement.getAttribute(name).trim() : '',
|
||||||
setAttribute = (name, value) => oElement.setAttribute(name, value),
|
setAttribute = (name, value) => oElement.setAttribute(name, value),
|
||||||
delAttribute = name => oElement.removeAttribute(name);
|
delAttribute = name => {
|
||||||
|
let value = getAttribute(name);
|
||||||
|
oElement.removeAttribute(name);
|
||||||
|
return value;
|
||||||
|
};
|
||||||
|
|
||||||
if ('mail-body' === oElement.className) {
|
if ('mail-body' === className) {
|
||||||
forEachObjectEntry(tasks, (name, cb) => {
|
forEachObjectEntry(tasks, (name, cb) =>
|
||||||
if (hasAttribute(name)) {
|
hasAttribute(name) && cb(delAttribute(name), oElement)
|
||||||
cb(getAttribute(name), oElement);
|
);
|
||||||
delAttribute(name);
|
} else if (msgId && className) {
|
||||||
}
|
oElement.className = className.replace(/(^|\s+)/g, '$1msg-');
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (oElement.hasAttributes()) {
|
if (oElement.hasAttributes()) {
|
||||||
|
|
@ -229,34 +357,26 @@ export const
|
||||||
|
|
||||||
// if ('TABLE' === name || 'TD' === name || 'TH' === name) {
|
// if ('TABLE' === name || 'TD' === name || 'TH' === name) {
|
||||||
if (!oStyle.backgroundImage) {
|
if (!oStyle.backgroundImage) {
|
||||||
value = oStyle.width;
|
|
||||||
if (hasAttribute('width')) {
|
|
||||||
if (!value) {
|
|
||||||
value = getAttribute('width');
|
|
||||||
if (1 < pInt(value)) {
|
|
||||||
oStyle.width = value.includes('%') ? value : value + 'px';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
delAttribute('width');
|
|
||||||
}
|
|
||||||
if (value && !value.includes('%')) {
|
|
||||||
oStyle.maxWidth = value;
|
|
||||||
if ('TD' !== name && 'TH' !== name) {
|
if ('TD' !== name && 'TH' !== name) {
|
||||||
|
['width','height'].forEach(key => {
|
||||||
|
if (hasAttribute(key)) {
|
||||||
|
value = delAttribute(key);
|
||||||
|
oStyle[key] || (oStyle[key] = value.includes('%') ? value : value + 'px');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// Make width responsive
|
||||||
|
value = oStyle.width;
|
||||||
|
if (100 < parseInt(value,10) && !oStyle.maxWidth) {
|
||||||
|
oStyle.maxWidth = value;
|
||||||
oStyle.width = '100%';
|
oStyle.width = '100%';
|
||||||
}
|
}
|
||||||
}
|
// Make height responsive
|
||||||
if (hasAttribute('height')) {
|
|
||||||
value = getAttribute('height');
|
|
||||||
if (pInt(value)) {
|
|
||||||
oStyle.height = value.includes('%') ? value : value + 'px';
|
|
||||||
}
|
|
||||||
delAttribute('height');
|
|
||||||
}
|
|
||||||
value = oStyle.removeProperty('height');
|
value = oStyle.removeProperty('height');
|
||||||
if (value && !value.includes('%')) {
|
if (value && !oStyle.maxHeight) {
|
||||||
oStyle.maxHeight = value;
|
oStyle.maxHeight = value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
// } else
|
// } else
|
||||||
if ('A' === name) {
|
if ('A' === name) {
|
||||||
value = oElement.href;
|
value = oElement.href;
|
||||||
|
|
@ -269,10 +389,11 @@ export const
|
||||||
// setAttribute('rel', 'external nofollow noopener noreferrer');
|
// setAttribute('rel', 'external nofollow noopener noreferrer');
|
||||||
}
|
}
|
||||||
setAttribute('tabindex', '-1');
|
setAttribute('tabindex', '-1');
|
||||||
|
aColor && !oElement.style.color && (oElement.style.color = aColor);
|
||||||
}
|
}
|
||||||
|
|
||||||
// if (['CENTER','FORM'].includes(name)) {
|
// if (['CENTER','FORM'].includes(name)) {
|
||||||
if ('FORM' === name || 'O:P' === name || (nonEmptyTags.includes(name) && ('' == oElement.textContent.trim()))) {
|
if ('O:P' === name || (nonEmptyTags.includes(name) && ('' == oElement.textContent.trim()))) {
|
||||||
('A' !== name || !oElement.querySelector('IMG')) && replaceWithChildren(oElement);
|
('A' !== name || !oElement.querySelector('IMG')) && replaceWithChildren(oElement);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -286,8 +407,7 @@ export const
|
||||||
|
|
||||||
let skipStyle = false;
|
let skipStyle = false;
|
||||||
if (hasAttribute('src')) {
|
if (hasAttribute('src')) {
|
||||||
value = stripTracking(getAttribute('src'));
|
value = stripTracking(delAttribute('src'));
|
||||||
delAttribute('src');
|
|
||||||
|
|
||||||
if ('IMG' === name) {
|
if ('IMG' === name) {
|
||||||
oElement.loading = 'lazy';
|
oElement.loading = 'lazy';
|
||||||
|
|
@ -348,18 +468,15 @@ export const
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasAttribute('background')) {
|
if (hasAttribute('background')) {
|
||||||
oStyle.backgroundImage = 'url("' + getAttribute('background') + '")';
|
oStyle.backgroundImage = 'url("' + delAttribute('background') + '")';
|
||||||
delAttribute('background');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasAttribute('bgcolor')) {
|
if (hasAttribute('bgcolor')) {
|
||||||
oStyle.backgroundColor = getAttribute('bgcolor');
|
oStyle.backgroundColor = delAttribute('bgcolor');
|
||||||
delAttribute('bgcolor');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasAttribute('color')) {
|
if (hasAttribute('color')) {
|
||||||
oStyle.color = getAttribute('color');
|
oStyle.color = delAttribute('color');
|
||||||
delAttribute('color');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!skipStyle) {
|
if (!skipStyle) {
|
||||||
|
|
@ -410,11 +527,12 @@ export const
|
||||||
if (urls_broken.length) {
|
if (urls_broken.length) {
|
||||||
setAttribute('data-x-style-broken-urls', JSON.stringify(urls_broken));
|
setAttribute('data-x-style-broken-urls', JSON.stringify(urls_broken));
|
||||||
}
|
}
|
||||||
|
/*
|
||||||
|
// https://github.com/the-djmaze/snappymail/issues/1082
|
||||||
if (11 > pInt(oStyle.fontSize)) {
|
if (11 > pInt(oStyle.fontSize)) {
|
||||||
oStyle.removeProperty('font-size');
|
oStyle.removeProperty('font-size');
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
// Removes background and color
|
// Removes background and color
|
||||||
// Many e-mails incorrectly only define one, not both
|
// Many e-mails incorrectly only define one, not both
|
||||||
// And in dark theme mode this kills the readability
|
// And in dark theme mode this kills the readability
|
||||||
|
|
@ -424,8 +542,7 @@ export const
|
||||||
oStyle.removeProperty('color');
|
oStyle.removeProperty('color');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Drop Microsoft Office style properties
|
oStyle.cssText = cleanCSS(oStyle.cssText);
|
||||||
// oStyle.cssText = oStyle.cssText.replace(/mso-[^:;]+:[^;]+/gi, '');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (debug && aAttrsForRemove.length) {
|
if (debug && aAttrsForRemove.length) {
|
||||||
|
|
@ -433,8 +550,10 @@ export const
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// return tpl.content.firstChild;
|
blockquoteSwitcher();
|
||||||
result.html = tpl.innerHTML.trim();
|
|
||||||
|
// return tmpl.content.firstChild;
|
||||||
|
result.html = tmpl.innerHTML.trim();
|
||||||
return result;
|
return result;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -445,7 +564,7 @@ export const
|
||||||
htmlToPlain = html => {
|
htmlToPlain = html => {
|
||||||
const
|
const
|
||||||
hr = '⎯'.repeat(64),
|
hr = '⎯'.repeat(64),
|
||||||
forEach = (selector, fn) => tpl.content.querySelectorAll(selector).forEach(fn),
|
forEach = (selector, fn) => tmpl.content.querySelectorAll(selector).forEach(fn),
|
||||||
blockquotes = node => {
|
blockquotes = node => {
|
||||||
let bq;
|
let bq;
|
||||||
while ((bq = node.querySelector('blockquote'))) {
|
while ((bq = node.querySelector('blockquote'))) {
|
||||||
|
|
@ -473,7 +592,7 @@ export const
|
||||||
html = html.replace(/\n*<\/(div|tr)(\s[\s\S]*?)?>\n*/gi, '\n');
|
html = html.replace(/\n*<\/(div|tr)(\s[\s\S]*?)?>\n*/gi, '\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
tpl.innerHTML = html
|
tmpl.innerHTML = html
|
||||||
.replace(/<t[dh](\s[\s\S]*?)?>/gi, '\t')
|
.replace(/<t[dh](\s[\s\S]*?)?>/gi, '\t')
|
||||||
.replace(/<\/tr(\s[\s\S]*?)?>/gi, '\n');
|
.replace(/<\/tr(\s[\s\S]*?)?>/gi, '\n');
|
||||||
|
|
||||||
|
|
@ -523,16 +642,16 @@ export const
|
||||||
forEach('i,em', i => i.replaceWith(`*${i.textContent}*`));
|
forEach('i,em', i => i.replaceWith(`*${i.textContent}*`));
|
||||||
|
|
||||||
// Convert line-breaks
|
// Convert line-breaks
|
||||||
tpl.innerHTML = tpl.innerHTML
|
tmpl.innerHTML = tmpl.innerHTML
|
||||||
.replace(/\n{3,}/gm, '\n\n')
|
.replace(/\n{3,}/gm, '\n\n')
|
||||||
.replace(/\n<br[^>]*>/g, '\n')
|
.replace(/\n<br[^>]*>/g, '\n')
|
||||||
.replace(/<br[^>]*>\n/g, '\n');
|
.replace(/<br[^>]*>\n/g, '\n');
|
||||||
forEach('br', br => br.replaceWith('\n'));
|
forEach('br', br => br.replaceWith('\n'));
|
||||||
|
|
||||||
// Blockquotes must be last
|
// Blockquotes must be last
|
||||||
blockquotes(tpl.content);
|
blockquotes(tmpl.content);
|
||||||
|
|
||||||
return (tpl.content.textContent || '').trim();
|
return (tmpl.content.textContent || '').trim();
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -543,7 +662,9 @@ export const
|
||||||
plainToHtml = plain => {
|
plainToHtml = plain => {
|
||||||
plain = plain.toString()
|
plain = plain.toString()
|
||||||
.replace(/\r/g, '')
|
.replace(/\r/g, '')
|
||||||
.replace(/^>[> ]>+/gm, ([match]) => (match ? match.replace(/[ ]+/g, '') : match));
|
.replace(/^>[> ]>+/gm, ([match]) => (match ? match.replace(/[ ]+/g, '') : match))
|
||||||
|
// https://github.com/the-djmaze/snappymail/issues/900
|
||||||
|
.replace(/\u2028/g,' ');
|
||||||
|
|
||||||
let bIn = false,
|
let bIn = false,
|
||||||
bDo = true,
|
bDo = true,
|
||||||
|
|
@ -584,21 +705,40 @@ export const
|
||||||
aText = aNextText;
|
aText = aNextText;
|
||||||
} while (bDo);
|
} while (bDo);
|
||||||
|
|
||||||
return aText.join('\n')
|
tmpl.innerHTML = aText.join('\n')
|
||||||
// .replace(/~~~\/blockquote~~~\n~~~blockquote~~~/g, '\n')
|
// .replace(/~~~\/blockquote~~~\n~~~blockquote~~~/g, '\n')
|
||||||
.replace(/&/g, '&')
|
.replace(/&/g, '&')
|
||||||
.replace(/>/g, '>')
|
.replace(/>/g, '>')
|
||||||
.replace(/</g, '<')
|
.replace(/</g, '<')
|
||||||
.replace(url, (...m) => {
|
.replace(urlRegExp, (...m) => {
|
||||||
m[2] = stripTracking(m[2]);
|
m[0] = stripTracking(m[0]);
|
||||||
return `${m[1]}<a href="${m[2]}" target="_blank">${m[2]}</a>`;
|
return `<a href="${m[0]}" target="_blank">${m[0]}</a>`;
|
||||||
})
|
})
|
||||||
.replace(email, '$1<a href="mailto:$2">$2</a>')
|
.replace(email, '$1<a href="mailto:$2">$2</a>')
|
||||||
.replace(tel, '$1<a href="$2">$2</a>')
|
.replace(tel, '<a href="$1">$1</a>')
|
||||||
.replace(/~~~blockquote~~~\s*/g, '<blockquote>')
|
.replace(/~~~blockquote~~~\s*/g, '<blockquote>')
|
||||||
.replace(/\s*~~~\/blockquote~~~/g, '</blockquote>')
|
.replace(/\s*~~~\/blockquote~~~/g, '</blockquote>')
|
||||||
.replace(/\n/g, '<br>');
|
.replace(/\n/g, '<br>');
|
||||||
};
|
blockquoteSwitcher();
|
||||||
|
return tmpl.innerHTML.trim();
|
||||||
|
},
|
||||||
|
|
||||||
|
WYSIWYGS = ko.observableArray();
|
||||||
|
|
||||||
|
WYSIWYGS.push(['Squire', (owner, container, onReady)=>{
|
||||||
|
let squire = new SquireUI(container);
|
||||||
|
setTimeout(()=>onReady(squire), 1);
|
||||||
|
/*
|
||||||
|
squire.on('blur', () => owner.blurTrigger());
|
||||||
|
squire.on('focus', () => clearTimeout(owner.blurTimer));
|
||||||
|
squire.on('mode', () => {
|
||||||
|
owner.blurTrigger();
|
||||||
|
owner.onModeChange?.(!owner.isPlain());
|
||||||
|
});
|
||||||
|
*/
|
||||||
|
}]);
|
||||||
|
|
||||||
|
rl.registerWYSIWYG = (name, construct) => WYSIWYGS.push([name, construct]);
|
||||||
|
|
||||||
export class HtmlEditor {
|
export class HtmlEditor {
|
||||||
/**
|
/**
|
||||||
|
|
@ -614,30 +754,23 @@ export class HtmlEditor {
|
||||||
this.onModeChange = onModeChange;
|
this.onModeChange = onModeChange;
|
||||||
|
|
||||||
if (element) {
|
if (element) {
|
||||||
let editor;
|
|
||||||
|
|
||||||
onReady = onReady ? [onReady] : [];
|
onReady = onReady ? [onReady] : [];
|
||||||
this.onReady = fn => onReady.push(fn);
|
this.onReady = fn => onReady.push(fn);
|
||||||
const readyCallback = () => {
|
// TODO: make 'which' user configurable
|
||||||
|
// const which = 'CKEditor4',
|
||||||
|
// wysiwyg = WYSIWYGS.find(item => which == item[0]) || WYSIWYGS.find(item => 'Squire' == item[0]);
|
||||||
|
const wysiwyg = WYSIWYGS.find(item => 'Squire' == item[0]);
|
||||||
|
wysiwyg[1](this, element, editor => {
|
||||||
this.editor = editor;
|
this.editor = editor;
|
||||||
this.onReady = fn => fn();
|
|
||||||
onReady.forEach(fn => fn());
|
|
||||||
};
|
|
||||||
|
|
||||||
if (rl.createWYSIWYG) {
|
|
||||||
editor = rl.createWYSIWYG(element, readyCallback);
|
|
||||||
}
|
|
||||||
if (!editor) {
|
|
||||||
editor = new SquireUI(element);
|
|
||||||
setTimeout(readyCallback, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
editor.on('blur', () => this.blurTrigger());
|
editor.on('blur', () => this.blurTrigger());
|
||||||
editor.on('focus', () => this.blurTimer && clearTimeout(this.blurTimer));
|
editor.on('focus', () => clearTimeout(this.blurTimer));
|
||||||
editor.on('mode', () => {
|
editor.on('mode', () => {
|
||||||
this.blurTrigger();
|
this.blurTrigger();
|
||||||
this.onModeChange?.(!this.isPlain());
|
this.onModeChange?.(!this.isPlain());
|
||||||
});
|
});
|
||||||
|
this.onReady = fn => fn();
|
||||||
|
onReady.forEach(fn => fn());
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
import { pInt } from 'Common/Utils';
|
import { pInt } from 'Common/Utils';
|
||||||
import { Settings } from 'Common/Globals';
|
import { doc, Settings } from 'Common/Globals';
|
||||||
|
|
||||||
const
|
const
|
||||||
|
BASE = doc.location.pathname.replace(/\/+$/,'') + '/',
|
||||||
HASH_PREFIX = '#/',
|
HASH_PREFIX = '#/',
|
||||||
SERVER_PREFIX = './?',
|
|
||||||
|
|
||||||
adminPath = () => rl.adminArea() && !Settings.app('adminHostUse'),
|
adminPath = () => rl.adminArea() && !Settings.app('adminHost'),
|
||||||
|
|
||||||
prefix = () => SERVER_PREFIX + (adminPath() ? Settings.app('adminPath') : '');
|
prefix = () => BASE + '?' + (adminPath() ? Settings.app('adminPath') : '');
|
||||||
|
|
||||||
export const
|
export const
|
||||||
SUB_QUERY_PREFIX = '&q[]=',
|
SUB_QUERY_PREFIX = '&q[]=',
|
||||||
|
|
@ -21,7 +21,7 @@ export const
|
||||||
/**
|
/**
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
logoutLink = () => adminPath() ? prefix() : './',
|
logoutLink = () => adminPath() ? prefix() : BASE,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} type
|
* @param {string} type
|
||||||
|
|
@ -30,8 +30,8 @@ export const
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
serverRequestRaw = (type, hash) =>
|
serverRequestRaw = (type, hash) =>
|
||||||
SERVER_PREFIX + '/Raw/' + SUB_QUERY_PREFIX + '/'
|
BASE + '?/Raw/' + SUB_QUERY_PREFIX + '/'
|
||||||
+ '0/' // Settings.get('AccountHash') ?
|
+ '0/' // Settings.get('accountHash') ?
|
||||||
+ (type
|
+ (type
|
||||||
? type + '/' + (hash ? SUB_QUERY_PREFIX + '/' + hash : '')
|
? type + '/' + (hash ? SUB_QUERY_PREFIX + '/' + hash : '')
|
||||||
: ''),
|
: ''),
|
||||||
|
|
@ -45,8 +45,9 @@ export const
|
||||||
serverRequestRaw('Download', download, customSpecSuffix),
|
serverRequestRaw('Download', download, customSpecSuffix),
|
||||||
|
|
||||||
proxy = url =>
|
proxy = url =>
|
||||||
SERVER_PREFIX + '/ProxyExternal/'
|
BASE + '?/ProxyExternal/'
|
||||||
+ btoa(url.replace(/ /g, '%20')).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''),
|
+ btoa(url.replace(/ /g, '%20')).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''),
|
||||||
|
// + b64EncodeJSONSafe(url.replace(/ /g, '%20')),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} type
|
* @param {string} type
|
||||||
|
|
@ -54,13 +55,16 @@ export const
|
||||||
*/
|
*/
|
||||||
serverRequest = type => prefix() + '/' + type + '/' + SUB_QUERY_PREFIX + '/0/',
|
serverRequest = type => prefix() + '/' + type + '/' + SUB_QUERY_PREFIX + '/0/',
|
||||||
|
|
||||||
|
// Is '?/Css/0/Admin' needed?
|
||||||
|
cssLink = theme => BASE + '?/Css/0/User/-/' + encodeURI(theme) + '/-/' + Date.now() + '/Hash/-/Json/',
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} lang
|
* @param {string} lang
|
||||||
* @param {boolean} isAdmin
|
* @param {boolean} isAdmin
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
langLink = (lang, isAdmin) =>
|
langLink = (lang, isAdmin) =>
|
||||||
SERVER_PREFIX + '/Lang/0/' + (isAdmin ? 'Admin' : 'App')
|
BASE + '?/Lang/0/' + (isAdmin ? 'Admin' : 'App')
|
||||||
+ '/' + encodeURI(lang)
|
+ '/' + encodeURI(lang)
|
||||||
+ '/' + Settings.app('version') + '/',
|
+ '/' + Settings.app('version') + '/',
|
||||||
|
|
||||||
|
|
@ -75,6 +79,10 @@ export const
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
themePreviewLink = theme => {
|
themePreviewLink = theme => {
|
||||||
|
if (theme.endsWith('@nextcloud')) {
|
||||||
|
theme = theme.slice(0, theme.length - 10).trim();
|
||||||
|
return parent.OC.webroot + '/themes/' + encodeURI(theme) + '/snappymail/preview.png';
|
||||||
|
}
|
||||||
let path = 'webVersionPath';
|
let path = 'webVersionPath';
|
||||||
if (theme.endsWith('@custom')) {
|
if (theme.endsWith('@custom')) {
|
||||||
theme = theme.slice(0, theme.length - 7).trim();
|
theme = theme.slice(0, theme.length - 7).trim();
|
||||||
|
|
|
||||||
|
|
@ -87,8 +87,7 @@ export class Selector {
|
||||||
* Below code is used to keep checked/focused/selected states when array is refreshed.
|
* Below code is used to keep checked/focused/selected states when array is refreshed.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
let aCache = [],
|
let aCheckedCache = [],
|
||||||
aCheckedCache = [],
|
|
||||||
mFocused = null,
|
mFocused = null,
|
||||||
mSelected = null;
|
mSelected = null;
|
||||||
|
|
||||||
|
|
@ -99,7 +98,6 @@ export class Selector {
|
||||||
items.forEach(item => {
|
items.forEach(item => {
|
||||||
const uid = this.getItemUid(item);
|
const uid = this.getItemUid(item);
|
||||||
if (uid) {
|
if (uid) {
|
||||||
aCache.push(uid);
|
|
||||||
item.checked() && aCheckedCache.push(uid);
|
item.checked() && aCheckedCache.push(uid);
|
||||||
if (!mFocused && item.focused()) {
|
if (!mFocused && item.focused()) {
|
||||||
mFocused = uid;
|
mFocused = uid;
|
||||||
|
|
@ -118,8 +116,7 @@ export class Selector {
|
||||||
koList.subscribe(aItems => {
|
koList.subscribe(aItems => {
|
||||||
selectedItemUseCallback = false;
|
selectedItemUseCallback = false;
|
||||||
|
|
||||||
koFocusedItem(null);
|
this.unselect();
|
||||||
koSelectedItem(null);
|
|
||||||
|
|
||||||
if (isArray(aItems)) {
|
if (isArray(aItems)) {
|
||||||
let temp,
|
let temp,
|
||||||
|
|
@ -182,7 +179,6 @@ export class Selector {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
aCache = [];
|
|
||||||
aCheckedCache = [];
|
aCheckedCache = [];
|
||||||
mFocused = null;
|
mFocused = null;
|
||||||
mSelected = null;
|
mSelected = null;
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
import ko from 'ko';
|
import ko from 'ko';
|
||||||
import { Notification, UploadErrorCode } from 'Common/Enums';
|
import { Notifications, 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';
|
import { getKeyByValue, forEachObjectEntry } from 'Common/Utils';
|
||||||
import { pInt } from 'Common/Utils';
|
import { pInt } from 'Common/Utils';
|
||||||
|
import { LanguageStore } from 'Stores/Language';
|
||||||
|
|
||||||
let I18N_DATA = {};
|
let I18N_DATA = {};
|
||||||
|
|
||||||
|
|
@ -11,8 +12,8 @@ const
|
||||||
init = () => {
|
init = () => {
|
||||||
if (rl.I18N) {
|
if (rl.I18N) {
|
||||||
I18N_DATA = rl.I18N;
|
I18N_DATA = rl.I18N;
|
||||||
Date.defineRelativeTimeFormat(rl.relativeTime || {});
|
|
||||||
rl.I18N = null;
|
rl.I18N = null;
|
||||||
|
doc.documentElement.dir = I18N_DATA.LANG_DIR;
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -20,13 +21,40 @@ const
|
||||||
i18nKey = key => key.replace(/([a-z])([A-Z])/g, '$1_$2').toUpperCase(),
|
i18nKey = key => key.replace(/([a-z])([A-Z])/g, '$1_$2').toUpperCase(),
|
||||||
|
|
||||||
getNotificationMessage = code => {
|
getNotificationMessage = code => {
|
||||||
let key = getKeyByValue(Notification, code);
|
let key = getKeyByValue(Notifications, code);
|
||||||
return key ? I18N_DATA.NOTIFICATIONS[i18nKey(key).replace('_NOTIFICATION', '_ERROR')] : '';
|
return key ? I18N_DATA.NOTIFICATIONS[i18nKey(key).replace('_NOTIFICATION', '_ERROR')] : '';
|
||||||
};
|
},
|
||||||
|
|
||||||
|
fromNow = date => relativeTime(Math.round((date.getTime() - Date.now()) / 1000));
|
||||||
|
|
||||||
export const
|
export const
|
||||||
translateTrigger = ko.observable(false),
|
translateTrigger = ko.observable(false),
|
||||||
|
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/RelativeTimeFormat
|
||||||
|
// see /snappymail/v/0.0.0/app/localization/relativetimeformat/
|
||||||
|
relativeTime = seconds => {
|
||||||
|
let unit = 'second',
|
||||||
|
t = [[60,'minute'],[3600,'hour'],[86400,'day'],[2628000,'month'],[31536000,'year']],
|
||||||
|
i = 5,
|
||||||
|
abs = Math.abs(seconds);
|
||||||
|
while (i--) {
|
||||||
|
if (t[i][0] <= abs) {
|
||||||
|
seconds = Math.round(seconds / t[i][0]);
|
||||||
|
unit = t[i][1];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (Intl.RelativeTimeFormat) {
|
||||||
|
let rtf = new Intl.RelativeTimeFormat(doc.documentElement.lang);
|
||||||
|
return rtf.format(seconds, unit);
|
||||||
|
}
|
||||||
|
// Safari < 14
|
||||||
|
abs = Math.abs(seconds);
|
||||||
|
let rtf = rl.relativeTime.long[unit][0 > seconds ? 'past' : 'future'],
|
||||||
|
plural = rl.relativeTime.plural(abs);
|
||||||
|
return (rtf[plural] || rtf).replace('{0}', abs);
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} key
|
* @param {string} key
|
||||||
* @param {Object=} valueList
|
* @param {Object=} valueList
|
||||||
|
|
@ -77,27 +105,31 @@ export const
|
||||||
time = 0 < timeStampInUTC ? Math.min(now, timeStampInUTC * 1000) : (0 === timeStampInUTC ? now : 0);
|
time = 0 < timeStampInUTC ? Math.min(now, timeStampInUTC * 1000) : (0 === timeStampInUTC ? now : 0);
|
||||||
|
|
||||||
if (31536000000 < time) {
|
if (31536000000 < time) {
|
||||||
const m = new Date(time);
|
const m = new Date(time), h = LanguageStore.hourCycle();
|
||||||
switch (formatStr) {
|
switch (formatStr) {
|
||||||
case 'FROMNOW':
|
case 'FROMNOW':
|
||||||
return m.fromNow();
|
return fromNow(m);
|
||||||
case 'SHORT': {
|
case 'AUTO': {
|
||||||
if (4 >= (now - time) / 3600000)
|
// 4 hours
|
||||||
return m.fromNow();
|
if (14400000 >= now - time)
|
||||||
const mt = m.getTime(), date = new Date,
|
return fromNow(m);
|
||||||
|
const date = new Date,
|
||||||
dt = date.setHours(0,0,0,0);
|
dt = date.setHours(0,0,0,0);
|
||||||
if (mt > dt)
|
return (time > dt - 86400000)
|
||||||
return i18n('MESSAGE_LIST/TODAY_AT', {TIME: m.format('LT')});
|
? i18n(
|
||||||
if (mt > dt - 86400000)
|
time > dt ? 'MESSAGE_LIST/TODAY_AT' : 'MESSAGE_LIST/YESTERDAY_AT',
|
||||||
return i18n('MESSAGE_LIST/YESTERDAY_AT', {TIME: m.format('LT')});
|
{TIME: m.format('LT',0,h)}
|
||||||
if (date.getFullYear() === m.getFullYear())
|
)
|
||||||
return m.format('d M');
|
: m.format(
|
||||||
return m.format('LL');
|
date.getFullYear() === m.getFullYear()
|
||||||
|
? {day: '2-digit', month: 'short', hour: 'numeric', minute: 'numeric'}
|
||||||
|
: {dateStyle: 'medium', timeStyle: 'short'}
|
||||||
|
, 0, h);
|
||||||
}
|
}
|
||||||
case 'FULL':
|
case 'FULL':
|
||||||
return m.format('LLL');
|
return m.format('LLL',0,h);
|
||||||
default:
|
default:
|
||||||
return m.format(formatStr);
|
return m.format(formatStr,0,h);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -107,7 +139,7 @@ export const
|
||||||
timeToNode = (element, time) => {
|
timeToNode = (element, time) => {
|
||||||
try {
|
try {
|
||||||
if (time) {
|
if (time) {
|
||||||
element.dateTime = (new Date(time * 1000)).format('Y-m-d\\TH:i:s');
|
element.dateTime = new Date(time * 1000).toISOString();
|
||||||
} else {
|
} else {
|
||||||
time = Date.parse(element.dateTime) / 1000;
|
time = Date.parse(element.dateTime) / 1000;
|
||||||
}
|
}
|
||||||
|
|
@ -115,10 +147,9 @@ export const
|
||||||
let key = element.dataset.momentFormat;
|
let key = element.dataset.momentFormat;
|
||||||
if (key) {
|
if (key) {
|
||||||
element.textContent = timestampToString(time, key);
|
element.textContent = timestampToString(time, key);
|
||||||
|
if ('FULL' !== key && 'FROMNOW' !== key) {
|
||||||
|
element.title = timestampToString(time, 'FULL');
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((key = element.dataset.momentFormatTitle)) {
|
|
||||||
element.title = timestampToString(time, key);
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// prevent knockout crashes
|
// prevent knockout crashes
|
||||||
|
|
@ -126,9 +157,7 @@ export const
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
reloadTime = () => setTimeout(() =>
|
reloadTime = () => doc.querySelectorAll('time').forEach(element => timeToNode(element)),
|
||||||
doc.querySelectorAll('time').forEach(element => timeToNode(element))
|
|
||||||
, 1),
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Function} startCallback
|
* @param {Function} startCallback
|
||||||
|
|
@ -148,7 +177,7 @@ export const
|
||||||
*/
|
*/
|
||||||
getNotification = (code, message = '', defCode = 0) => {
|
getNotification = (code, message = '', defCode = 0) => {
|
||||||
code = pInt(code);
|
code = pInt(code);
|
||||||
if (Notification.ClientViewError === code && message) {
|
if (Notifications.ClientViewError === code && message) {
|
||||||
return message;
|
return message;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -170,15 +199,15 @@ export const
|
||||||
* @param {boolean} admin
|
* @param {boolean} admin
|
||||||
* @param {string} language
|
* @param {string} language
|
||||||
*/
|
*/
|
||||||
translatorReload = (admin, language) =>
|
translatorReload = (language, admin) =>
|
||||||
new Promise((resolve, reject) => {
|
new Promise((resolve, reject) => {
|
||||||
const script = createElement('script');
|
const script = createElement('script');
|
||||||
script.onload = () => {
|
script.onload = () => {
|
||||||
// reload the data
|
// reload the data
|
||||||
if (init()) {
|
if (init()) {
|
||||||
i18nToNodes(doc);
|
i18nToNodes(doc);
|
||||||
admin || reloadTime();
|
|
||||||
translateTrigger(!translateTrigger());
|
translateTrigger(!translateTrigger());
|
||||||
|
// admin || reloadTime();
|
||||||
}
|
}
|
||||||
script.remove();
|
script.remove();
|
||||||
resolve();
|
resolve();
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,3 @@
|
||||||
import { SaveSettingStatus } from 'Common/Enums';
|
|
||||||
import { elementById } from 'Common/Globals';
|
|
||||||
|
|
||||||
let __themeTimer = 0,
|
|
||||||
__themeJson = null;
|
|
||||||
|
|
||||||
export const
|
export const
|
||||||
isArray = Array.isArray,
|
isArray = Array.isArray,
|
||||||
arrayLength = array => isArray(array) && array.length,
|
arrayLength = array => isArray(array) && array.length,
|
||||||
|
|
@ -19,52 +13,19 @@ export const
|
||||||
return isNaN(value) || !isFinite(value) ? defaultValue : value;
|
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) =>
|
defaultOptionsAfterRender = (domItem, item) =>
|
||||||
item && undefined !== item.disabled && domItem?.classList.toggle('disabled', domItem.disabled = item.disabled),
|
item && undefined !== item.disabled && domItem?.classList.toggle('disabled', domItem.disabled = item.disabled),
|
||||||
|
|
||||||
// unescape(encodeURIComponent()) makes the UTF-16 DOMString to an UTF-8 string
|
// unescape(encodeURIComponent()) makes the UTF-16 DOMString to an UTF-8 string
|
||||||
b64EncodeJSON = data => btoa(unescape(encodeURIComponent(JSON.stringify(data)))),
|
b64Encode = data => btoa(unescape(encodeURIComponent(data))),
|
||||||
/* // Without deprecated 'unescape':
|
/* // Without deprecated 'unescape':
|
||||||
b64EncodeJSON = data => btoa(encodeURIComponent(JSON.stringify(data)).replace(
|
b64Encode = data => btoa(encodeURIComponent(data).replace(
|
||||||
/%([0-9A-F]{2})/g, (match, p1) => String.fromCharCode('0x' + p1)
|
/%([0-9A-F]{2})/g, (match, p1) => String.fromCharCode('0x' + p1)
|
||||||
)),
|
)),
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
b64EncodeJSON = data => b64Encode(JSON.stringify(data)),
|
||||||
|
|
||||||
b64EncodeJSONSafe = data => b64EncodeJSON(data).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''),
|
b64EncodeJSONSafe = data => b64EncodeJSON(data).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''),
|
||||||
|
|
||||||
changeTheme = (value, themeTrigger = ()=>0) => {
|
|
||||||
const themeStyle = elementById('app-theme-style'),
|
|
||||||
clearTimer = () => {
|
|
||||||
__themeTimer = setTimeout(() => themeTrigger(SaveSettingStatus.Idle), 1000);
|
|
||||||
__themeJson = null;
|
|
||||||
},
|
|
||||||
url = themeStyle.dataset.href.replace(/(Admin|User)\/-\/[^/]+\//, '$1/-/' + value + '/') + 'Json/';
|
|
||||||
|
|
||||||
clearTimeout(__themeTimer);
|
|
||||||
|
|
||||||
themeTrigger(SaveSettingStatus.Saving);
|
|
||||||
|
|
||||||
if (__themeJson) {
|
|
||||||
__themeJson.abort();
|
|
||||||
}
|
|
||||||
let init = {};
|
|
||||||
if (window.AbortController) {
|
|
||||||
__themeJson = new AbortController();
|
|
||||||
init.signal = __themeJson.signal;
|
|
||||||
}
|
|
||||||
rl.fetchJSON(url, init)
|
|
||||||
.then(data => {
|
|
||||||
if (2 === arrayLength(data)) {
|
|
||||||
themeStyle.textContent = data[1];
|
|
||||||
themeTrigger(SaveSettingStatus.Success);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.then(clearTimer, clearTimer);
|
|
||||||
},
|
|
||||||
|
|
||||||
getKeyByValue = (o, v) => Object.keys(o).find(key => o[key] === v);
|
getKeyByValue = (o, v) => Object.keys(o).find(key => o[key] === v);
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,21 @@
|
||||||
import { MessageFlagsCache } from 'Common/Cache';
|
import { Notifications } from 'Common/Enums';
|
||||||
import { Notification } from 'Common/Enums';
|
|
||||||
import { MessageSetAction, ComposeType/*, FolderType*/ } from 'Common/EnumsUser';
|
import { MessageSetAction, ComposeType/*, FolderType*/ } from 'Common/EnumsUser';
|
||||||
import { doc, createElement, elementById, dropdowns, dropdownVisibility, SettingsGet } from 'Common/Globals';
|
import { doc, createElement, elementById, dropdowns, dropdownVisibility, SettingsGet, leftPanelDisabled } from 'Common/Globals';
|
||||||
import { plainToHtml } from 'Common/Html';
|
import { plainToHtml } from 'Common/Html';
|
||||||
import { getNotification } from 'Common/Translator';
|
import { getNotification } from 'Common/Translator';
|
||||||
import { EmailModel } from 'Model/Email';
|
import { EmailCollectionModel } from 'Model/EmailCollection';
|
||||||
import { MessageModel } from 'Model/Message';
|
|
||||||
import { MessageUserStore } from 'Stores/User/Message';
|
import { MessageUserStore } from 'Stores/User/Message';
|
||||||
import { MessagelistUserStore } from 'Stores/User/Messagelist';
|
import { MessagelistUserStore } from 'Stores/User/Messagelist';
|
||||||
import { SettingsUserStore } from 'Stores/User/Settings';
|
import { SettingsUserStore } from 'Stores/User/Settings';
|
||||||
import * as Local from 'Storage/Client';
|
import * as Local from 'Storage/Client';
|
||||||
import { ThemeStore } from 'Stores/Theme';
|
import { ThemeStore } from 'Stores/Theme';
|
||||||
import Remote from 'Remote/User/Fetch';
|
import Remote from 'Remote/User/Fetch';
|
||||||
|
import { attachmentDownload } from 'Common/Links';
|
||||||
|
|
||||||
export const
|
export const
|
||||||
|
|
||||||
|
moveAction = ko.observable(false),
|
||||||
|
|
||||||
dropdownsDetectVisibility = (() =>
|
dropdownsDetectVisibility = (() =>
|
||||||
dropdownVisibility(!!dropdowns.find(item => item.classList.contains('show')))
|
dropdownVisibility(!!dropdowns.find(item => item.classList.contains('show')))
|
||||||
).debounce(50),
|
).debounce(50),
|
||||||
|
|
@ -40,6 +41,29 @@ download = (link, name = "") => {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
downloadZip = (name, hashes, onError, fTrigger, folder) => {
|
||||||
|
if (hashes.length) {
|
||||||
|
let params = {
|
||||||
|
target: 'zip',
|
||||||
|
filename: name,
|
||||||
|
hashes: hashes
|
||||||
|
};
|
||||||
|
if (!onError) {
|
||||||
|
onError = () => alert('Download failed');
|
||||||
|
}
|
||||||
|
if (folder) {
|
||||||
|
params.folder = folder;
|
||||||
|
// params.uids = uids;
|
||||||
|
}
|
||||||
|
Remote.post('AttachmentsActions', fTrigger || null, params)
|
||||||
|
.then(result => {
|
||||||
|
let hash = result?.Result?.fileHash;
|
||||||
|
hash ? download(attachmentDownload(hash), hash+'.zip') : onError();
|
||||||
|
})
|
||||||
|
.catch(onError);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns {function}
|
* @returns {function}
|
||||||
*/
|
*/
|
||||||
|
|
@ -48,13 +72,14 @@ computedPaginatorHelper = (koCurrentPage, koPageCount) => {
|
||||||
const currentPage = koCurrentPage(),
|
const currentPage = koCurrentPage(),
|
||||||
pageCount = koPageCount(),
|
pageCount = koPageCount(),
|
||||||
result = [],
|
result = [],
|
||||||
|
lang = doc.documentElement.lang,
|
||||||
fAdd = (index, push = true, customName = '') => {
|
fAdd = (index, push = true, customName = '') => {
|
||||||
const data = {
|
const name = index.toLocaleString(lang),
|
||||||
|
data = {
|
||||||
current: index === currentPage,
|
current: index === currentPage,
|
||||||
name: customName ? customName.toString() : index.toString(),
|
name: customName || name,
|
||||||
custom: !!customName,
|
title: customName ? name : '',
|
||||||
title: customName ? index.toString() : '',
|
value: index
|
||||||
value: index.toString()
|
|
||||||
};
|
};
|
||||||
|
|
||||||
push ? result.push(data) : result.unshift(data);
|
push ? result.push(data) : result.unshift(data);
|
||||||
|
|
@ -131,29 +156,15 @@ mailToHelper = mailToUrl => {
|
||||||
mailToUrl = mailToUrl.slice(7).split('?');
|
mailToUrl = mailToUrl.slice(7).split('?');
|
||||||
|
|
||||||
const
|
const
|
||||||
email = mailToUrl[0],
|
email = decodeURIComponent(mailToUrl[0]),
|
||||||
params = new URLSearchParams(mailToUrl[1]),
|
params = new URLSearchParams(mailToUrl[1]),
|
||||||
toEmailModel = value => null != value ? EmailModel.parseEmailLine(value) : null;
|
to = params.get('to'),
|
||||||
|
toEmailModel = value => EmailCollectionModel.fromString(value);
|
||||||
|
|
||||||
showMessageComposer([
|
showMessageComposer([
|
||||||
ComposeType.Empty,
|
ComposeType.Empty,
|
||||||
null,
|
null,
|
||||||
params.get('to')
|
toEmailModel(to ? email + ',' + to : email),
|
||||||
? Object.values(
|
|
||||||
toEmailModel(email + ',' + params.get('to')).reduce((result, value) => {
|
|
||||||
if (value) {
|
|
||||||
if (result[value.email]) {
|
|
||||||
if (!result[value.email].name) {
|
|
||||||
result[value.email] = value;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
result[value.email] = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}, {})
|
|
||||||
)
|
|
||||||
: EmailModel.parseEmailLine(email),
|
|
||||||
toEmailModel(params.get('cc')),
|
toEmailModel(params.get('cc')),
|
||||||
toEmailModel(params.get('bcc')),
|
toEmailModel(params.get('bcc')),
|
||||||
params.get('subject'),
|
params.get('subject'),
|
||||||
|
|
@ -171,37 +182,24 @@ showMessageComposer = (params = []) =>
|
||||||
rl.app.showMessageComposer(params);
|
rl.app.showMessageComposer(params);
|
||||||
},
|
},
|
||||||
|
|
||||||
setLayoutResizer = (source, target, sClientSideKeyName, mode) =>
|
setLayoutResizer = (source, sClientSideKeyName, mode) =>
|
||||||
{
|
{
|
||||||
if (source.layoutResizer && source.layoutResizer.mode != mode) {
|
if (source.layoutResizer && source.layoutResizer.mode != mode) {
|
||||||
target.removeAttribute('style');
|
|
||||||
source.removeAttribute('style');
|
source.removeAttribute('style');
|
||||||
}
|
}
|
||||||
source.observer?.disconnect();
|
source.observer?.disconnect();
|
||||||
// source.classList.toggle('resizable', mode);
|
// source.classList.toggle('resizable', mode);
|
||||||
if (mode) {
|
if (mode) {
|
||||||
const length = Local.get(sClientSideKeyName + mode) || SettingsGet('Resizer' + sClientSideKeyName + mode),
|
const length = Local.get(sClientSideKeyName + mode) || SettingsGet('Resizer' + sClientSideKeyName + mode);
|
||||||
setTargetPos = mode => {
|
|
||||||
let value;
|
|
||||||
if ('Width' == mode) {
|
|
||||||
value = source.offsetWidth;
|
|
||||||
target.style.left = value + 'px';
|
|
||||||
} else {
|
|
||||||
value = source.offsetHeight;
|
|
||||||
target.style.top = (4 + source.offsetTop + value) + 'px';
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
};
|
|
||||||
if (length) {
|
if (length) {
|
||||||
source.style[mode.toLowerCase()] = length + 'px';
|
source.style[mode.toLowerCase()] = length + 'px';
|
||||||
setTargetPos(mode);
|
|
||||||
}
|
}
|
||||||
if (!source.layoutResizer) {
|
if (!source.layoutResizer) {
|
||||||
const resizer = createElement('div', {'class':'resizer'}),
|
const resizer = createElement('div', {'class':'resizer'}),
|
||||||
save = (data => Remote.saveSettings(0, data)).debounce(500),
|
save = (data => Remote.saveSettings(0, data)).debounce(500),
|
||||||
size = {},
|
size = {},
|
||||||
store = () => {
|
store = () => {
|
||||||
const value = setTargetPos(resizer.mode),
|
const value = ('Width' == resizer.mode) ? source.offsetWidth : source.offsetHeight,
|
||||||
prop = resizer.key + resizer.mode;
|
prop = resizer.key + resizer.mode;
|
||||||
(value == Local.get(prop)) || Local.set(prop, value);
|
(value == Local.get(prop)) || Local.set(prop, value);
|
||||||
(value == SettingsGet('Resizer' + prop)) || save({['Resizer' + prop]: value});
|
(value == SettingsGet('Resizer' + prop)) || save({['Resizer' + prop]: value});
|
||||||
|
|
@ -248,111 +246,80 @@ setLayoutResizer = (source, target, sClientSideKeyName, mode) =>
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
populateMessageBody = (oMessage, popup) => {
|
viewMessage = (oMessage, popup) => {
|
||||||
if (oMessage) {
|
if (popup) {
|
||||||
popup || MessageUserStore.message(oMessage);
|
oMessage.viewPopupMessage();
|
||||||
popup || MessageUserStore.loading(true);
|
|
||||||
Remote.message((iError, oData/*, bCached*/) => {
|
|
||||||
if (iError) {
|
|
||||||
if (Notification.RequestAborted !== iError && !popup) {
|
|
||||||
MessageUserStore.message(null);
|
|
||||||
MessageUserStore.error(getNotification(iError));
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
let json = oData?.Result;
|
MessageUserStore.error('');
|
||||||
|
let id = 'rl-msg-' + oMessage.hash,
|
||||||
if (
|
body = oMessage.body || elementById(id);
|
||||||
json &&
|
if (!body) {
|
||||||
MessageModel.validJson(json) &&
|
body = createElement('div',{
|
||||||
oMessage.folder === json.Folder
|
id:id,
|
||||||
) {
|
hidden:'',
|
||||||
const threads = oMessage.threads(),
|
class:'b-text-part'
|
||||||
isNew = !popup && oMessage.uid != json.Uid && threads.includes(json.Uid),
|
|
||||||
messagesDom = MessageUserStore.bodiesDom();
|
|
||||||
if (isNew) {
|
|
||||||
oMessage = MessageModel.reviveFromJson(json);
|
|
||||||
if (oMessage) {
|
|
||||||
oMessage.threads(threads);
|
|
||||||
MessageFlagsCache.initMessage(oMessage);
|
|
||||||
|
|
||||||
// Set clone
|
|
||||||
oMessage = MessageModel.fromMessageListItem(oMessage);
|
|
||||||
}
|
|
||||||
MessageUserStore.message(oMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (oMessage && oMessage.uid == json.Uid) {
|
|
||||||
popup || MessageUserStore.error('');
|
|
||||||
/*
|
|
||||||
if (bCached) {
|
|
||||||
delete json.Flags;
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
isNew || oMessage.revivePropertiesFromJson(json);
|
|
||||||
|
|
||||||
if (messagesDom) {
|
|
||||||
let id = 'rl-msg-' + oMessage.hash.replace(/[^a-zA-Z0-9]/g, ''),
|
|
||||||
body = elementById(id);
|
|
||||||
if (body) {
|
|
||||||
oMessage.body = body;
|
|
||||||
oMessage.isHtml(body.classList.contains('html'));
|
|
||||||
oMessage.hasImages(body.rlHasImages);
|
|
||||||
} else {
|
|
||||||
body = Element.fromHTML('<div id="' + id + '" hidden="" class="b-text-part '
|
|
||||||
+ (oMessage.pgpSigned() ? ' openpgp-signed' : '')
|
+ (oMessage.pgpSigned() ? ' openpgp-signed' : '')
|
||||||
+ (oMessage.pgpEncrypted() ? ' openpgp-encrypted' : '')
|
+ (oMessage.pgpEncrypted() ? ' openpgp-encrypted' : '')
|
||||||
+ '">'
|
});
|
||||||
+ '</div>');
|
MessageUserStore.purgeCache();
|
||||||
|
}
|
||||||
|
|
||||||
|
body.message = oMessage;
|
||||||
oMessage.body = body;
|
oMessage.body = body;
|
||||||
|
|
||||||
if (!SettingsUserStore.viewHTML() || !oMessage.viewHtml()) {
|
if (!SettingsUserStore.viewHTML() || !oMessage.viewHtml()) {
|
||||||
oMessage.viewPlain();
|
oMessage.viewPlain();
|
||||||
}
|
}
|
||||||
|
|
||||||
MessageUserStore.purgeMessageBodyCache();
|
MessageUserStore.bodiesDom().append(body);
|
||||||
}
|
|
||||||
|
|
||||||
messagesDom.append(body);
|
MessageUserStore.loading(false);
|
||||||
|
oMessage.body.hidden = false;
|
||||||
|
|
||||||
popup || (oMessage.body.hidden = false);
|
|
||||||
popup && oMessage.viewPopupMessage();
|
|
||||||
}
|
|
||||||
|
|
||||||
MessageFlagsCache.initMessage(oMessage);
|
|
||||||
if (oMessage.isUnseen()) {
|
if (oMessage.isUnseen()) {
|
||||||
MessageUserStore.MessageSeenTimer = setTimeout(
|
MessageUserStore.MessageSeenTimer = setTimeout(
|
||||||
() => MessagelistUserStore.setAction(oMessage.folder, MessageSetAction.SetSeen, [oMessage]),
|
() => MessagelistUserStore.setAction(oMessage.folder, MessageSetAction.SetSeen, [oMessage]),
|
||||||
SettingsUserStore.messageReadDelay() * 1000 // seconds
|
SettingsUserStore.messageReadDelay() * 1000 // seconds
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
if (isNew) {
|
populateMessageBody = (oMessage, popup) => {
|
||||||
let selectedMessage = MessagelistUserStore.selectedMessage();
|
if (oMessage) {
|
||||||
if (
|
popup || MessageUserStore.message(oMessage);
|
||||||
selectedMessage &&
|
if (oMessage.body) {
|
||||||
(oMessage.folder !== selectedMessage.folder || oMessage.uid != selectedMessage.uid)
|
viewMessage(oMessage, popup);
|
||||||
|
} else {
|
||||||
|
popup || MessageUserStore.loading(true);
|
||||||
|
Remote.message((iError, oData/*, bCached*/) => {
|
||||||
|
if (iError) {
|
||||||
|
if (Notifications.RequestAborted !== iError && !popup) {
|
||||||
|
MessageUserStore.message(null);
|
||||||
|
MessageUserStore.error(getNotification(iError));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let json = oData?.Result;
|
||||||
|
if (json
|
||||||
|
&& oMessage.hash === json.hash
|
||||||
|
// && oMessage.folder === json.folder
|
||||||
|
// && oMessage.uid == json.uid
|
||||||
|
&& oMessage.revivePropertiesFromJson(json)
|
||||||
) {
|
) {
|
||||||
MessagelistUserStore.selectedMessage(null);
|
/*
|
||||||
if (1 === MessagelistUserStore.length) {
|
if (bCached) {
|
||||||
MessagelistUserStore.focusedMessage(null);
|
delete json.flags;
|
||||||
}
|
|
||||||
} else if (!selectedMessage) {
|
|
||||||
selectedMessage = MessagelistUserStore.find(
|
|
||||||
subMessage =>
|
|
||||||
subMessage &&
|
|
||||||
subMessage.folder === oMessage.folder &&
|
|
||||||
subMessage.uid == oMessage.uid
|
|
||||||
);
|
|
||||||
|
|
||||||
if (selectedMessage) {
|
|
||||||
MessagelistUserStore.selectedMessage(selectedMessage);
|
|
||||||
MessagelistUserStore.focusedMessage(selectedMessage);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
oMessage.body.remove();
|
||||||
|
*/
|
||||||
|
viewMessage(oMessage, popup);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
popup || MessageUserStore.loading(false);
|
popup || MessageUserStore.loading(false);
|
||||||
}, oMessage.folder, oMessage.uid);
|
}, oMessage.folder, oMessage.uid);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
leftPanelDisabled.subscribe(value => value && moveAction(false));
|
||||||
|
moveAction.subscribe(value => value && leftPanelDisabled(false));
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
export class CheckboxComponent {
|
export class CheckboxComponent {
|
||||||
constructor(params = {}) {
|
constructor(params = {}) {
|
||||||
|
this.name = params.name;
|
||||||
|
|
||||||
this.value = ko.isObservable(params.value) ? params.value
|
this.value = ko.isObservable(params.value) ? params.value
|
||||||
: ko.observable(!!params.value);
|
: ko.observable(!!params.value);
|
||||||
|
|
||||||
|
|
@ -7,7 +9,6 @@ export class CheckboxComponent {
|
||||||
: ko.observable(undefined === params.enable || !!params.enable);
|
: ko.observable(undefined === params.enable || !!params.enable);
|
||||||
|
|
||||||
this.label = params.label;
|
this.label = params.label;
|
||||||
this.inline = params.inline;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
click() {
|
click() {
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,29 @@
|
||||||
import { doc, createElement, addEventsListeners } from 'Common/Globals';
|
import { doc, createElement, addEventsListeners } from 'Common/Globals';
|
||||||
import { EmailModel } from 'Model/Email';
|
import { EmailModel, addressparser } from 'Model/Email';
|
||||||
|
|
||||||
const contentType = 'snappymail/emailaddress',
|
const contentType = 'snappymail/emailaddress',
|
||||||
getAddressKey = li => li?.emailaddress?.key;
|
getAddressKey = li => li?.emailaddress?.key,
|
||||||
|
|
||||||
|
parseEmailLine = line => addressparser(line).map(item =>
|
||||||
|
(item.name || item.email)
|
||||||
|
? new EmailModel(item.email, item.name) : null
|
||||||
|
).filter(v => v),
|
||||||
|
splitEmailLine = line => {
|
||||||
|
const result = [];
|
||||||
|
let exists = false;
|
||||||
|
addressparser(line).forEach(item => {
|
||||||
|
const address = (item.name || item.email)
|
||||||
|
? new EmailModel(item.email, item.name)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
if (address?.email) {
|
||||||
|
exists = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
result.push(address ? address.toLine() : item.name);
|
||||||
|
});
|
||||||
|
return exists ? result : null;
|
||||||
|
};
|
||||||
|
|
||||||
let dragAddress, datalist;
|
let dragAddress, datalist;
|
||||||
|
|
||||||
|
|
@ -154,8 +175,8 @@ export class EmailAddressesComponent {
|
||||||
if (val) {
|
if (val) {
|
||||||
const self = this,
|
const self = this,
|
||||||
v = val.trim(),
|
v = val.trim(),
|
||||||
hook = (v && [',', ';', '\n'].includes(v.slice(-1))) ? EmailModel.splitEmailLine(val) : null,
|
hook = (v && [',', ';', '\n'].includes(v.slice(-1))) ? splitEmailLine(val) : null,
|
||||||
values = (hook || [val]).map(value => EmailModel.parseEmailLine(value))
|
values = (hook || [val]).map(value => parseEmailLine(value))
|
||||||
.flat(Infinity)
|
.flat(Infinity)
|
||||||
.map(item => (item.toLine ? [item.toLine(), item] : [item, null]));
|
.map(item => (item.toLine ? [item.toLine(), item] : [item, null]));
|
||||||
|
|
||||||
|
|
|
||||||
248
dev/External/SquireUI.js
vendored
248
dev/External/SquireUI.js
vendored
|
|
@ -14,12 +14,13 @@ const
|
||||||
createElement = name => doc.createElement(name),
|
createElement = name => doc.createElement(name),
|
||||||
|
|
||||||
tpl = createElement('template'),
|
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(),
|
||||||
htmlToPlain = html => rl.Utils.htmlToPlain(html).trim(),
|
htmlToPlain = html => rl.Utils.htmlToPlain(html).trim(),
|
||||||
plainToHtml = text => rl.Utils.plainToHtml(text),
|
plainToHtml = text => rl.Utils.plainToHtml(text),
|
||||||
|
|
||||||
|
forEachObjectValue = (obj, fn) => Object.values(obj).forEach(fn),
|
||||||
|
|
||||||
getFragmentOfChildren = parent => {
|
getFragmentOfChildren = parent => {
|
||||||
let frag = doc.createDocumentFragment();
|
let frag = doc.createDocumentFragment();
|
||||||
frag.append(...parent.childNodes);
|
frag.append(...parent.childNodes);
|
||||||
|
|
@ -28,11 +29,6 @@ const
|
||||||
|
|
||||||
SquireDefaultConfig = {
|
SquireDefaultConfig = {
|
||||||
/*
|
/*
|
||||||
blockTag: 'P',
|
|
||||||
undo: {
|
|
||||||
documentSizeThreshold: -1, // -1 means no threshold
|
|
||||||
undoLimit: -1 // -1 means no limit
|
|
||||||
},
|
|
||||||
addLinks: true // allow_smart_html_links
|
addLinks: true // allow_smart_html_links
|
||||||
*/
|
*/
|
||||||
sanitizeToDOMFragment: (html, isPaste/*, squire*/) => {
|
sanitizeToDOMFragment: (html, isPaste/*, squire*/) => {
|
||||||
|
|
@ -61,18 +57,19 @@ const
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
clr.type = "color";
|
|
||||||
clr.style.display = 'none';
|
|
||||||
doc.body.append(clr);
|
|
||||||
|
|
||||||
class SquireUI
|
class SquireUI
|
||||||
{
|
{
|
||||||
constructor(container) {
|
constructor(container) {
|
||||||
const
|
const
|
||||||
doClr = name => () => {
|
clr = createElement('input'),
|
||||||
|
doClr = name => input => {
|
||||||
|
// https://github.com/the-djmaze/snappymail/issues/826
|
||||||
|
clr.style.left = (input.offsetLeft + input.parentNode.offsetLeft) + 'px';
|
||||||
|
clr.style.width = input.offsetWidth + 'px';
|
||||||
|
|
||||||
clr.value = '';
|
clr.value = '';
|
||||||
clr.onchange = () => squire.setStyle({[name]:clr.value});
|
clr.onchange = () => squire.setStyle({[name]:clr.value});
|
||||||
clr.click();
|
setTimeout(()=>clr.click(),1);
|
||||||
},
|
},
|
||||||
|
|
||||||
actions = {
|
actions = {
|
||||||
|
|
@ -84,8 +81,7 @@ class SquireUI
|
||||||
[i18n('SETTINGS_GENERAL/EDITOR_HTML'),'wysiwyg'],
|
[i18n('SETTINGS_GENERAL/EDITOR_HTML'),'wysiwyg'],
|
||||||
[i18n('SETTINGS_GENERAL/EDITOR_PLAIN'),'plain']
|
[i18n('SETTINGS_GENERAL/EDITOR_PLAIN'),'plain']
|
||||||
],
|
],
|
||||||
cmd: s => this.setMode('plain' == s.value ? 'plain' : 'wysiwyg'),
|
cmd: s => this.setMode('plain' == s.value ? 'plain' : 'wysiwyg')
|
||||||
hint: i18n('EDITOR/TEXT_SWITCHER_PLAIN_TEXT', 'Plain')
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
font: {
|
font: {
|
||||||
|
|
@ -112,19 +108,44 @@ class SquireUI
|
||||||
},
|
},
|
||||||
fontSize: {
|
fontSize: {
|
||||||
select: ['11px','13px','16px','20px','24px','30px'],
|
select: ['11px','13px','16px','20px','24px','30px'],
|
||||||
|
defaultValueIndex: 2,
|
||||||
cmd: s => squire.setStyle({ fontSize: s.value })
|
cmd: s => squire.setStyle({ fontSize: s.value })
|
||||||
|
// TODO: maybe consider using https://developer.mozilla.org/en-US/docs/Web/CSS/font-size#values
|
||||||
|
// example:
|
||||||
|
// select: ['xx-small', 'x-small',' small',' medium', 'large', 'x-large', 'xx-large', 'xxx-large'],
|
||||||
|
// defaultValueIndex: 3,
|
||||||
|
},
|
||||||
|
// dir: {
|
||||||
|
// select: [
|
||||||
|
// [i18n('EDITOR/DIR_LTR', 'LTR'),'ltr'],
|
||||||
|
// [i18n('EDITOR/DIR_RTL', 'RTL'),'rtl'],
|
||||||
|
// [i18n('EDITOR/DIR_AUTO', 'Auto'),'auto'],
|
||||||
|
// ['',''],
|
||||||
|
// ],
|
||||||
|
// cmd: s => {
|
||||||
|
// squire.setAttribute('dir', s.value || null);
|
||||||
|
// // squire.setStyle({ 'unicode-bidi': 'plaintext' });
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
},
|
||||||
|
dir: {
|
||||||
|
dir_ltr: {
|
||||||
|
html: '⁋',
|
||||||
|
cmd: () => squire.bidi('ltr')
|
||||||
|
},
|
||||||
|
dir_rtl: {
|
||||||
|
html: '¶',
|
||||||
|
cmd: () => squire.bidi('rtl')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
colors: {
|
colors: {
|
||||||
textColor: {
|
textColor: {
|
||||||
html: 'A<sub>▾</sub>',
|
html: 'A<sub>▾</sub>',
|
||||||
cmd: doClr('color'),
|
cmd: doClr('color')
|
||||||
hint: 'Text color'
|
|
||||||
},
|
},
|
||||||
backgroundColor: {
|
backgroundColor: {
|
||||||
html: '🎨', /* ▧ */
|
html: '🎨', /* ▧ */
|
||||||
cmd: doClr('backgroundColor'),
|
cmd: doClr('backgroundColor')
|
||||||
hint: 'Background color'
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
inline: {
|
inline: {
|
||||||
|
|
@ -132,37 +153,37 @@ class SquireUI
|
||||||
html: 'B',
|
html: 'B',
|
||||||
cmd: () => this.doAction('bold'),
|
cmd: () => this.doAction('bold'),
|
||||||
key: 'B',
|
key: 'B',
|
||||||
hint: 'Bold'
|
matches: 'B,STRONT'
|
||||||
},
|
},
|
||||||
italic: {
|
italic: {
|
||||||
html: 'I',
|
html: 'I',
|
||||||
cmd: () => this.doAction('italic'),
|
cmd: () => this.doAction('italic'),
|
||||||
key: 'I',
|
key: 'I',
|
||||||
hint: 'Italic'
|
matches: 'I'
|
||||||
},
|
},
|
||||||
underline: {
|
underline: {
|
||||||
html: '<u>U</u>',
|
html: '<u>U</u>',
|
||||||
cmd: () => this.doAction('underline'),
|
cmd: () => this.doAction('underline'),
|
||||||
key: 'U',
|
key: 'U',
|
||||||
hint: 'Underline'
|
matches: 'U'
|
||||||
},
|
},
|
||||||
strike: {
|
strike: {
|
||||||
html: '<s>S</s>',
|
html: '<s>S</s>',
|
||||||
cmd: () => this.doAction('strikethrough'),
|
cmd: () => this.doAction('strikethrough'),
|
||||||
key: 'Shift + 7',
|
key: 'Shift + 7',
|
||||||
hint: 'Strikethrough'
|
matches: 'S'
|
||||||
},
|
},
|
||||||
sub: {
|
sub: {
|
||||||
html: 'Xₙ',
|
html: 'Xₙ',
|
||||||
cmd: () => this.doAction('subscript'),
|
cmd: () => this.doAction('subscript'),
|
||||||
key: 'Shift + 5',
|
key: 'Shift + 5',
|
||||||
hint: 'Subscript'
|
matches: 'SUB'
|
||||||
},
|
},
|
||||||
sup: {
|
sup: {
|
||||||
html: 'Xⁿ',
|
html: 'Xⁿ',
|
||||||
cmd: () => this.doAction('superscript'),
|
cmd: () => this.doAction('superscript'),
|
||||||
key: 'Shift + 6',
|
key: 'Shift + 6',
|
||||||
hint: 'Superscript'
|
matches: 'SUP'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
block: {
|
block: {
|
||||||
|
|
@ -170,13 +191,13 @@ class SquireUI
|
||||||
html: '#',
|
html: '#',
|
||||||
cmd: () => this.doList('OL'),
|
cmd: () => this.doList('OL'),
|
||||||
key: 'Shift + 8',
|
key: 'Shift + 8',
|
||||||
hint: 'Ordered list'
|
matches: 'OL'
|
||||||
},
|
},
|
||||||
ul: {
|
ul: {
|
||||||
html: '⋮',
|
html: '⋮',
|
||||||
cmd: () => this.doList('UL'),
|
cmd: () => this.doList('UL'),
|
||||||
key: 'Shift + 9',
|
key: 'Shift + 9',
|
||||||
hint: 'Unordered list'
|
matches: 'UL'
|
||||||
},
|
},
|
||||||
quote: {
|
quote: {
|
||||||
html: '"',
|
html: '"',
|
||||||
|
|
@ -184,19 +205,17 @@ class SquireUI
|
||||||
let parent = squire.getSelectionClosest('UL,OL,BLOCKQUOTE')?.nodeName;
|
let parent = squire.getSelectionClosest('UL,OL,BLOCKQUOTE')?.nodeName;
|
||||||
('BLOCKQUOTE' == parent) ? squire.decreaseQuoteLevel() : squire.increaseQuoteLevel();
|
('BLOCKQUOTE' == parent) ? squire.decreaseQuoteLevel() : squire.increaseQuoteLevel();
|
||||||
},
|
},
|
||||||
hint: 'Blockquote'
|
matches: 'BLOCKQUOTE'
|
||||||
},
|
},
|
||||||
indentDecrease: {
|
indentDecrease: {
|
||||||
html: '⇤',
|
html: '⇤',
|
||||||
cmd: () => squire.changeIndentationLevel('decrease'),
|
cmd: () => squire.changeIndentationLevel('decrease'),
|
||||||
key: ']',
|
key: ']'
|
||||||
hint: 'Decrease indent'
|
|
||||||
},
|
},
|
||||||
indentIncrease: {
|
indentIncrease: {
|
||||||
html: '⇥',
|
html: '⇥',
|
||||||
cmd: () => squire.changeIndentationLevel('increase'),
|
cmd: () => squire.changeIndentationLevel('increase'),
|
||||||
key: '[',
|
key: '['
|
||||||
hint: 'Increase indent'
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
targets: {
|
targets: {
|
||||||
|
|
@ -209,21 +228,21 @@ class SquireUI
|
||||||
url.length ? squire.makeLink(url) : (node && squire.removeLink());
|
url.length ? squire.makeLink(url) : (node && squire.removeLink());
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
hint: 'Link'
|
matches: 'A'
|
||||||
},
|
},
|
||||||
imageUrl: {
|
imageUrl: {
|
||||||
html: '🖼️',
|
html: '🖼️',
|
||||||
cmd: () => {
|
cmd: () => {
|
||||||
let node = squire.getSelectionClosest('IMG'),
|
let node = squire.getSelectionClosest('IMG'),
|
||||||
src = prompt("Image", node?.src || "https://");
|
src = prompt("Image", node?.src || "https://");
|
||||||
src.length ? squire.insertImage(src) : (node && squire.detach(node));
|
src?.length ? squire.insertImage(src) : (node && squire.detach(node));
|
||||||
},
|
},
|
||||||
hint: 'Image URL'
|
matches: 'IMG'
|
||||||
},
|
},
|
||||||
imageUpload: {
|
imageUpload: {
|
||||||
html: '📂️',
|
html: '📂️',
|
||||||
cmd: () => browseImage.click(),
|
cmd: () => browseImage.click(),
|
||||||
hint: 'Image select',
|
matches: 'IMG'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
/*
|
/*
|
||||||
|
|
@ -235,19 +254,19 @@ class SquireUI
|
||||||
undo: {
|
undo: {
|
||||||
html: '↶',
|
html: '↶',
|
||||||
cmd: () => squire.undo(),
|
cmd: () => squire.undo(),
|
||||||
key: 'Z',
|
key: 'Z'
|
||||||
hint: 'Undo'
|
|
||||||
},
|
},
|
||||||
redo: {
|
redo: {
|
||||||
html: '↷',
|
html: '↷',
|
||||||
cmd: () => squire.redo(),
|
cmd: () => squire.redo(),
|
||||||
key: 'Y',
|
key: 'Y'
|
||||||
hint: 'Redo'
|
|
||||||
},
|
},
|
||||||
source: {
|
source: {
|
||||||
html: '👁',
|
html: '👁',
|
||||||
cmd: () => this.setMode('source' == this.mode ? 'wysiwyg' : 'source'),
|
cmd: btn => {
|
||||||
hint: i18n('EDITOR/TEXT_SWITCHER_SOURCE', 'Source')
|
this.setMode('source' == this.mode ? 'wysiwyg' : 'source');
|
||||||
|
btn.classList.toggle('active', 'source' == this.mode);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -258,6 +277,9 @@ class SquireUI
|
||||||
browseImage = createElement('input'),
|
browseImage = createElement('input'),
|
||||||
squire = new Squire(wysiwyg, SquireDefaultConfig);
|
squire = new Squire(wysiwyg, SquireDefaultConfig);
|
||||||
|
|
||||||
|
clr.type = 'color';
|
||||||
|
toolbar.append(clr);
|
||||||
|
|
||||||
browseImage.type = 'file';
|
browseImage.type = 'file';
|
||||||
browseImage.accept = 'image/*';
|
browseImage.accept = 'image/*';
|
||||||
browseImage.style.display = 'none';
|
browseImage.style.display = 'none';
|
||||||
|
|
@ -271,6 +293,7 @@ class SquireUI
|
||||||
|
|
||||||
plain.className = 'squire-plain';
|
plain.className = 'squire-plain';
|
||||||
wysiwyg.className = 'squire-wysiwyg';
|
wysiwyg.className = 'squire-wysiwyg';
|
||||||
|
wysiwyg.dir = 'auto';
|
||||||
this.mode = ''; // 'plain' | 'wysiwyg'
|
this.mode = ''; // 'plain' | 'wysiwyg'
|
||||||
this.__plain = {
|
this.__plain = {
|
||||||
getRawData: () => this.plain.value,
|
getRawData: () => this.plain.value,
|
||||||
|
|
@ -282,14 +305,11 @@ class SquireUI
|
||||||
this.plain = plain;
|
this.plain = plain;
|
||||||
this.wysiwyg = wysiwyg;
|
this.wysiwyg = wysiwyg;
|
||||||
|
|
||||||
|
dispatchEvent(new CustomEvent('squire-toolbar', {detail:{squire:this,actions:actions}}));
|
||||||
|
|
||||||
toolbar.className = 'squire-toolbar btn-toolbar';
|
toolbar.className = 'squire-toolbar btn-toolbar';
|
||||||
let group, action/*, touchTap*/;
|
let group, action/*, touchTap*/;
|
||||||
for (group in actions) {
|
for (group in actions) {
|
||||||
/*
|
|
||||||
if ('bidi' == group && !rl.settings.app('allowHtmlEditorBitiButtons')) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
let toolgroup = createElement('div');
|
let toolgroup = createElement('div');
|
||||||
toolgroup.className = 'btn-group';
|
toolgroup.className = 'btn-group';
|
||||||
toolgroup.id = 'squire-toolgroup-'+group;
|
toolgroup.id = 'squire-toolgroup-'+group;
|
||||||
|
|
@ -342,6 +362,7 @@ class SquireUI
|
||||||
*/
|
*/
|
||||||
}
|
}
|
||||||
input.addEventListener(ev, () => cfg.cmd(input));
|
input.addEventListener(ev, () => cfg.cmd(input));
|
||||||
|
cfg.hint = i18n('EDITOR/' + action.toUpperCase());
|
||||||
if (cfg.hint) {
|
if (cfg.hint) {
|
||||||
input.title = cfg.key ? cfg.hint + ' (' + ctrlKey + cfg.key + ')' : cfg.hint;
|
input.title = cfg.key ? cfg.hint + ' (' + ctrlKey + cfg.key + ')' : cfg.hint;
|
||||||
} else if (cfg.key) {
|
} else if (cfg.key) {
|
||||||
|
|
@ -364,21 +385,127 @@ class SquireUI
|
||||||
changes.redo.input.disabled = !state.canRedo;
|
changes.redo.input.disabled = !state.canRedo;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
actions.font.fontSize.input.selectedIndex = actions.font.fontSize.defaultValueIndex;
|
||||||
|
|
||||||
// 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);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} fontName
|
||||||
|
* @return {string}
|
||||||
|
*/
|
||||||
|
const normalizeFontName = (fontName) => fontName.trim().replace(/(^["']*|["']*$)/g, '').trim().toLowerCase();
|
||||||
|
|
||||||
|
/** @type {string[]} - lower cased array of available font families*/
|
||||||
|
const fontFamiliesLowerCase = Object.values(actions.font.fontFamily.input.options).map(option => option.value.toLowerCase());
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A theme might have CSS like div.squire-wysiwyg[contenteditable="true"] {
|
||||||
|
* font-family: 'Times New Roman', Times, serif; }
|
||||||
|
* so let's find the best match squire.getRoot()'s font
|
||||||
|
* it will also help to properly handle generic font names like 'sans-serif'
|
||||||
|
* @type {number}
|
||||||
|
*/
|
||||||
|
let defaultFontFamilyIndex = 0;
|
||||||
|
const squireRootFonts = getComputedStyle(squire.getRoot()).fontFamily.split(',').map(normalizeFontName);
|
||||||
|
fontFamiliesLowerCase.some((family, index) => {
|
||||||
|
const matchFound = family.split(',').some(availableFontName => {
|
||||||
|
const normalizedFontName = normalizeFontName(availableFontName);
|
||||||
|
return squireRootFonts.some(squireFontName => squireFontName === normalizedFontName);
|
||||||
|
});
|
||||||
|
if (matchFound) {
|
||||||
|
defaultFontFamilyIndex = index;
|
||||||
|
}
|
||||||
|
return matchFound;
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instead of comparing whole 'font-family' strings,
|
||||||
|
* we are going to look for individual font names, because we might be
|
||||||
|
* editing a Draft started in another email client for example
|
||||||
|
*
|
||||||
|
* @type {Object.<string,number>}
|
||||||
|
*/
|
||||||
|
const fontNamesMap = {};
|
||||||
|
/**
|
||||||
|
* @param {string} fontFamily
|
||||||
|
* @param {number} index
|
||||||
|
*/
|
||||||
|
const processFontFamilyString = (fontFamily, index) => {
|
||||||
|
fontFamily.split(',').forEach(fontName => {
|
||||||
|
const key = normalizeFontName(fontName);
|
||||||
|
if (fontNamesMap[key] === undefined) {
|
||||||
|
fontNamesMap[key] = index;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
// first deal with the default font family
|
||||||
|
processFontFamilyString(fontFamiliesLowerCase[defaultFontFamilyIndex], defaultFontFamilyIndex);
|
||||||
|
// and now with the rest of the font families
|
||||||
|
fontFamiliesLowerCase.forEach((fontFamily, index) => {
|
||||||
|
if (index !== defaultFontFamilyIndex) {
|
||||||
|
processFontFamilyString(fontFamily, index);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// -----
|
||||||
|
|
||||||
|
squire.addEventListener('pathChange', e => {
|
||||||
|
|
||||||
|
const squireRoot = squire.getRoot();
|
||||||
|
|
||||||
|
forEachObjectValue(actions, entries => {
|
||||||
|
forEachObjectValue(entries, cfg => {
|
||||||
|
// cfg.matches && cfg.input.classList.toggle('active', e.element && e.element.matches(cfg.matches));
|
||||||
|
cfg.matches && cfg.input.classList.toggle('active', e.element && e.element.closestWithin(cfg.matches, squireRoot));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (e.element) {
|
||||||
|
// try to find font-family and/or font-size and set "select" elements' values
|
||||||
|
|
||||||
|
let sizeSelectedIndex = actions.font.fontSize.defaultValueIndex;
|
||||||
|
let familySelectedIndex = defaultFontFamilyIndex;
|
||||||
|
|
||||||
|
let elm = e.element;
|
||||||
|
let familyFound = false;
|
||||||
|
let sizeFound = false;
|
||||||
|
do {
|
||||||
|
if (!familyFound && elm.style.fontFamily) {
|
||||||
|
familyFound = true;
|
||||||
|
familySelectedIndex = -1; // show empty select if we don't know the font
|
||||||
|
const fontNames = elm.style.fontFamily.split(',');
|
||||||
|
for (let i = 0; i < fontNames.length; i++) {
|
||||||
|
const index = fontNamesMap[normalizeFontName(fontNames[i])];
|
||||||
|
if (index !== undefined) {
|
||||||
|
familySelectedIndex = index;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!sizeFound && elm.style.fontSize) {
|
||||||
|
sizeFound = true;
|
||||||
|
// -1 is ok because it will just show a black <select>
|
||||||
|
sizeSelectedIndex = actions.font.fontSize.select.indexOf(elm.style.fontSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
elm = elm.parentElement;
|
||||||
|
} while ((!familyFound || !sizeFound) && elm && elm !== squireRoot);
|
||||||
|
|
||||||
|
actions.font.fontFamily.input.selectedIndex = familySelectedIndex;
|
||||||
|
actions.font.fontSize.input.selectedIndex = sizeSelectedIndex;
|
||||||
|
}
|
||||||
|
});
|
||||||
/*
|
/*
|
||||||
squire.addEventListener('dragover', );
|
squire.addEventListener('cursor', e => {
|
||||||
squire.addEventListener('drop', );
|
console.dir({cursor:e.range});
|
||||||
squire.addEventListener('pathChange', );
|
});
|
||||||
squire.addEventListener('cursor', );
|
squire.addEventListener('select', e => {
|
||||||
squire.addEventListener('select', );
|
console.dir({select:e.range});
|
||||||
squire.addEventListener('input', );
|
});
|
||||||
squire.addEventListener('willPaste', );
|
|
||||||
squire.addEventListener( 'keydown keyup', monitorShiftKey )
|
|
||||||
squire.addEventListener( 'keydown', onKey )
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// CKEditor gimmicks used by HtmlEditor
|
// CKEditor gimmicks used by HtmlEditor
|
||||||
|
|
@ -401,11 +528,11 @@ class SquireUI
|
||||||
fn = {UL:'makeUnorderedList',OL:'makeOrderedList'};
|
fn = {UL:'makeUnorderedList',OL:'makeOrderedList'};
|
||||||
(parent == type) ? this.squire.removeList() : this.squire[fn[type]]();
|
(parent == type) ? this.squire.removeList() : this.squire[fn[type]]();
|
||||||
}
|
}
|
||||||
|
/*
|
||||||
testPresenceinSelection(format, validation) {
|
testPresenceinSelection(format, validation) {
|
||||||
return validation.test(this.squire.getPath()) || this.squire.hasFormat(format);
|
return validation.test(this.squire.getPath()) || this.squire.hasFormat(format);
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
setMode(mode) {
|
setMode(mode) {
|
||||||
if (this.mode != mode) {
|
if (this.mode != mode) {
|
||||||
let cl = this.container.classList, source = 'source' == this.mode;
|
let cl = this.container.classList, source = 'source' == this.mode;
|
||||||
|
|
@ -493,7 +620,12 @@ class SquireUI
|
||||||
}
|
}
|
||||||
|
|
||||||
focus() {
|
focus() {
|
||||||
('plain' == this.mode ? this.plain : this.squire).focus();
|
if ('plain' == this.mode) {
|
||||||
|
this.plain.focus();
|
||||||
|
this.plain.setSelectionRange(0, 0);
|
||||||
|
} else {
|
||||||
|
this.squire.focus();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
42
dev/External/User/ko.js
vendored
42
dev/External/User/ko.js
vendored
|
|
@ -2,7 +2,7 @@ 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/Translator';
|
import { timeToNode } from 'Common/Translator';
|
||||||
import { doc, elementById, addEventsListeners, dropdowns } from 'Common/Globals';
|
import { doc, elementById, addEventsListeners, dropdowns, leftPanelDisabled } from 'Common/Globals';
|
||||||
import { dropdownsDetectVisibility } from 'Common/UtilsUser';
|
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';
|
||||||
|
|
@ -29,37 +29,39 @@ const rlContentType = 'snappymail/action',
|
||||||
},
|
},
|
||||||
|
|
||||||
dragTimer = {
|
dragTimer = {
|
||||||
id: 0,
|
id: 0
|
||||||
stop: () => clearTimeout(dragTimer.id),
|
|
||||||
start: fn => dragTimer.id = setTimeout(fn, 500)
|
|
||||||
},
|
},
|
||||||
|
|
||||||
fnStop = (e, element) => {
|
dragStop = (e, element) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
element.classList.remove('droppableHover');
|
element?.classList.remove('droppableHover');
|
||||||
dragTimer.stop();
|
if (dragTimer.node == element) {
|
||||||
|
dragTimer.node = null;
|
||||||
|
clearTimeout(dragTimer.id);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
fnHover = (e, element, folder) => {
|
dragEnter = (e, element, folder) => {
|
||||||
let files = false;
|
let files = false;
|
||||||
// if (e.dataTransfer.types.includes('Files'))
|
// if (e.dataTransfer.types.includes('Files'))
|
||||||
for (const item of e.dataTransfer.items) {
|
for (const item of e.dataTransfer.items) {
|
||||||
files |= 'file' === item.kind && 'message/rfc822' === item.type;
|
files |= 'file' === item.kind && 'message/rfc822' === item.type;
|
||||||
}
|
}
|
||||||
if (files || dragMessages()) {
|
if (files || dragMessages()) {
|
||||||
fnStop(e, element);
|
e.stopPropagation();
|
||||||
files && e.stopPropagation();
|
dragStop(e, dragTimer.node);
|
||||||
e.dataTransfer.dropEffect = files ? 'copy' : (e.ctrlKey ? 'copy' : 'move');
|
e.dataTransfer.dropEffect = files ? 'copy' : (e.ctrlKey ? 'copy' : 'move');
|
||||||
element.classList.add('droppableHover');
|
element.classList.add('droppableHover');
|
||||||
if (folder.collapsed()) {
|
if (folder.collapsed()) {
|
||||||
dragTimer.start(() => {
|
dragTimer.node = element;
|
||||||
|
dragTimer.id = setTimeout(() => {
|
||||||
folder.collapsed(false);
|
folder.collapsed(false);
|
||||||
setExpandedFolder(folder.fullName, true);
|
setExpandedFolder(folder.fullName, true);
|
||||||
}, 500);
|
}, 500);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
fnDrop = (e, element, folder, dragData) => {
|
dragDrop = (e, element, folder, dragData) => {
|
||||||
fnStop(e, element);
|
dragStop(e, element);
|
||||||
if (dragMessages() && 'copyMove' == e.dataTransfer.effectAllowed) {
|
if (dragMessages() && 'copyMove' == e.dataTransfer.effectAllowed) {
|
||||||
moveMessagesToFolder(FolderUserStore.currentFolderFullName(), dragData.data, folder.fullName, e.ctrlKey);
|
moveMessagesToFolder(FolderUserStore.currentFolderFullName(), dragData.data, folder.fullName, e.ctrlKey);
|
||||||
} else if (e.dataTransfer.types.includes('Files')) {
|
} else if (e.dataTransfer.types.includes('Files')) {
|
||||||
|
|
@ -79,8 +81,8 @@ Object.assign(ko.bindingHandlers, {
|
||||||
let editor = null;
|
let editor = null;
|
||||||
|
|
||||||
const fValue = fValueAccessor(),
|
const fValue = fValueAccessor(),
|
||||||
fUpdateEditorValue = () => fValue?.__editor?.setHtmlOrPlain(fValue()),
|
fUpdateEditorValue = () => fValue.__editor?.setHtmlOrPlain(fValue()),
|
||||||
fUpdateKoValue = () => fValue?.__editor && fValue(fValue.__editor.getDataWithHtmlMark()),
|
fUpdateKoValue = () => fValue.__editor && fValue(fValue.__editor.getDataWithHtmlMark()),
|
||||||
fOnReady = () => {
|
fOnReady = () => {
|
||||||
fValue.__editor = editor;
|
fValue.__editor = editor;
|
||||||
fUpdateEditorValue();
|
fUpdateEditorValue();
|
||||||
|
|
@ -144,6 +146,8 @@ Object.assign(ko.bindingHandlers, {
|
||||||
|
|
||||||
// Remove the Chrome visibility
|
// Remove the Chrome visibility
|
||||||
dragImage.style.cssText = '';
|
dragImage.style.cssText = '';
|
||||||
|
|
||||||
|
leftPanelDisabled(false);
|
||||||
} else {
|
} else {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}
|
}
|
||||||
|
|
@ -158,10 +162,10 @@ Object.assign(ko.bindingHandlers, {
|
||||||
init: (element, fValueAccessor) => {
|
init: (element, fValueAccessor) => {
|
||||||
const folder = fValueAccessor(); // ko.dataFor(element)
|
const folder = fValueAccessor(); // ko.dataFor(element)
|
||||||
folder && addEventsListeners(element, {
|
folder && addEventsListeners(element, {
|
||||||
dragenter: e => fnHover(e, element, folder),
|
dragenter: e => dragEnter(e, element, folder),
|
||||||
dragover: e => fnHover(e, element, folder),
|
dragover: e => e.preventDefault(),
|
||||||
dragleave: e => fnStop(e, element),
|
dragleave: e => dragStop(e, element),
|
||||||
drop: e => fnDrop(e, element, folder, dragData)
|
drop: e => dragDrop(e, element, folder, dragData)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
47
dev/External/ko.js
vendored
47
dev/External/ko.js
vendored
|
|
@ -28,6 +28,18 @@ export const
|
||||||
|
|
||||||
dispose = disposable => isFunction(disposable?.dispose) && disposable.dispose(),
|
dispose = disposable => isFunction(disposable?.dispose) && disposable.dispose(),
|
||||||
|
|
||||||
|
onKey = (key, element, fValueAccessor, fAllBindings, model) => {
|
||||||
|
let fn = event => {
|
||||||
|
if (key == event.key) {
|
||||||
|
// stopEvent(event);
|
||||||
|
// element.dispatchEvent(new Event('change'));
|
||||||
|
fValueAccessor().call(model);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
element.addEventListener('keydown', fn);
|
||||||
|
ko.utils.domNodeDisposal.addDisposeCallback(element, () => element.removeEventListener('keydown', fn));
|
||||||
|
},
|
||||||
|
|
||||||
// With this we don't need delegateRunOnDestroy
|
// With this we don't need delegateRunOnDestroy
|
||||||
koArrayWithDestroy = data => {
|
koArrayWithDestroy = data => {
|
||||||
data = ko.observableArray(data);
|
data = ko.observableArray(data);
|
||||||
|
|
@ -56,41 +68,18 @@ Object.assign(ko.bindingHandlers, {
|
||||||
},
|
},
|
||||||
|
|
||||||
onEnter: {
|
onEnter: {
|
||||||
init: (element, fValueAccessor, fAllBindings, viewModel) => {
|
init: (element, fValueAccessor, fAllBindings, model) =>
|
||||||
let fn = event => {
|
onKey('Enter', element, fValueAccessor, fAllBindings, model)
|
||||||
if ('Enter' == event.key) {
|
|
||||||
element.dispatchEvent(new Event('change'));
|
|
||||||
fValueAccessor().call(viewModel);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
element.addEventListener('keydown', fn);
|
|
||||||
ko.utils.domNodeDisposal.addDisposeCallback(element, () => element.removeEventListener('keydown', fn));
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
onEsc: {
|
onEsc: {
|
||||||
init: (element, fValueAccessor, fAllBindings, viewModel) => {
|
init: (element, fValueAccessor, fAllBindings, model) =>
|
||||||
let fn = event => {
|
onKey('Escape', element, fValueAccessor, fAllBindings, model)
|
||||||
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));
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
onSpace: {
|
onSpace: {
|
||||||
init: (element, fValueAccessor, fAllBindings, viewModel) => {
|
init: (element, fValueAccessor, fAllBindings, model) =>
|
||||||
let fn = event => {
|
onKey(' ', element, fValueAccessor, fAllBindings, model)
|
||||||
if (' ' == event.key) {
|
|
||||||
fValueAccessor().call(viewModel, event);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
element.addEventListener('keyup', fn);
|
|
||||||
ko.utils.domNodeDisposal.addDisposeCallback(element, () => element.removeEventListener('keyup', fn));
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
i18nUpdate: {
|
i18nUpdate: {
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,12 @@ pre {
|
||||||
body > * {
|
body > * {
|
||||||
padding: 0.5em 1em;
|
padding: 0.5em 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#attachments > * {
|
||||||
|
border: 1px solid rgba(125,128,128,0.5);
|
||||||
|
padding: 0.25em;
|
||||||
|
margin-right: 1em;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body></body>
|
<body></body>
|
||||||
|
|
|
||||||
|
|
@ -80,13 +80,11 @@ export class AbstractModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
revivePropertiesFromJson(json) {
|
revivePropertiesFromJson(json) {
|
||||||
let model = this.constructor;
|
const model = this.constructor,
|
||||||
if (!model.validJson(json)) {
|
valid = model.validJson(json);
|
||||||
return false;
|
valid && forEachObjectEntry(json, (key, value) => {
|
||||||
}
|
|
||||||
forEachObjectEntry(json, (key, value) => {
|
|
||||||
if ('@' !== key[0]) try {
|
if ('@' !== key[0]) try {
|
||||||
key = key[0].toLowerCase() + key.slice(1);
|
// key = key[0].toLowerCase() + key.slice(1);
|
||||||
switch (typeof this[key])
|
switch (typeof this[key])
|
||||||
{
|
{
|
||||||
case 'function':
|
case 'function':
|
||||||
|
|
@ -102,9 +100,12 @@ export class AbstractModel {
|
||||||
case 'string':
|
case 'string':
|
||||||
this[key] = typeCast(this[key], value);
|
this[key] = typeCast(this[key], value);
|
||||||
break;
|
break;
|
||||||
// fall through
|
|
||||||
case 'undefined':
|
case 'undefined':
|
||||||
default:
|
console.log(`Undefined ${model.name}.${key} set`);
|
||||||
|
this[key] = value;
|
||||||
|
break;
|
||||||
|
// default:
|
||||||
|
// console.log((typeof this[key])+` ${model.name}.${key} not revived`);
|
||||||
// console.log((typeof this[key])+' '+(model.name)+'.'+key+' not revived');
|
// console.log((typeof this[key])+' '+(model.name)+'.'+key+' not revived');
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
@ -112,7 +113,7 @@ export class AbstractModel {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return true;
|
return valid;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import ko from 'ko';
|
import ko from 'ko';
|
||||||
|
|
||||||
import { addObservablesTo, addComputablesTo, addSubscribablesTo } from 'External/ko';
|
import { addObservablesTo, addComputablesTo, addSubscribablesTo } from 'External/ko';
|
||||||
import { keyScope, SettingsGet, leftPanelDisabled, elementById } from 'Common/Globals';
|
import { keyScope, addShortcut, SettingsGet, leftPanelDisabled, toggleLeftPanel, elementById } from 'Common/Globals';
|
||||||
import { ViewTypePopup, showScreenPopup } from 'Knoin/Knoin';
|
import { ViewTypePopup, showScreenPopup } from 'Knoin/Knoin';
|
||||||
|
|
||||||
import { SaveSettingStatus } from 'Common/Enums';
|
import { SaveSettingStatus } from 'Common/Enums';
|
||||||
|
|
@ -59,7 +59,8 @@ export class AbstractViewPopup extends AbstractView
|
||||||
super('Popups' + name, ViewTypePopup);
|
super('Popups' + name, ViewTypePopup);
|
||||||
this.keyScope.scope = name;
|
this.keyScope.scope = name;
|
||||||
this.modalVisible = ko.observable(false).extend({ rateLimit: 0 });
|
this.modalVisible = ko.observable(false).extend({ rateLimit: 0 });
|
||||||
shortcuts.add('escape,close', '', name, () => {
|
this.close = () => this.modalVisible(false);
|
||||||
|
addShortcut('escape,close', '', name, () => {
|
||||||
if (this.modalVisible() && false !== this.onClose()) {
|
if (this.modalVisible() && false !== this.onClose()) {
|
||||||
this.close();
|
this.close();
|
||||||
}
|
}
|
||||||
|
|
@ -78,8 +79,6 @@ export class AbstractViewPopup extends AbstractView
|
||||||
afterShow() {} // Happens after showModal() animation transitionend
|
afterShow() {} // Happens after showModal() animation transitionend
|
||||||
onHide() {} // Happens before animation transitionend
|
onHide() {} // Happens before animation transitionend
|
||||||
afterHide() {} // Happens after animation transitionend
|
afterHide() {} // Happens after animation transitionend
|
||||||
|
|
||||||
close() {}
|
|
||||||
*/
|
*/
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -97,6 +96,7 @@ export class AbstractViewLeft extends AbstractView
|
||||||
{
|
{
|
||||||
super(templateID, 'left');
|
super(templateID, 'left');
|
||||||
this.leftPanelDisabled = leftPanelDisabled;
|
this.leftPanelDisabled = leftPanelDisabled;
|
||||||
|
this.toggleLeftPanel = toggleLeftPanel;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,19 @@
|
||||||
import ko from 'ko';
|
import ko from 'ko';
|
||||||
import { koComputable } from 'External/ko';
|
import { koComputable } from 'External/ko';
|
||||||
import { doc, $htmlCL, elementById, fireEvent } from 'Common/Globals';
|
import { doc, $htmlCL, elementById, createElement, fireEvent } from 'Common/Globals';
|
||||||
import { forEachObjectValue, forEachObjectEntry } from 'Common/Utils';
|
import { forEachObjectEntry } from 'Common/Utils';
|
||||||
import { i18nToNodes } from 'Common/Translator';
|
import { i18nToNodes } from 'Common/Translator';
|
||||||
|
|
||||||
|
import { leftPanelDisabled } from 'Common/Globals';
|
||||||
|
import { ThemeStore } from 'Stores/Theme';
|
||||||
|
|
||||||
let
|
let
|
||||||
SCREENS = {},
|
|
||||||
currentScreen = null,
|
currentScreen = null,
|
||||||
defaultScreenName = '';
|
defaultScreenName = '';
|
||||||
|
|
||||||
const
|
const
|
||||||
|
SCREENS = new Map,
|
||||||
|
|
||||||
autofocus = dom => dom.querySelector('[autofocus]')?.focus(),
|
autofocus = dom => dom.querySelector('[autofocus]')?.focus(),
|
||||||
|
|
||||||
visiblePopups = new Set,
|
visiblePopups = new Set,
|
||||||
|
|
@ -18,7 +22,7 @@ const
|
||||||
* @param {string} screenName
|
* @param {string} screenName
|
||||||
* @returns {?Object}
|
* @returns {?Object}
|
||||||
*/
|
*/
|
||||||
screen = screenName => (screenName && SCREENS[screenName]) || null,
|
screen = screenName => (screenName && SCREENS.get(screenName)) || null,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Function} ViewModelClass
|
* @param {Function} ViewModelClass
|
||||||
|
|
@ -35,27 +39,24 @@ const
|
||||||
dialog = ViewTypePopup === vm.viewType,
|
dialog = ViewTypePopup === vm.viewType,
|
||||||
vmPlace = doc.getElementById(position);
|
vmPlace = doc.getElementById(position);
|
||||||
|
|
||||||
fireEvent('rl-view-model.create', vm);
|
|
||||||
|
|
||||||
ViewModelClass.__builded = true;
|
ViewModelClass.__builded = true;
|
||||||
ViewModelClass.__vm = vm;
|
ViewModelClass.__vm = vm;
|
||||||
|
|
||||||
if (vmPlace) {
|
if (vmPlace) {
|
||||||
vmDom = Element.fromHTML(dialog
|
vmDom = dialog
|
||||||
? '<dialog id="V-'+ id + '"></dialog>'
|
? createElement('dialog',{id:'V-'+id})
|
||||||
: '<div id="V-'+ id + '" hidden=""></div>');
|
: createElement('div',{id:'V-'+id,hidden:''})
|
||||||
vmPlace.append(vmDom);
|
vmPlace.append(vmDom);
|
||||||
|
|
||||||
vm.viewModelDom = ViewModelClass.__dom = vmDom;
|
vm.viewModelDom = ViewModelClass.__dom = vmDom;
|
||||||
|
|
||||||
if (dialog) {
|
if (dialog) {
|
||||||
vm.close = () => hideScreenPopup(ViewModelClass);
|
|
||||||
|
|
||||||
// Firefox < 98 / Safari < 15.4 HTMLDialogElement not defined
|
// Firefox < 98 / Safari < 15.4 HTMLDialogElement not defined
|
||||||
if (!vmDom.showModal) {
|
if (!vmDom.showModal) {
|
||||||
|
vmDom.className = 'polyfill';
|
||||||
vmDom.showModal = () => {
|
vmDom.showModal = () => {
|
||||||
vmDom.backdrop ||
|
vmDom.backdrop ||
|
||||||
vmDom.before(vmDom.backdrop = Element.fromHTML('<div class="dialog-backdrop"></div>'));
|
vmDom.before(vmDom.backdrop = createElement('div',{class:'dialog-backdrop'}));
|
||||||
vmDom.setAttribute('open','');
|
vmDom.setAttribute('open','');
|
||||||
vmDom.open = true;
|
vmDom.open = true;
|
||||||
vmDom.returnValue = null;
|
vmDom.returnValue = null;
|
||||||
|
|
@ -79,7 +80,6 @@ const
|
||||||
const endShowHide = e => {
|
const endShowHide = e => {
|
||||||
if (e.target === vmDom) {
|
if (e.target === vmDom) {
|
||||||
if (vmDom.classList.contains('animate')) {
|
if (vmDom.classList.contains('animate')) {
|
||||||
autofocus(vmDom);
|
|
||||||
vm.afterShow?.();
|
vm.afterShow?.();
|
||||||
} else {
|
} else {
|
||||||
vmDom.close();
|
vmDom.close();
|
||||||
|
|
@ -98,6 +98,7 @@ const
|
||||||
vmDom.backdrop.style.zIndex = 3000 + (visiblePopups.size * 2);
|
vmDom.backdrop.style.zIndex = 3000 + (visiblePopups.size * 2);
|
||||||
}
|
}
|
||||||
vm.keyScope.set();
|
vm.keyScope.set();
|
||||||
|
setTimeout(()=>autofocus(vmDom),1);
|
||||||
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('animate'); // trigger the transitions
|
vmDom.classList.add('animate'); // trigger the transitions
|
||||||
|
|
@ -113,6 +114,8 @@ const
|
||||||
vmDom.addEventListener('transitionend', endShowHide);
|
vmDom.addEventListener('transitionend', endShowHide);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fireEvent('rl-view-model.create', vm);
|
||||||
|
|
||||||
ko.applyBindingAccessorsToNode(
|
ko.applyBindingAccessorsToNode(
|
||||||
vmDom,
|
vmDom,
|
||||||
{
|
{
|
||||||
|
|
@ -129,7 +132,7 @@ const
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return ViewModelClass && ViewModelClass.__vm;
|
return ViewModelClass?.__vm;
|
||||||
},
|
},
|
||||||
|
|
||||||
forEachViewModel = (screen, fn) => {
|
forEachViewModel = (screen, fn) => {
|
||||||
|
|
@ -151,16 +154,7 @@ const
|
||||||
vm.onHide?.();
|
vm.onHide?.();
|
||||||
destroy && vm.viewModelDom.remove();
|
destroy && vm.viewModelDom.remove();
|
||||||
});
|
});
|
||||||
},
|
ThemeStore.isMobile() && leftPanelDisabled(true);
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {Function} ViewModelClassToHide
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
hideScreenPopup = ViewModelClassToHide => {
|
|
||||||
if (ViewModelClassToHide?.__vm && ViewModelClassToHide?.__dom) {
|
|
||||||
ViewModelClassToHide.__vm.modalVisible(false);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -169,22 +163,14 @@ const
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
screenOnRoute = (screenName, subPart) => {
|
screenOnRoute = (screenName, subPart) => {
|
||||||
let vmScreen = null,
|
screenName = screenName || defaultScreenName;
|
||||||
isSameScreen = false;
|
if (screenName && fireEvent('sm-show-screen', screenName, 1)) {
|
||||||
|
|
||||||
if (null == screenName || '' == screenName) {
|
|
||||||
screenName = defaultScreenName;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (fireEvent('sm-show-screen', screenName, 1)) {
|
|
||||||
|
|
||||||
// Close all popups
|
// Close all popups
|
||||||
for (let vm of visiblePopups) {
|
for (let vm of visiblePopups) {
|
||||||
(false === vm.onClose()) || vm.close();
|
(false === vm.onClose()) || vm.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (screenName) {
|
let vmScreen = screen(screenName);
|
||||||
vmScreen = screen(screenName);
|
|
||||||
if (!vmScreen) {
|
if (!vmScreen) {
|
||||||
vmScreen = screen(defaultScreenName);
|
vmScreen = screen(defaultScreenName);
|
||||||
if (vmScreen) {
|
if (vmScreen) {
|
||||||
|
|
@ -194,7 +180,7 @@ const
|
||||||
}
|
}
|
||||||
|
|
||||||
if (vmScreen?.__started) {
|
if (vmScreen?.__started) {
|
||||||
isSameScreen = currentScreen && vmScreen === currentScreen;
|
let isSameScreen = currentScreen && vmScreen === currentScreen;
|
||||||
|
|
||||||
if (!vmScreen.__builded) {
|
if (!vmScreen.__builded) {
|
||||||
vmScreen.__builded = true;
|
vmScreen.__builded = true;
|
||||||
|
|
@ -208,9 +194,7 @@ const
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
// hide screen
|
// hide screen
|
||||||
if (currentScreen && !isSameScreen) {
|
currentScreen && !isSameScreen && hideScreen(currentScreen);
|
||||||
hideScreen(currentScreen);
|
|
||||||
}
|
|
||||||
// --
|
// --
|
||||||
|
|
||||||
currentScreen = vmScreen;
|
currentScreen = vmScreen;
|
||||||
|
|
@ -233,7 +217,6 @@ const
|
||||||
}, 1);
|
}, 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -267,21 +250,19 @@ export const
|
||||||
*/
|
*/
|
||||||
startScreens = screensClasses => {
|
startScreens = screensClasses => {
|
||||||
hasher.clear();
|
hasher.clear();
|
||||||
forEachObjectValue(SCREENS, screen => hideScreen(screen, 1));
|
SCREENS.forEach(screen => hideScreen(screen, 1));
|
||||||
SCREENS = {};
|
SCREENS.clear();
|
||||||
currentScreen = null,
|
currentScreen = null,
|
||||||
defaultScreenName = '';
|
defaultScreenName = '';
|
||||||
|
|
||||||
screensClasses.forEach(CScreen => {
|
screensClasses.forEach(CScreen => {
|
||||||
if (CScreen) {
|
|
||||||
const vmScreen = new CScreen(),
|
const vmScreen = new CScreen(),
|
||||||
screenName = vmScreen.screenName;
|
screenName = vmScreen.screenName;
|
||||||
defaultScreenName || (defaultScreenName = screenName);
|
defaultScreenName || (defaultScreenName = screenName);
|
||||||
SCREENS[screenName] = vmScreen;
|
SCREENS.set(screenName, vmScreen);
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
forEachObjectValue(SCREENS, vmScreen => {
|
SCREENS.forEach(vmScreen => {
|
||||||
if (!vmScreen.__started) {
|
if (!vmScreen.__started) {
|
||||||
vmScreen.onStart();
|
vmScreen.onStart();
|
||||||
vmScreen.__started = true;
|
vmScreen.__started = true;
|
||||||
|
|
@ -289,7 +270,7 @@ export const
|
||||||
});
|
});
|
||||||
|
|
||||||
const cross = new Crossroads();
|
const cross = new Crossroads();
|
||||||
cross.addRoute(/^([a-zA-Z0-9-]*)\/?(.*)$/, screenOnRoute);
|
cross.addRoute(/^([^/]*)\/?(.*)$/, screenOnRoute);
|
||||||
|
|
||||||
hasher.add(cross.parse.bind(cross));
|
hasher.add(cross.parse.bind(cross));
|
||||||
hasher.init();
|
hasher.init();
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,17 @@
|
||||||
|
//import { b64Encode } from 'Common/Utils';
|
||||||
|
|
||||||
const
|
const
|
||||||
// RFC2045
|
// RFC2045
|
||||||
QPDecodeIn = /=([0-9A-F]{2})/g,
|
QPDecodeParams = [/=([0-9A-F]{2})/g, (...args) => String.fromCharCode(parseInt(args[1], 16))],
|
||||||
QPDecodeOut = (...args) => String.fromCharCode(parseInt(args[1], 16));
|
QPDecode = data => data.replace(/=\r?\n/g, '').replace(...QPDecodeParams),
|
||||||
|
decodeText = (charset, data) => {
|
||||||
|
try {
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/API/Encoding_API/Encodings
|
||||||
|
return new TextDecoder(charset).decode(Uint8Array.from(data, c => c.charCodeAt(0)));
|
||||||
|
} catch (e) {
|
||||||
|
console.error({charset:charset,error:e});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export function ParseMime(text)
|
export function ParseMime(text)
|
||||||
{
|
{
|
||||||
|
|
@ -40,22 +49,14 @@ export function ParseMime(text)
|
||||||
|
|
||||||
get body() {
|
get body() {
|
||||||
let body = this.bodyRaw,
|
let body = this.bodyRaw,
|
||||||
// charset = this.header('content-type')?.params.charset,
|
charset = this.header('content-type')?.params.charset,
|
||||||
encoding = this.headerValue('content-transfer-encoding');
|
encoding = this.headerValue('content-transfer-encoding');
|
||||||
if ('quoted-printable' == encoding) {
|
if ('quoted-printable' == encoding) {
|
||||||
body = body.replace(/=\r?\n/g, '').replace(QPDecodeIn, QPDecodeOut);
|
body = QPDecode(body);
|
||||||
} else if ('base64' == encoding) {
|
} else if ('base64' == encoding) {
|
||||||
body = atob(body.replace(/\r?\n/g, ''));
|
body = atob(body.replace(/\r?\n/g, ''));
|
||||||
}
|
}
|
||||||
/*
|
return decodeText(charset, body);
|
||||||
try {
|
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/API/Encoding_API/Encodings
|
|
||||||
return new TextDecoder(charset).decode(Uint8Array.from(body, c => c.charCodeAt(0)));
|
|
||||||
// return new TextDecoder(charset).decode(new TextEncoder().encode(body));
|
|
||||||
} catch (e) {
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
return body;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get dataUrl() {
|
get dataUrl() {
|
||||||
|
|
@ -65,9 +66,10 @@ export function ParseMime(text)
|
||||||
body = body.replace(/\r?\n/g, '');
|
body = body.replace(/\r?\n/g, '');
|
||||||
} else {
|
} else {
|
||||||
if ('quoted-printable' == encoding) {
|
if ('quoted-printable' == encoding) {
|
||||||
body = body.replace(/=\r?\n/g, '').replace(QPDecodeIn, QPDecodeOut);
|
body = QPDecode(body);
|
||||||
}
|
}
|
||||||
body = btoa(body);
|
body = btoa(body);
|
||||||
|
// body = b64Encode(body);
|
||||||
}
|
}
|
||||||
return 'data:' + this.headerValue('content-type') + ';base64,' + body;
|
return 'data:' + this.headerValue('content-type') + ';base64,' + body;
|
||||||
}
|
}
|
||||||
|
|
@ -111,8 +113,12 @@ export function ParseMime(text)
|
||||||
[...header.matchAll(/;\s*([^;=]+)=\s*"?([^;"]+)"?/g)].forEach(param =>
|
[...header.matchAll(/;\s*([^;=]+)=\s*"?([^;"]+)"?/g)].forEach(param =>
|
||||||
params[param[1].trim().toLowerCase()] = param[2].trim()
|
params[param[1].trim().toLowerCase()] = param[2].trim()
|
||||||
);
|
);
|
||||||
|
// encoded-word = "=?" charset "?" encoding "?" encoded-text "?="
|
||||||
|
match[2] = match[2].trim().replace(/=\?([^?]+)\?(B|Q)\?(.+?)\?=/g, (m, charset, encoding, text) =>
|
||||||
|
decodeText(charset, 'B' == encoding ? atob(text) : QPDecode(text))
|
||||||
|
);
|
||||||
headers[match[1].trim().toLowerCase()] = {
|
headers[match[1].trim().toLowerCase()] = {
|
||||||
value: match[2].trim(),
|
value: match[2],
|
||||||
params: params
|
params: params
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
|
|
||||||
import { ParseMime } from 'Mime/Parser';
|
import { ParseMime } from 'Mime/Parser';
|
||||||
import { AttachmentModel } from 'Model/Attachment';
|
import { AttachmentModel } from 'Model/Attachment';
|
||||||
import { EmailModel } from 'Model/Email';
|
|
||||||
import { FileInfo } from 'Common/File';
|
import { FileInfo } from 'Common/File';
|
||||||
import { BEGIN_PGP_MESSAGE } from 'Stores/User/Pgp';
|
import { BEGIN_PGP_MESSAGE } from 'Stores/User/Pgp';
|
||||||
|
|
||||||
|
|
@ -14,25 +13,20 @@ export function MimeToMessage(data, message)
|
||||||
let signed;
|
let signed;
|
||||||
const struct = ParseMime(data);
|
const struct = ParseMime(data);
|
||||||
if (struct.headers) {
|
if (struct.headers) {
|
||||||
let html = struct.getByContentType('text/html');
|
let html = struct.getByContentType('text/html'),
|
||||||
|
subject = struct.headerValue('subject');
|
||||||
html = html ? html.body : '';
|
html = html ? html.body : '';
|
||||||
|
|
||||||
if (struct.headers.subject) {
|
subject && message.subject(subject);
|
||||||
message.subject(struct.headers.subject.value);
|
|
||||||
}
|
// EmailCollectionModel
|
||||||
['from','to'].forEach(name => {
|
['from','to'].forEach(name => message[name].fromString(struct.headerValue(name)));
|
||||||
if (struct.headers[name] && !message[name].length) {
|
|
||||||
let mail = new EmailModel;
|
|
||||||
mail.parse(struct.headers[name].value);
|
|
||||||
message[name].push(mail);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
struct.forEach(part => {
|
struct.forEach(part => {
|
||||||
let cd = part.header('content-disposition'),
|
let cd = part.header('content-disposition'),
|
||||||
cid = part.header('content-id'),
|
cId = part.header('content-id'),
|
||||||
type = part.header('content-type');
|
type = part.header('content-type');
|
||||||
if (cid || cd) {
|
if (cId || cd) {
|
||||||
// if (cd && 'attachment' === cd.value) {
|
// if (cd && 'attachment' === cd.value) {
|
||||||
let attachment = new AttachmentModel;
|
let attachment = new AttachmentModel;
|
||||||
attachment.mimeType = type.value;
|
attachment.mimeType = type.value;
|
||||||
|
|
@ -40,17 +34,15 @@ export function MimeToMessage(data, message)
|
||||||
attachment.fileNameExt = attachment.fileName.replace(/^.+(\.[a-z]+)$/, '$1');
|
attachment.fileNameExt = attachment.fileName.replace(/^.+(\.[a-z]+)$/, '$1');
|
||||||
attachment.fileType = FileInfo.getType('', type.value);
|
attachment.fileType = FileInfo.getType('', type.value);
|
||||||
attachment.url = part.dataUrl;
|
attachment.url = part.dataUrl;
|
||||||
attachment.friendlySize = FileInfo.friendlySize(part.body.length);
|
attachment.estimatedSize = part.body.length;
|
||||||
/*
|
/*
|
||||||
attachment.isThumbnail = false;
|
|
||||||
attachment.contentLocation = '';
|
attachment.contentLocation = '';
|
||||||
attachment.download = '';
|
|
||||||
attachment.folder = '';
|
attachment.folder = '';
|
||||||
attachment.uid = '';
|
attachment.uid = '';
|
||||||
attachment.mimeIndex = part.id;
|
attachment.mimeIndex = part.id;
|
||||||
*/
|
*/
|
||||||
attachment.cid = cid ? cid.value : '';
|
attachment.cId = cId ? cId.value : '';
|
||||||
if (cid && html) {
|
if (cId && html) {
|
||||||
let cid = 'cid:' + attachment.contentId(),
|
let cid = 'cid:' + attachment.contentId(),
|
||||||
found = html.includes(cid);
|
found = html.includes(cid);
|
||||||
attachment.isInline(found);
|
attachment.isInline(found);
|
||||||
|
|
@ -64,9 +56,9 @@ export function MimeToMessage(data, message)
|
||||||
}
|
}
|
||||||
} else if ('multipart/signed' === type.value && 'application/pgp-signature' === type.params.protocol) {
|
} else if ('multipart/signed' === type.value && 'application/pgp-signature' === type.params.protocol) {
|
||||||
signed = {
|
signed = {
|
||||||
MicAlg: type.micalg,
|
micAlg: type.micalg,
|
||||||
BodyPart: part.parts[0],
|
bodyPart: part.parts[0],
|
||||||
SigPart: part.parts[1]
|
sigPart: part.parts[1]
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
import { AbstractModel } from 'Knoin/AbstractModel';
|
import { AbstractModel } from 'Knoin/AbstractModel';
|
||||||
import { addObservablesTo } from 'External/ko';
|
import { addObservablesTo } from 'External/ko';
|
||||||
|
import Remote from 'Remote/User/Fetch';
|
||||||
|
import { SettingsUserStore } from 'Stores/User/Settings';
|
||||||
|
|
||||||
export class AccountModel extends AbstractModel {
|
export class AccountModel extends AbstractModel {
|
||||||
/**
|
/**
|
||||||
|
|
@ -7,16 +9,52 @@ export class AccountModel extends AbstractModel {
|
||||||
* @param {boolean=} canBeDelete = true
|
* @param {boolean=} canBeDelete = true
|
||||||
* @param {number=} count = 0
|
* @param {number=} count = 0
|
||||||
*/
|
*/
|
||||||
constructor(email/*, count = 0*/, isAdditional = true) {
|
constructor(email, name, isAdditional = true) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
|
this.name = name;
|
||||||
this.email = email;
|
this.email = email;
|
||||||
|
|
||||||
|
this.displayName = name ? name + ' <' + email + '>' : email;
|
||||||
|
|
||||||
addObservablesTo(this, {
|
addObservablesTo(this, {
|
||||||
// count: count || 0,
|
unreadEmails: null,
|
||||||
askDelete: false,
|
askDelete: false,
|
||||||
isAdditional: isAdditional
|
isAdditional: isAdditional
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Load at random between 3 and 30 seconds
|
||||||
|
SettingsUserStore.showUnreadCount() && isAdditional
|
||||||
|
&& setTimeout(()=>this.fetchUnread(), (Math.ceil(Math.random() * 10)) * 3000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get INBOX unread messages
|
||||||
|
*/
|
||||||
|
fetchUnread() {
|
||||||
|
Remote.request('AccountUnread', (iError, oData) => {
|
||||||
|
iError || this.unreadEmails(oData?.Result?.unreadEmails || null);
|
||||||
|
}, {
|
||||||
|
email: this.email
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Imports all mail to main account
|
||||||
|
*//*
|
||||||
|
importAll(account) {
|
||||||
|
Remote.streamPerLine(line => {
|
||||||
|
try {
|
||||||
|
line = JSON.parse(line);
|
||||||
|
console.dir(line);
|
||||||
|
} catch (e) {
|
||||||
|
// OOPS
|
||||||
|
}
|
||||||
|
}, 'AccountImport', {
|
||||||
|
Action: 'AccountImport',
|
||||||
|
email: account.email
|
||||||
|
});
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
import ko from 'ko';
|
import ko from 'ko';
|
||||||
|
|
||||||
import { FileInfo, FileType } from 'Common/File';
|
import { FileInfo, FileType } from 'Common/File';
|
||||||
|
import { stopEvent, SettingsGet, SettingsCapa } from 'Common/Globals';
|
||||||
|
import { b64EncodeJSONSafe } from 'Common/Utils';
|
||||||
import {
|
import {
|
||||||
attachmentDownload,
|
attachmentDownload,
|
||||||
serverRequestRaw
|
serverRequestRaw
|
||||||
|
|
@ -18,13 +20,12 @@ export class AttachmentModel extends AbstractModel {
|
||||||
this.checked = ko.observable(true);
|
this.checked = ko.observable(true);
|
||||||
|
|
||||||
this.mimeType = '';
|
this.mimeType = '';
|
||||||
|
// this.mimeTypeParams = '';
|
||||||
this.fileName = '';
|
this.fileName = '';
|
||||||
this.fileNameExt = '';
|
this.fileNameExt = '';
|
||||||
this.fileType = FileType.Unknown;
|
this.fileType = FileType.Unknown;
|
||||||
this.isThumbnail = false;
|
this.cId = '';
|
||||||
this.cid = '';
|
|
||||||
this.contentLocation = '';
|
this.contentLocation = '';
|
||||||
this.download = '';
|
|
||||||
this.folder = '';
|
this.folder = '';
|
||||||
this.uid = '';
|
this.uid = '';
|
||||||
this.url = '';
|
this.url = '';
|
||||||
|
|
@ -51,12 +52,17 @@ export class AttachmentModel extends AbstractModel {
|
||||||
return attachment;
|
return attachment;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toggleChecked(self, event) {
|
||||||
|
stopEvent(event);
|
||||||
|
self.checked(!self.checked());
|
||||||
|
}
|
||||||
|
|
||||||
friendlySize() {
|
friendlySize() {
|
||||||
return FileInfo.friendlySize(this.estimatedSize) + (this.isLinked() ? ' 🔗' : '');
|
return FileInfo.friendlySize(this.estimatedSize) + (this.isLinked() ? ' 🔗' : '');
|
||||||
}
|
}
|
||||||
|
|
||||||
contentId() {
|
contentId() {
|
||||||
return this.cid.replace(/^<+|>+$/g, '');
|
return this.cId.replace(/^<+|>+$/g, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -119,6 +125,17 @@ export class AttachmentModel extends AbstractModel {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get download() {
|
||||||
|
return b64EncodeJSONSafe({
|
||||||
|
folder: this.folder,
|
||||||
|
uid: this.uid,
|
||||||
|
mimeIndex: this.mimeIndex,
|
||||||
|
mimeType: this.mimeType,
|
||||||
|
fileName: this.fileName,
|
||||||
|
accountHash: SettingsGet('accountHash')
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
|
|
@ -137,7 +154,7 @@ export class AttachmentModel extends AbstractModel {
|
||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
*/
|
*/
|
||||||
hasThumbnail() {
|
hasThumbnail() {
|
||||||
return this.isThumbnail && !this.isLinked();
|
return SettingsCapa('AttachmentThumbnails') && this.isImage() && !this.isLinked();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -146,7 +163,7 @@ export class AttachmentModel extends AbstractModel {
|
||||||
thumbnailStyle() {
|
thumbnailStyle() {
|
||||||
return this.hasThumbnail()
|
return this.hasThumbnail()
|
||||||
? 'background:url(' + serverRequestRaw('ViewThumbnail', this.download) + ')'
|
? 'background:url(' + serverRequestRaw('ViewThumbnail', this.download) + ')'
|
||||||
: '';
|
: null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -21,11 +21,11 @@ export class AttachmentCollectionModel extends AbstractCollectionModel
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} cid
|
* @param {string} cId
|
||||||
* @returns {*}
|
* @returns {*}
|
||||||
*/
|
*/
|
||||||
findByCid(cid) {
|
findByCid(cId) {
|
||||||
cid = cid.replace(/^<+|>+$/g, '');
|
cId = cId.replace(/^<+|>+$/g, '');
|
||||||
return this.find(item => cid === item.contentId());
|
return this.find(item => cId === item.contentId());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,16 +10,16 @@ export class ComposeAttachmentModel extends AbstractModel {
|
||||||
* @param {?number=} size = null
|
* @param {?number=} size = null
|
||||||
* @param {boolean=} isInline = false
|
* @param {boolean=} isInline = false
|
||||||
* @param {boolean=} isLinked = false
|
* @param {boolean=} isLinked = false
|
||||||
* @param {string=} CID = ''
|
* @param {string=} cId = ''
|
||||||
* @param {string=} contentLocation = ''
|
* @param {string=} contentLocation = ''
|
||||||
*/
|
*/
|
||||||
constructor(id, fileName, size = null, isInline = false, isLinked = false, CID = '', contentLocation = '') {
|
constructor(id, fileName, size = null, isInline = false, isLinked = false, cId = '', contentLocation = '') {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
this.id = id;
|
this.id = id;
|
||||||
this.isInline = !!isInline;
|
this.isInline = !!isInline;
|
||||||
this.isLinked = !!isLinked;
|
this.isLinked = !!isLinked;
|
||||||
this.CID = CID;
|
this.cId = cId;
|
||||||
this.contentLocation = contentLocation;
|
this.contentLocation = contentLocation;
|
||||||
this.fromMessage = false;
|
this.fromMessage = false;
|
||||||
|
|
||||||
|
|
@ -27,6 +27,7 @@ export class ComposeAttachmentModel extends AbstractModel {
|
||||||
fileName: fileName,
|
fileName: fileName,
|
||||||
size: size,
|
size: size,
|
||||||
tempName: '',
|
tempName: '',
|
||||||
|
type: '', // application/octet-stream
|
||||||
|
|
||||||
progress: 0,
|
progress: 0,
|
||||||
error: '',
|
error: '',
|
||||||
|
|
@ -54,7 +55,7 @@ export class ComposeAttachmentModel extends AbstractModel {
|
||||||
return null === localSize ? '' : FileInfo.friendlySize(localSize);
|
return null === localSize ? '' : FileInfo.friendlySize(localSize);
|
||||||
},
|
},
|
||||||
|
|
||||||
mimeType: () => FileInfo.getContentType(this.fileName()),
|
mimeType: () => this.type() || FileInfo.getContentType(this.fileName()),
|
||||||
fileExt: () => FileInfo.getExtension(this.fileName()),
|
fileExt: () => FileInfo.getExtension(this.fileName()),
|
||||||
|
|
||||||
iconClass: () => FileInfo.getIconClass(this.fileExt(), this.mimeType())
|
iconClass: () => FileInfo.getIconClass(this.fileExt(), this.mimeType())
|
||||||
|
|
|
||||||
|
|
@ -119,7 +119,7 @@ export class ContactModel extends AbstractModel {
|
||||||
|
|
||||||
display: () => {
|
display: () => {
|
||||||
let a = this.fullName(),
|
let a = this.fullName(),
|
||||||
b = this.email()?.[0]?.value,
|
b = this.email()[0]?.value(),
|
||||||
c = this.nickname();
|
c = this.nickname();
|
||||||
return a || b || c;
|
return a || b || c;
|
||||||
}
|
}
|
||||||
|
|
@ -139,7 +139,7 @@ export class ContactModel extends AbstractModel {
|
||||||
*/
|
*/
|
||||||
getNameAndEmailHelper() {
|
getNameAndEmailHelper() {
|
||||||
let name = (this.givenName() + ' ' + this.surName()).trim(),
|
let name = (this.givenName() + ' ' + this.surName()).trim(),
|
||||||
email = this.email()[0];
|
email = this.email()[0]?.value();
|
||||||
/*
|
/*
|
||||||
// this.jCard.getOne('fn')?.notEmpty() ||
|
// this.jCard.getOne('fn')?.notEmpty() ||
|
||||||
this.jCard.parseFullName({set:true});
|
this.jCard.parseFullName({set:true});
|
||||||
|
|
@ -304,7 +304,7 @@ export class ContactModel extends AbstractModel {
|
||||||
// jCard.set('rev', '2022-05-21T10:59:52Z')
|
// jCard.set('rev', '2022-05-21T10:59:52Z')
|
||||||
|
|
||||||
return {
|
return {
|
||||||
Uid: this.id,
|
uid: this.id,
|
||||||
jCard: JSON.stringify(jCard)
|
jCard: JSON.stringify(jCard)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,157 +18,24 @@ import { AbstractModel } from 'Knoin/AbstractModel';
|
||||||
* @param {String} str Address field
|
* @param {String} str Address field
|
||||||
* @return {Array} An array of address objects
|
* @return {Array} An array of address objects
|
||||||
*/
|
*/
|
||||||
function addressparser(str) {
|
export function addressparser(str) {
|
||||||
var tokenizer = new Tokenizer(str);
|
str = (str || '').toString();
|
||||||
var tokens = tokenizer.tokenize();
|
|
||||||
var addresses = [];
|
|
||||||
var address = [];
|
|
||||||
var parsedAddresses = [];
|
|
||||||
|
|
||||||
tokens.forEach(token => {
|
let
|
||||||
if (token.type === 'operator' && (token.value === ',' || token.value === ';')) {
|
endOperator = '',
|
||||||
address.length && addresses.push(address);
|
node = {
|
||||||
address = [];
|
type: 'text',
|
||||||
} else {
|
value: ''
|
||||||
address.push(token);
|
},
|
||||||
}
|
escaped = false,
|
||||||
});
|
address = [],
|
||||||
|
addresses = [];
|
||||||
|
|
||||||
address.length && addresses.push(address);
|
const
|
||||||
|
/*
|
||||||
addresses.forEach(address => {
|
|
||||||
address = _handleAddress(address);
|
|
||||||
if (address.length) {
|
|
||||||
parsedAddresses = parsedAddresses.concat(address);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return parsedAddresses;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts tokens for a single address into an address object
|
|
||||||
*
|
|
||||||
* @param {Array} tokens Tokens object
|
|
||||||
* @return {Object} Address object
|
|
||||||
*/
|
|
||||||
function _handleAddress(tokens) {
|
|
||||||
var isGroup = false;
|
|
||||||
var state = 'text';
|
|
||||||
var address = void 0;
|
|
||||||
var addresses = [];
|
|
||||||
var data = {
|
|
||||||
address: [],
|
|
||||||
comment: [],
|
|
||||||
group: [],
|
|
||||||
text: []
|
|
||||||
};
|
|
||||||
|
|
||||||
// Filter out <addresses>, (comments) and regular text
|
|
||||||
tokens.forEach(token => {
|
|
||||||
if (token.type === 'operator') {
|
|
||||||
switch (token.value) {
|
|
||||||
case '<':
|
|
||||||
state = 'address';
|
|
||||||
break;
|
|
||||||
case '(':
|
|
||||||
state = 'comment';
|
|
||||||
break;
|
|
||||||
case ':':
|
|
||||||
state = 'group';
|
|
||||||
isGroup = true;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
state = 'text';
|
|
||||||
}
|
|
||||||
} else if (token.value) {
|
|
||||||
data[state].push(token.value);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// If there is no text but a comment, replace the two
|
|
||||||
if (!data.text.length && data.comment.length) {
|
|
||||||
data.text = data.comment;
|
|
||||||
data.comment = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isGroup) {
|
|
||||||
// http://tools.ietf.org/html/rfc2822#appendix-A.1.3
|
|
||||||
data.text = data.text.join(' ');
|
|
||||||
addresses.push({
|
|
||||||
name: data.text || address && address.name,
|
|
||||||
group: data.group.length ? addressparser(data.group.join(',')) : []
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// If no address was found, try to detect one from regular text
|
|
||||||
if (!data.address.length && data.text.length) {
|
|
||||||
var i = data.text.length;
|
|
||||||
while (i--) {
|
|
||||||
if (data.text[i].match(/^[^@\s]+@[^@\s]+$/)) {
|
|
||||||
data.address = data.text.splice(i, 1);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// still no address
|
|
||||||
if (!data.address.length) {
|
|
||||||
i = data.text.length;
|
|
||||||
while (i--) {
|
|
||||||
data.text[i] = data.text[i].replace(/\s*\b[^@\s]+@[^@\s]+\b\s*/, address => {
|
|
||||||
if (!data.address.length) {
|
|
||||||
data.address = [address.trim()];
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
return address.trim();
|
|
||||||
});
|
|
||||||
if (data.address.length) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If there's still is no text but a comment exixts, replace the two
|
|
||||||
if (!data.text.length && data.comment.length) {
|
|
||||||
data.text = data.comment;
|
|
||||||
data.comment = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Keep only the first address occurence, push others to regular text
|
|
||||||
if (data.address.length > 1) {
|
|
||||||
data.text = data.text.concat(data.address.splice(1));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Join values with spaces
|
|
||||||
data.text = data.text.join(' ');
|
|
||||||
data.address = data.address.join(' ');
|
|
||||||
|
|
||||||
if (!data.address && isGroup) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
address = {
|
|
||||||
address: data.address || data.text || '',
|
|
||||||
name: data.text || data.address || ''
|
|
||||||
};
|
|
||||||
|
|
||||||
if (address.address === address.name) {
|
|
||||||
if ((address.address || '').match(/@/)) {
|
|
||||||
address.name = '';
|
|
||||||
} else {
|
|
||||||
address.address = '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
addresses.push(address);
|
|
||||||
}
|
|
||||||
|
|
||||||
return addresses;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Operator tokens and which tokens are expected to end the sequence
|
* Operator tokens and which tokens are expected to end the sequence
|
||||||
*/
|
*/
|
||||||
var OPERATORS = {
|
OPERATORS = {
|
||||||
'"': '"',
|
'"': '"',
|
||||||
'(': ')',
|
'(': ')',
|
||||||
'<': '>',
|
'<': '>',
|
||||||
|
|
@ -182,76 +49,155 @@ var OPERATORS = {
|
||||||
// comma in their UI, it makes sense to treat them the same as a comma
|
// comma in their UI, it makes sense to treat them the same as a comma
|
||||||
// when used outside of a group.
|
// when used outside of a group.
|
||||||
';': ''
|
';': ''
|
||||||
};
|
},
|
||||||
|
pushToken = token => {
|
||||||
class Tokenizer
|
token.value = (token.value || '').toString().trim();
|
||||||
{
|
token.value.length && address.push(token);
|
||||||
constructor(str) {
|
node = {
|
||||||
this.str = (str || '').toString();
|
|
||||||
this.operatorCurrent = '';
|
|
||||||
this.operatorExpecting = '';
|
|
||||||
this.node = null;
|
|
||||||
this.escaped = false;
|
|
||||||
this.list = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
tokenize() {
|
|
||||||
var list = [];
|
|
||||||
[...this.str].forEach(c => this.checkChar(c));
|
|
||||||
|
|
||||||
this.list.forEach(node => {
|
|
||||||
node.value = (node.value || '').toString().trim();
|
|
||||||
node.value && list.push(node);
|
|
||||||
});
|
|
||||||
|
|
||||||
return list;
|
|
||||||
}
|
|
||||||
|
|
||||||
checkChar(chr) {
|
|
||||||
if ((chr in OPERATORS || chr === '\\') && this.escaped) {
|
|
||||||
this.escaped = false;
|
|
||||||
} else if (this.operatorExpecting && chr === this.operatorExpecting) {
|
|
||||||
this.node = {
|
|
||||||
type: 'operator',
|
|
||||||
value: chr
|
|
||||||
};
|
|
||||||
this.list.push(this.node);
|
|
||||||
this.node = null;
|
|
||||||
this.operatorExpecting = '';
|
|
||||||
this.escaped = false;
|
|
||||||
return;
|
|
||||||
} else if (!this.operatorExpecting && chr in OPERATORS) {
|
|
||||||
this.node = {
|
|
||||||
type: 'operator',
|
|
||||||
value: chr
|
|
||||||
};
|
|
||||||
this.list.push(this.node);
|
|
||||||
this.node = null;
|
|
||||||
this.operatorExpecting = OPERATORS[chr];
|
|
||||||
this.escaped = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.escaped && chr === '\\') {
|
|
||||||
this.escaped = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.node) {
|
|
||||||
this.node = {
|
|
||||||
type: 'text',
|
type: 'text',
|
||||||
value: ''
|
value: ''
|
||||||
|
},
|
||||||
|
escaped = false;
|
||||||
|
},
|
||||||
|
pushAddress = () => {
|
||||||
|
if (address.length) {
|
||||||
|
address = _handleAddress(address);
|
||||||
|
if (address.length) {
|
||||||
|
addresses = addresses.concat(address);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
address = [];
|
||||||
};
|
};
|
||||||
this.list.push(this.node);
|
|
||||||
|
[...str].forEach(chr => {
|
||||||
|
if (!escaped && (chr === endOperator || (!endOperator && chr in OPERATORS))) {
|
||||||
|
pushToken(node);
|
||||||
|
if (',' === chr || ';' === chr) {
|
||||||
|
pushAddress();
|
||||||
|
} else {
|
||||||
|
endOperator = endOperator ? '' : OPERATORS[chr];
|
||||||
|
if ('<' === chr) {
|
||||||
|
node.type = 'email';
|
||||||
|
} else if ('(' === chr) {
|
||||||
|
node.type = 'comment';
|
||||||
|
} else if (':' === chr) {
|
||||||
|
node.type = 'group';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
node.value += chr;
|
||||||
|
escaped = !escaped && '\\' === chr;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
pushToken(node);
|
||||||
|
|
||||||
|
pushAddress();
|
||||||
|
|
||||||
|
return addresses;
|
||||||
|
// return addresses.map(item => (item.name || item.email) ? new EmailModel(item.email, item.name) : null).filter(v => v);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts tokens for a single address into an address object
|
||||||
|
*
|
||||||
|
* @param {Array} tokens Tokens object
|
||||||
|
* @return {Object} Address object
|
||||||
|
*/
|
||||||
|
function _handleAddress(tokens) {
|
||||||
|
let
|
||||||
|
isGroup = false,
|
||||||
|
address = {},
|
||||||
|
addresses = [],
|
||||||
|
data = {
|
||||||
|
email: [],
|
||||||
|
comment: [],
|
||||||
|
group: [],
|
||||||
|
text: []
|
||||||
|
};
|
||||||
|
|
||||||
|
tokens.forEach(token => {
|
||||||
|
isGroup = isGroup || 'group' === token.type;
|
||||||
|
data[token.type].push(token.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
// If there is no text but a comment, replace the two
|
||||||
|
if (!data.text.length && data.comment.length) {
|
||||||
|
data.text = data.comment;
|
||||||
|
data.comment = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.escaped && chr !== '\\') {
|
if (isGroup) {
|
||||||
this.node.value += '\\';
|
// http://tools.ietf.org/html/rfc2822#appendix-A.1.3
|
||||||
|
/*
|
||||||
|
addresses.push({
|
||||||
|
email: '',
|
||||||
|
name: data.text.join(' ').trim(),
|
||||||
|
group: addressparser(data.group.join(','))
|
||||||
|
// ,comment: data.comment.join(' ').trim()
|
||||||
|
});
|
||||||
|
*/
|
||||||
|
addresses = addresses.concat(addressparser(data.group.join(',')));
|
||||||
|
} else {
|
||||||
|
// If no address was found, try to detect one from regular text
|
||||||
|
if (!data.email.length && data.text.length) {
|
||||||
|
var i = data.text.length;
|
||||||
|
while (i--) {
|
||||||
|
if (data.text[i].match(/^[^@\s]+@[^@\s]+$/)) {
|
||||||
|
data.email = data.text.splice(i, 1);
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.node.value += chr;
|
// still no address
|
||||||
this.escaped = false;
|
if (!data.email.length) {
|
||||||
|
i = data.text.length;
|
||||||
|
while (i--) {
|
||||||
|
data.text[i] = data.text[i].replace(/\s*\b[^@\s]+@[^@\s]+\b\s*/, address => {
|
||||||
|
if (!data.email.length) {
|
||||||
|
data.email = [address.trim()];
|
||||||
|
return '';
|
||||||
}
|
}
|
||||||
|
return address.trim();
|
||||||
|
});
|
||||||
|
if (data.email.length) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there's still no text but a comment exists, replace the two
|
||||||
|
if (!data.text.length && data.comment.length) {
|
||||||
|
data.text = data.comment;
|
||||||
|
data.comment = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep only the first address occurence, push others to regular text
|
||||||
|
if (data.email.length > 1) {
|
||||||
|
data.text = data.text.concat(data.email.splice(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
address = {
|
||||||
|
// Join values with spaces
|
||||||
|
email: data.email.join(' ').trim(),
|
||||||
|
name: data.text.join(' ').trim()
|
||||||
|
// ,comment: data.comment.join(' ').trim()
|
||||||
|
};
|
||||||
|
|
||||||
|
if (address.email === address.name) {
|
||||||
|
if (address.email.includes('@')) {
|
||||||
|
address.name = '';
|
||||||
|
} else {
|
||||||
|
address.email = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// address.email = address.email.replace(/^[<]+(.*)[>]+$/g, '$1');
|
||||||
|
|
||||||
|
addresses.push(address);
|
||||||
|
}
|
||||||
|
|
||||||
|
return addresses;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class EmailModel extends AbstractModel {
|
export class EmailModel extends AbstractModel {
|
||||||
|
|
@ -259,16 +205,13 @@ export class EmailModel extends AbstractModel {
|
||||||
* @param {string=} email = ''
|
* @param {string=} email = ''
|
||||||
* @param {string=} name = ''
|
* @param {string=} name = ''
|
||||||
* @param {string=} dkimStatus = 'none'
|
* @param {string=} dkimStatus = 'none'
|
||||||
* @param {string=} dkimValue = ''
|
|
||||||
*/
|
*/
|
||||||
constructor(email = '', name = '', dkimStatus = 'none', dkimValue = '') {
|
constructor(email, name, dkimStatus = 'none') {
|
||||||
super();
|
super();
|
||||||
this.email = email;
|
this.email = email || '';
|
||||||
this.name = name;
|
this.name = name || '';
|
||||||
this.dkimStatus = dkimStatus;
|
this.dkimStatus = dkimStatus;
|
||||||
this.dkimValue = dkimValue;
|
this.cleanup();
|
||||||
|
|
||||||
this.clearDuplicateName();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -278,61 +221,34 @@ export class EmailModel extends AbstractModel {
|
||||||
*/
|
*/
|
||||||
static reviveFromJson(json) {
|
static reviveFromJson(json) {
|
||||||
const email = super.reviveFromJson(json);
|
const email = super.reviveFromJson(json);
|
||||||
email?.clearDuplicateName();
|
email?.cleanup();
|
||||||
return email;
|
return email?.valid() ? email : null;
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
clear() {
|
|
||||||
this.email = '';
|
|
||||||
this.name = '';
|
|
||||||
|
|
||||||
this.dkimStatus = 'none';
|
|
||||||
this.dkimValue = '';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
*/
|
*/
|
||||||
validate() {
|
valid() {
|
||||||
return this.name || this.email;
|
return this.name || this.email;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {boolean} withoutName = false
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
hash(withoutName = false) {
|
|
||||||
return '#' + (withoutName ? '' : this.name) + '#' + this.email + '#';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
clearDuplicateName() {
|
cleanup() {
|
||||||
if (this.name === this.email) {
|
if (this.name === this.email) {
|
||||||
this.name = '';
|
this.name = '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {string} query
|
|
||||||
* @returns {boolean}
|
|
||||||
*/
|
|
||||||
search(query) {
|
|
||||||
return (this.name + ' ' + this.email).toLowerCase().includes(query.toLowerCase());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {boolean} friendlyView = false
|
* @param {boolean} friendlyView = false
|
||||||
* @param {boolean} wrapWithLink = false
|
* @param {boolean} wrapWithLink = false
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
toLine(friendlyView, wrapWithLink) {
|
toLine(friendlyView, wrapWithLink) {
|
||||||
let result = this.email,
|
let name = this.name,
|
||||||
name = this.name,
|
result = this.email,
|
||||||
toLink = text =>
|
toLink = text =>
|
||||||
'<a href="mailto:'
|
'<a href="mailto:'
|
||||||
+ encodeHtml(result) + (name ? '?to=' + encodeURIComponent('"' + name + '" <' + result + '>') : '')
|
+ encodeHtml(result) + (name ? '?to=' + encodeURIComponent('"' + name + '" <' + result + '>') : '')
|
||||||
|
|
@ -351,47 +267,6 @@ export class EmailModel extends AbstractModel {
|
||||||
result = toLink();
|
result = toLink();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return result;
|
return result || name;
|
||||||
}
|
|
||||||
|
|
||||||
static splitEmailLine(line) {
|
|
||||||
const result = [];
|
|
||||||
let exists = false;
|
|
||||||
addressparser(line).forEach(item => {
|
|
||||||
const address = item.address
|
|
||||||
? new EmailModel(item.address.replace(/^[<]+(.*)[>]+$/g, '$1'), item.name || '')
|
|
||||||
: null;
|
|
||||||
|
|
||||||
if (address?.email) {
|
|
||||||
exists = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
result.push(address ? address.toLine() : item.name);
|
|
||||||
});
|
|
||||||
return exists ? result : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
static parseEmailLine(line) {
|
|
||||||
return addressparser(line).map(item =>
|
|
||||||
item.address ? new EmailModel(item.address.replace(/^[<]+(.*)[>]+$/g, '$1'), item.name || '') : null
|
|
||||||
).filter(v => v);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {string} emailAddress
|
|
||||||
* @returns {boolean}
|
|
||||||
*/
|
|
||||||
parse(emailAddress) {
|
|
||||||
emailAddress = emailAddress.trim();
|
|
||||||
if (emailAddress) {
|
|
||||||
const result = addressparser(emailAddress);
|
|
||||||
if (result.length) {
|
|
||||||
this.name = result[0].name || '';
|
|
||||||
this.email = result[0].address || '';
|
|
||||||
this.clearDuplicateName();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { AbstractCollectionModel } from 'Model/AbstractCollection';
|
import { AbstractCollectionModel } from 'Model/AbstractCollection';
|
||||||
import { EmailModel } from 'Model/Email';
|
import { EmailModel, addressparser } from 'Model/Email';
|
||||||
|
import { forEachObjectValue } from 'Common/Utils';
|
||||||
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
|
@ -13,6 +14,16 @@ export class EmailCollectionModel extends AbstractCollectionModel
|
||||||
return super.reviveFromJson(items, email => EmailModel.reviveFromJson(email));
|
return super.reviveFromJson(items, email => EmailModel.reviveFromJson(email));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} text
|
||||||
|
* @returns {EmailCollectionModel}
|
||||||
|
*/
|
||||||
|
static fromString(str) {
|
||||||
|
let list = new this();
|
||||||
|
list.fromString(str);
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {boolean=} friendlyView = false
|
* @param {boolean=} friendlyView = false
|
||||||
* @param {boolean=} wrapWithLink = false
|
* @param {boolean=} wrapWithLink = false
|
||||||
|
|
@ -21,4 +32,23 @@ export class EmailCollectionModel extends AbstractCollectionModel
|
||||||
toString(friendlyView, wrapWithLink) {
|
toString(friendlyView, wrapWithLink) {
|
||||||
return this.map(email => email.toLine(friendlyView, wrapWithLink)).join(', ');
|
return this.map(email => email.toLine(friendlyView, wrapWithLink)).join(', ');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} text
|
||||||
|
*/
|
||||||
|
fromString(str) {
|
||||||
|
if (str) {
|
||||||
|
let items = {}, key;
|
||||||
|
addressparser(str).forEach(item => {
|
||||||
|
item = new EmailModel(item.email, item.name);
|
||||||
|
// Make them unique
|
||||||
|
key = item.email || item.name;
|
||||||
|
if (key && (item.name || !items[key])) {
|
||||||
|
items[key] = item;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
forEachObjectValue(items, item => this.push(item));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,9 @@ import { AbstractCollectionModel } from 'Model/AbstractCollection';
|
||||||
import { UNUSED_OPTION_VALUE } from 'Common/Consts';
|
import { UNUSED_OPTION_VALUE } from 'Common/Consts';
|
||||||
import { isArray, getKeyByValue, forEachObjectEntry, b64EncodeJSONSafe } from 'Common/Utils';
|
import { isArray, getKeyByValue, forEachObjectEntry, b64EncodeJSONSafe } from 'Common/Utils';
|
||||||
import { ClientSideKeyNameExpandedFolders, FolderType, FolderMetadataKeys } from 'Common/EnumsUser';
|
import { ClientSideKeyNameExpandedFolders, FolderType, FolderMetadataKeys } from 'Common/EnumsUser';
|
||||||
import { getFolderFromCacheList, setFolder, setFolderInboxName } from 'Common/Cache';
|
import { clearCache, getFolderFromCacheList, setFolder, setFolderInboxName, removeFolderFromCacheList } from 'Common/Cache';
|
||||||
import { Settings, SettingsGet, fireEvent } from 'Common/Globals';
|
import { Settings, SettingsGet, fireEvent } from 'Common/Globals';
|
||||||
|
import { Notifications } from 'Common/Enums';
|
||||||
|
|
||||||
import * as Local from 'Storage/Client';
|
import * as Local from 'Storage/Client';
|
||||||
|
|
||||||
|
|
@ -14,18 +15,18 @@ import { MessagelistUserStore } from 'Stores/User/Messagelist';
|
||||||
import { SettingsUserStore } from 'Stores/User/Settings';
|
import { SettingsUserStore } from 'Stores/User/Settings';
|
||||||
|
|
||||||
import { sortFolders } from 'Common/Folders';
|
import { sortFolders } from 'Common/Folders';
|
||||||
import { i18n, translateTrigger } from 'Common/Translator';
|
import { i18n, translateTrigger, getNotification } from 'Common/Translator';
|
||||||
|
|
||||||
import { AbstractModel } from 'Knoin/AbstractModel';
|
import { AbstractModel } from 'Knoin/AbstractModel';
|
||||||
|
|
||||||
import { koComputable, addObservablesTo } from 'External/ko';
|
import { /*koComputable,*/ addObservablesTo } from 'External/ko';
|
||||||
|
|
||||||
//import { mailBox } from 'Common/Links';
|
//import { mailBox } from 'Common/Links';
|
||||||
|
|
||||||
import Remote from 'Remote/User/Fetch';
|
import Remote from 'Remote/User/Fetch';
|
||||||
|
|
||||||
const
|
const
|
||||||
isPosNumeric = value => null != value && /^[0-9]*$/.test(value.toString()),
|
// isPosNumeric = value => null != value && /^[0-9]*$/.test(value.toString()),
|
||||||
|
|
||||||
normalizeFolder = sFolderFullName => ('' === sFolderFullName
|
normalizeFolder = sFolderFullName => ('' === sFolderFullName
|
||||||
|| UNUSED_OPTION_VALUE === sFolderFullName
|
|| UNUSED_OPTION_VALUE === sFolderFullName
|
||||||
|
|
@ -37,7 +38,7 @@ const
|
||||||
Inbox: 0,
|
Inbox: 0,
|
||||||
Sent: 0,
|
Sent: 0,
|
||||||
Drafts: 0,
|
Drafts: 0,
|
||||||
Spam: 0,
|
Junk: 0, // Spam
|
||||||
Trash: 0,
|
Trash: 0,
|
||||||
Archive: 0
|
Archive: 0
|
||||||
},
|
},
|
||||||
|
|
@ -62,7 +63,7 @@ const
|
||||||
case FolderType.Trash:
|
case FolderType.Trash:
|
||||||
case FolderType.Archive:
|
case FolderType.Archive:
|
||||||
return i18n('FOLDER_LIST/' + getKeyByValue(FolderType, type).toUpperCase() + '_NAME');
|
return i18n('FOLDER_LIST/' + getKeyByValue(FolderType, type).toUpperCase() + '_NAME');
|
||||||
case FolderType.Spam:
|
case FolderType.Junk:
|
||||||
return i18n('GLOBAL/SPAM');
|
return i18n('GLOBAL/SPAM');
|
||||||
// no default
|
// no default
|
||||||
}
|
}
|
||||||
|
|
@ -91,6 +92,7 @@ export const
|
||||||
Remote.abort('Folders')
|
Remote.abort('Folders')
|
||||||
.post('Folders', FolderUserStore.foldersLoading)
|
.post('Folders', FolderUserStore.foldersLoading)
|
||||||
.then(data => {
|
.then(data => {
|
||||||
|
clearCache();
|
||||||
FolderCollectionModel.reviveFromJson(data.Result)?.storeIt();
|
FolderCollectionModel.reviveFromJson(data.Result)?.storeIt();
|
||||||
fCallback?.(true);
|
fCallback?.(true);
|
||||||
// Repeat every 15 minutes?
|
// Repeat every 15 minutes?
|
||||||
|
|
@ -104,12 +106,11 @@ export class FolderCollectionModel extends AbstractCollectionModel
|
||||||
/*
|
/*
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.CountRec
|
this.quotaUsage;
|
||||||
this.IsThreadsSupported
|
this.quotaLimit;
|
||||||
this.Namespace;
|
this.namespace;
|
||||||
this.Optimized
|
this.optimized
|
||||||
this.SystemFolders
|
this.capabilities
|
||||||
this.Capabilities
|
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
@ -119,20 +120,17 @@ export class FolderCollectionModel extends AbstractCollectionModel
|
||||||
*/
|
*/
|
||||||
static reviveFromJson(object) {
|
static reviveFromJson(object) {
|
||||||
const expandedFolders = Local.get(ClientSideKeyNameExpandedFolders);
|
const expandedFolders = Local.get(ClientSideKeyNameExpandedFolders);
|
||||||
if (object?.SystemFolders) {
|
|
||||||
forEachObjectEntry(SystemFolders, key =>
|
forEachObjectEntry(SystemFolders, (key, value) =>
|
||||||
SystemFolders[key] = SettingsGet(key+'Folder') || object.SystemFolders[FolderType[key]]
|
value || (SystemFolders[key] = SettingsGet(key+'Folder'))
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
const result = super.reviveFromJson(object, oFolder => {
|
const result = super.reviveFromJson(object, oFolder => {
|
||||||
let oCacheFolder = getFolderFromCacheList(oFolder.FullName),
|
let oCacheFolder = getFolderFromCacheList(oFolder.fullName);
|
||||||
type = FolderType[getKeyByValue(SystemFolders, oFolder.FullName)];
|
|
||||||
|
|
||||||
if (oCacheFolder) {
|
if (oCacheFolder) {
|
||||||
// oCacheFolder.revivePropertiesFromJson(oFolder);
|
// oCacheFolder.revivePropertiesFromJson(oFolder);
|
||||||
if (oFolder.Hash) {
|
if (oFolder.etag) {
|
||||||
oCacheFolder.hash = oFolder.Hash;
|
oCacheFolder.etag = oFolder.etag;
|
||||||
}
|
}
|
||||||
if (null != oFolder.totalEmails) {
|
if (null != oFolder.totalEmails) {
|
||||||
oCacheFolder.totalEmails(oFolder.totalEmails);
|
oCacheFolder.totalEmails(oFolder.totalEmails);
|
||||||
|
|
@ -144,17 +142,59 @@ export class FolderCollectionModel extends AbstractCollectionModel
|
||||||
oCacheFolder = FolderModel.reviveFromJson(oFolder);
|
oCacheFolder = FolderModel.reviveFromJson(oFolder);
|
||||||
if (!oCacheFolder)
|
if (!oCacheFolder)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
if (1 == type) {
|
|
||||||
oCacheFolder.type(type);
|
|
||||||
setFolderInboxName(oFolder.FullName);
|
|
||||||
}
|
|
||||||
setFolder(oCacheFolder);
|
setFolder(oCacheFolder);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (1 < type) {
|
// JMAP RFC 8621
|
||||||
oCacheFolder.type(type);
|
let role = oFolder.role;
|
||||||
|
/*
|
||||||
|
if (!role) {
|
||||||
|
// Kolab
|
||||||
|
let type = oFolder.metadata[FolderMetadataKeys.KolabFolderType]
|
||||||
|
|| oFolder.metadata[FolderMetadataKeys.KolabFolderTypeShared];
|
||||||
|
switch (type) {
|
||||||
|
case 'mail.inbox':
|
||||||
|
case 'mail.drafts':
|
||||||
|
role = type.replace('mail.', '');
|
||||||
|
break;
|
||||||
|
// case 'mail.outbox':
|
||||||
|
case 'mail.sentitems':
|
||||||
|
role = 'sent';
|
||||||
|
break;
|
||||||
|
case 'mail.junkemail':
|
||||||
|
role = 'spam';
|
||||||
|
break;
|
||||||
|
case 'mail.wastebasket':
|
||||||
|
role = 'trash';
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
// Flags
|
||||||
|
if (oFolder.attributes.includes('\\sentmail')) {
|
||||||
|
role = 'sent';
|
||||||
|
}
|
||||||
|
if (oFolder.attributes.includes('\\spam')) {
|
||||||
|
role = 'junk';
|
||||||
|
}
|
||||||
|
if (oFolder.attributes.includes('\\bin')) {
|
||||||
|
role = 'trash';
|
||||||
|
}
|
||||||
|
if (oFolder.attributes.includes('\\important')) {
|
||||||
|
role = 'important';
|
||||||
|
}
|
||||||
|
if (oFolder.attributes.includes('\\starred')) {
|
||||||
|
role = 'flagged';
|
||||||
|
}
|
||||||
|
if (oFolder.attributes.includes('\\all') || oFolder.flags.includes('\\allmail')) {
|
||||||
|
role = 'all';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
if (role) {
|
||||||
|
role = role[0].toUpperCase() + role.slice(1);
|
||||||
|
SystemFolders[role] || (SystemFolders[role] = oFolder.fullName);
|
||||||
|
}
|
||||||
|
|
||||||
|
oCacheFolder.type(FolderType[getKeyByValue(SystemFolders, oFolder.fullName)] || 0);
|
||||||
|
|
||||||
oCacheFolder.collapsed(!expandedFolders
|
oCacheFolder.collapsed(!expandedFolders
|
||||||
|| !isArray(expandedFolders)
|
|| !isArray(expandedFolders)
|
||||||
|
|
@ -163,12 +203,42 @@ export class FolderCollectionModel extends AbstractCollectionModel
|
||||||
return oCacheFolder;
|
return oCacheFolder;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
result.CountRec = result.length;
|
||||||
|
setFolderInboxName(SystemFolders.Inbox);
|
||||||
|
|
||||||
let i = result.length;
|
let i = result.length;
|
||||||
if (i) {
|
if (i) {
|
||||||
sortFolders(result);
|
sortFolders(result);
|
||||||
try {
|
try {
|
||||||
while (i--) {
|
while (i--) {
|
||||||
let folder = result[i], parent = getFolderFromCacheList(folder.parentName);
|
let folder = result[i], parent = getFolderFromCacheList(folder.parentName);
|
||||||
|
if (!parent) {
|
||||||
|
// Create NonExistent parent folders
|
||||||
|
let delimiter = folder.delimiter;
|
||||||
|
if (delimiter) {
|
||||||
|
let parents = folder.fullName.split(delimiter);
|
||||||
|
parents.pop();
|
||||||
|
while (parents.length) {
|
||||||
|
let parentName = parents.join(delimiter),
|
||||||
|
name = parents.pop(),
|
||||||
|
pfolder = getFolderFromCacheList(parentName);
|
||||||
|
if (!pfolder) {
|
||||||
|
console.log('Create nonexistent folder ' + parentName);
|
||||||
|
pfolder = FolderModel.reviveFromJson({
|
||||||
|
'@Object': 'Object/Folder',
|
||||||
|
name: name,
|
||||||
|
fullName: parentName,
|
||||||
|
delimiter: delimiter,
|
||||||
|
attributes: ['\\nonexistent']
|
||||||
|
});
|
||||||
|
setFolder(pfolder);
|
||||||
|
result.splice(i, 0, pfolder);
|
||||||
|
++i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
parent = getFolderFromCacheList(folder.parentName);
|
||||||
|
}
|
||||||
|
}
|
||||||
if (parent) {
|
if (parent) {
|
||||||
parent.subFolders.unshift(folder);
|
parent.subFolders.unshift(folder);
|
||||||
result.splice(i,1);
|
result.splice(i,1);
|
||||||
|
|
@ -188,7 +258,7 @@ export class FolderCollectionModel extends AbstractCollectionModel
|
||||||
if (!(
|
if (!(
|
||||||
SettingsGet('SentFolder') +
|
SettingsGet('SentFolder') +
|
||||||
SettingsGet('DraftsFolder') +
|
SettingsGet('DraftsFolder') +
|
||||||
SettingsGet('SpamFolder') +
|
SettingsGet('JunkFolder') +
|
||||||
SettingsGet('TrashFolder') +
|
SettingsGet('TrashFolder') +
|
||||||
SettingsGet('ArchiveFolder')
|
SettingsGet('ArchiveFolder')
|
||||||
)
|
)
|
||||||
|
|
@ -198,18 +268,19 @@ export class FolderCollectionModel extends AbstractCollectionModel
|
||||||
|
|
||||||
FolderUserStore.folderList(this);
|
FolderUserStore.folderList(this);
|
||||||
|
|
||||||
FolderUserStore.namespace = this.Namespace;
|
FolderUserStore.namespace = this.namespace;
|
||||||
|
|
||||||
AppUserStore.threadsAllowed(!!(Settings.app('useImapThread') && this.IsThreadsSupported));
|
// 'THREAD=REFS', 'THREAD=REFERENCES', 'THREAD=ORDEREDSUBJECT'
|
||||||
|
AppUserStore.threadsAllowed(!!this.capabilities.some(capa => capa.startsWith('THREAD=')));
|
||||||
|
|
||||||
FolderUserStore.folderListOptimized(!!this.Optimized);
|
// FolderUserStore.folderListOptimized(!!this.optimized);
|
||||||
FolderUserStore.quotaUsage(this.quotaUsage);
|
FolderUserStore.quotaUsage(this.quotaUsage);
|
||||||
FolderUserStore.quotaLimit(this.quotaLimit);
|
FolderUserStore.quotaLimit(this.quotaLimit);
|
||||||
FolderUserStore.capabilities(this.Capabilities);
|
FolderUserStore.capabilities(this.capabilities);
|
||||||
|
|
||||||
FolderUserStore.sentFolder(normalizeFolder(SystemFolders.Sent));
|
FolderUserStore.sentFolder(normalizeFolder(SystemFolders.Sent));
|
||||||
FolderUserStore.draftsFolder(normalizeFolder(SystemFolders.Drafts));
|
FolderUserStore.draftsFolder(normalizeFolder(SystemFolders.Drafts));
|
||||||
FolderUserStore.spamFolder(normalizeFolder(SystemFolders.Spam));
|
FolderUserStore.spamFolder(normalizeFolder(SystemFolders.Junk));
|
||||||
FolderUserStore.trashFolder(normalizeFolder(SystemFolders.Trash));
|
FolderUserStore.trashFolder(normalizeFolder(SystemFolders.Trash));
|
||||||
FolderUserStore.archiveFolder(normalizeFolder(SystemFolders.Archive));
|
FolderUserStore.archiveFolder(normalizeFolder(SystemFolders.Archive));
|
||||||
|
|
||||||
|
|
@ -230,13 +301,14 @@ export class FolderModel extends AbstractModel {
|
||||||
|
|
||||||
this.exists = true;
|
this.exists = true;
|
||||||
|
|
||||||
this.hash = '';
|
this.etag = '';
|
||||||
// this.id = null;
|
this.id = 0;
|
||||||
this.uidNext = null;
|
this.uidNext = 0;
|
||||||
|
|
||||||
addObservablesTo(this, {
|
addObservablesTo(this, {
|
||||||
name: '',
|
name: '',
|
||||||
type: FolderType.User,
|
type: 0,
|
||||||
|
role: null,
|
||||||
selectable: false,
|
selectable: false,
|
||||||
|
|
||||||
focused: false,
|
focused: false,
|
||||||
|
|
@ -249,8 +321,8 @@ export class FolderModel extends AbstractModel {
|
||||||
nameForEdit: '',
|
nameForEdit: '',
|
||||||
errorMsg: '',
|
errorMsg: '',
|
||||||
|
|
||||||
totalEmailsValue: 0,
|
totalEmails: 0,
|
||||||
unreadEmailsValue: 0,
|
unreadEmails: 0,
|
||||||
|
|
||||||
kolabType: null,
|
kolabType: null,
|
||||||
|
|
||||||
|
|
@ -259,17 +331,20 @@ export class FolderModel extends AbstractModel {
|
||||||
tagsAllowed: false
|
tagsAllowed: false
|
||||||
});
|
});
|
||||||
|
|
||||||
this.flags = ko.observableArray();
|
this.attributes = ko.observableArray();
|
||||||
|
// For messages
|
||||||
this.permanentFlags = ko.observableArray();
|
this.permanentFlags = ko.observableArray();
|
||||||
|
|
||||||
this.addSubscribables({
|
this.addSubscribables({
|
||||||
kolabType: sValue => this.metadata[FolderMetadataKeys.KolabFolderType] = sValue,
|
kolabType: sValue => this.metadata[FolderMetadataKeys.KolabFolderType] = sValue,
|
||||||
permanentFlags: aValue => this.tagsAllowed(aValue.includes('\\*'))
|
permanentFlags: aValue => this.tagsAllowed(aValue.includes('\\*')),
|
||||||
|
editing: value => value && this.nameForEdit(this.name()),
|
||||||
|
unreadEmails: unread => FolderType.Inbox === this.type() && fireEvent('mailbox.inbox-unread-count', unread)
|
||||||
});
|
});
|
||||||
|
|
||||||
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 });
|
||||||
|
/*
|
||||||
this.totalEmails = koComputable({
|
this.totalEmails = koComputable({
|
||||||
read: this.totalEmailsValue,
|
read: this.totalEmailsValue,
|
||||||
write: iValue =>
|
write: iValue =>
|
||||||
|
|
@ -283,6 +358,7 @@ export class FolderModel extends AbstractModel {
|
||||||
isPosNumeric(value) ? this.unreadEmailsValue(value) : this.unreadEmailsValue.valueHasMutated()
|
isPosNumeric(value) ? this.unreadEmailsValue(value) : this.unreadEmailsValue.valueHasMutated()
|
||||||
})
|
})
|
||||||
.extend({ notify: 'always' });
|
.extend({ notify: 'always' });
|
||||||
|
*/
|
||||||
/*
|
/*
|
||||||
https://www.rfc-editor.org/rfc/rfc8621.html#section-2
|
https://www.rfc-editor.org/rfc/rfc8621.html#section-2
|
||||||
"myRights": {
|
"myRights": {
|
||||||
|
|
@ -297,6 +373,145 @@ export class FolderModel extends AbstractModel {
|
||||||
"mayReadItems": true
|
"mayReadItems": true
|
||||||
},
|
},
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
this.addComputables({
|
||||||
|
|
||||||
|
isInbox: () => FolderType.Inbox === this.type(),
|
||||||
|
|
||||||
|
isFlagged: () => FolderUserStore.currentFolder() === this
|
||||||
|
&& MessagelistUserStore.listSearch().includes('flagged'),
|
||||||
|
|
||||||
|
// isSubscribed: () => this.attributes().includes('\\subscribed'),
|
||||||
|
|
||||||
|
hasVisibleSubfolders: () => !!this.subFolders().find(folder => folder.visible()),
|
||||||
|
|
||||||
|
hasSubscriptions: () => this.isSubscribed() | !!this.subFolders().find(
|
||||||
|
oFolder => {
|
||||||
|
const subscribed = oFolder.hasSubscriptions();
|
||||||
|
return !oFolder.isSystemFolder() && subscribed;
|
||||||
|
}
|
||||||
|
),
|
||||||
|
|
||||||
|
canBeEdited: () => !this.type() && this.exists/* && this.selectable()*/,
|
||||||
|
|
||||||
|
isSystemFolder: () => this.type()
|
||||||
|
| (FolderUserStore.allowKolab() && !!this.kolabType() & !SettingsUserStore.unhideKolabFolders()),
|
||||||
|
|
||||||
|
canBeSelected: () => this.selectable() && !this.isSystemFolder(),
|
||||||
|
|
||||||
|
canBeDeleted: () => this.canBeSelected() && this.exists,
|
||||||
|
|
||||||
|
canBeSubscribed: () => this.selectable()
|
||||||
|
&& !(this.isSystemFolder() | !SettingsUserStore.hideUnsubscribed()),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Folder is visible when:
|
||||||
|
* - hasVisibleSubfolders()
|
||||||
|
* Or when all below conditions are true:
|
||||||
|
* - selectable()
|
||||||
|
* - isSubscribed() OR hideUnsubscribed = false
|
||||||
|
* - 0 == type()
|
||||||
|
* - not kolabType()
|
||||||
|
*/
|
||||||
|
visible: () => {
|
||||||
|
const selectable = this.canBeSelected(),
|
||||||
|
name = this.name(),
|
||||||
|
filter = foldersFilter(),
|
||||||
|
visible = (this.isSubscribed() | !SettingsUserStore.hideUnsubscribed())
|
||||||
|
&& selectable
|
||||||
|
&& (!filter || name.toLowerCase().includes(filter.toLowerCase()));
|
||||||
|
return this.hasVisibleSubfolders() | visible;
|
||||||
|
},
|
||||||
|
|
||||||
|
unreadCount: () => this.unreadEmails() || null,
|
||||||
|
/*
|
||||||
|
{
|
||||||
|
// TODO: make this optional in Settings
|
||||||
|
// https://github.com/the-djmaze/snappymail/issues/457
|
||||||
|
// https://github.com/the-djmaze/snappymail/issues/567
|
||||||
|
const
|
||||||
|
unread = this.unreadEmails(),
|
||||||
|
type = this.type();
|
||||||
|
// return ((!this.isSystemFolder() || type == FolderType.Inbox) && unread) ? unread : null;
|
||||||
|
},
|
||||||
|
*/
|
||||||
|
|
||||||
|
localName: () => {
|
||||||
|
let name = this.name();
|
||||||
|
if (this.isSystemFolder()) {
|
||||||
|
translateTrigger();
|
||||||
|
name = getSystemFolderName(this.type(), name);
|
||||||
|
}
|
||||||
|
return name;
|
||||||
|
},
|
||||||
|
|
||||||
|
nameInfo: () => {
|
||||||
|
if (this.isSystemFolder()) {
|
||||||
|
translateTrigger();
|
||||||
|
let suffix = getSystemFolderName(this.type(), getKolabFolderName(this.kolabType()));
|
||||||
|
if (this.name() !== suffix && 'inbox' !== suffix.toLowerCase()) {
|
||||||
|
return ' (' + suffix + ')';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
},
|
||||||
|
|
||||||
|
detailedName: () => this.name() + ' ' + this.nameInfo(),
|
||||||
|
|
||||||
|
hasSubscribedUnreadMessagesSubfolders: () =>
|
||||||
|
!!this.subFolders().find(
|
||||||
|
folder => folder.unreadCount() | folder.hasSubscribedUnreadMessagesSubfolders()
|
||||||
|
)
|
||||||
|
/*
|
||||||
|
!!this.subFolders().filter(
|
||||||
|
folder => folder.unreadCount() | folder.hasSubscribedUnreadMessagesSubfolders()
|
||||||
|
).length
|
||||||
|
*/
|
||||||
|
// ,href: () => this.canBeSelected() && mailBox(this.fullNameHash)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
edit() {
|
||||||
|
this.canBeEdited() && this.editing(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
unedit() {
|
||||||
|
this.editing(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
rename() {
|
||||||
|
const folder = this,
|
||||||
|
nameToEdit = folder.nameForEdit().trim();
|
||||||
|
if (nameToEdit && folder.name() !== nameToEdit) {
|
||||||
|
Remote.abort('Folders').post('FolderRename', FolderUserStore.foldersRenaming, {
|
||||||
|
folder: folder.fullName,
|
||||||
|
newFolderName: nameToEdit,
|
||||||
|
subscribe: folder.isSubscribed() ? 1 : 0
|
||||||
|
})
|
||||||
|
.then(data => {
|
||||||
|
folder.name(nameToEdit/*data.name*/);
|
||||||
|
if (folder.subFolders.length) {
|
||||||
|
Remote.setTrigger(FolderUserStore.foldersLoading, true);
|
||||||
|
// clearTimeout(Remote.foldersTimeout);
|
||||||
|
// Remote.foldersTimeout = setTimeout(loadFolders, 500);
|
||||||
|
setTimeout(loadFolders, 500);
|
||||||
|
// TODO: rename all subfolders with folder.delimiter to prevent reload?
|
||||||
|
} else {
|
||||||
|
removeFolderFromCacheList(folder.fullName);
|
||||||
|
folder.fullName = data.Result.fullName;
|
||||||
|
setFolder(folder);
|
||||||
|
const parent = getFolderFromCacheList(folder.parentName);
|
||||||
|
sortFolders(parent ? parent.subFolders : FolderUserStore.folderList);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
FolderUserStore.folderListError(
|
||||||
|
getNotification(error.code, '', Notifications.CantRenameFolder)
|
||||||
|
+ '.\n' + error.message);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
folder.editing(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -316,6 +531,7 @@ export class FolderModel extends AbstractModel {
|
||||||
const folder = super.reviveFromJson(json);
|
const folder = super.reviveFromJson(json);
|
||||||
if (folder) {
|
if (folder) {
|
||||||
const path = folder.fullName.split(folder.delimiter),
|
const path = folder.fullName.split(folder.delimiter),
|
||||||
|
attr = name => folder.attributes.includes(name),
|
||||||
type = (folder.metadata[FolderMetadataKeys.KolabFolderType]
|
type = (folder.metadata[FolderMetadataKeys.KolabFolderType]
|
||||||
|| folder.metadata[FolderMetadataKeys.KolabFolderTypeShared]
|
|| folder.metadata[FolderMetadataKeys.KolabFolderTypeShared]
|
||||||
|| ''
|
|| ''
|
||||||
|
|
@ -325,105 +541,11 @@ export class FolderModel extends AbstractModel {
|
||||||
path.pop();
|
path.pop();
|
||||||
folder.parentName = path.join(folder.delimiter);
|
folder.parentName = path.join(folder.delimiter);
|
||||||
|
|
||||||
|
folder.isSubscribed(attr('\\subscribed'));
|
||||||
|
folder.exists = !attr('\\nonexistent');
|
||||||
|
folder.selectable(folder.exists && !attr('\\noselect'));
|
||||||
|
|
||||||
type && 'mail' != type && folder.kolabType(type);
|
type && 'mail' != type && folder.kolabType(type);
|
||||||
|
|
||||||
folder.addComputables({
|
|
||||||
|
|
||||||
isInbox: () => FolderType.Inbox === folder.type(),
|
|
||||||
|
|
||||||
isFlagged: () => FolderUserStore.currentFolder() === folder
|
|
||||||
&& MessagelistUserStore.listSearch().includes('flagged'),
|
|
||||||
|
|
||||||
hasVisibleSubfolders: () => !!folder.subFolders().find(folder => folder.visible()),
|
|
||||||
|
|
||||||
hasSubscriptions: () => folder.isSubscribed() | !!folder.subFolders().find(
|
|
||||||
oFolder => {
|
|
||||||
const subscribed = oFolder.hasSubscriptions();
|
|
||||||
return !oFolder.isSystemFolder() && subscribed;
|
|
||||||
}
|
|
||||||
),
|
|
||||||
|
|
||||||
canBeEdited: () => FolderType.User === folder.type() && folder.exists/* && folder.selectable()*/,
|
|
||||||
|
|
||||||
isSystemFolder: () => FolderType.User !== folder.type()
|
|
||||||
| (FolderUserStore.allowKolab() && !!folder.kolabType() & !SettingsUserStore.unhideKolabFolders()),
|
|
||||||
|
|
||||||
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()
|
|
||||||
* - isSubscribed() OR hideUnsubscribed = false
|
|
||||||
* - FolderType.User
|
|
||||||
* - not kolabType()
|
|
||||||
*/
|
|
||||||
visible: () => {
|
|
||||||
const selectable = folder.canBeSelected(),
|
|
||||||
name = folder.name(),
|
|
||||||
filter = foldersFilter(),
|
|
||||||
visible = (folder.isSubscribed() | !SettingsUserStore.hideUnsubscribed())
|
|
||||||
&& selectable
|
|
||||||
&& (!filter || name.toLowerCase().includes(filter.toLowerCase()));
|
|
||||||
return folder.hasVisibleSubfolders() | visible;
|
|
||||||
},
|
|
||||||
|
|
||||||
printableUnreadCount: () => folder.unreadEmails() || null,
|
|
||||||
/*
|
|
||||||
{
|
|
||||||
// TODO: make this optional in Settings
|
|
||||||
// https://github.com/the-djmaze/snappymail/issues/457
|
|
||||||
// https://github.com/the-djmaze/snappymail/issues/567
|
|
||||||
const
|
|
||||||
unread = folder.unreadEmails(),
|
|
||||||
type = folder.type();
|
|
||||||
// return ((!folder.isSystemFolder() || type == FolderType.Inbox) && unread) ? unread : null;
|
|
||||||
},
|
|
||||||
*/
|
|
||||||
|
|
||||||
localName: () => {
|
|
||||||
let name = folder.name();
|
|
||||||
if (folder.isSystemFolder()) {
|
|
||||||
translateTrigger();
|
|
||||||
name = getSystemFolderName(folder.type(), name);
|
|
||||||
}
|
|
||||||
return name;
|
|
||||||
},
|
|
||||||
|
|
||||||
manageFolderSystemName: () => {
|
|
||||||
if (folder.isSystemFolder()) {
|
|
||||||
translateTrigger();
|
|
||||||
let suffix = getSystemFolderName(folder.type(), getKolabFolderName(folder.kolabType()));
|
|
||||||
if (folder.name() !== suffix && 'inbox' !== suffix.toLowerCase()) {
|
|
||||||
return '(' + suffix + ')';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return '';
|
|
||||||
},
|
|
||||||
|
|
||||||
hasSubscribedUnreadMessagesSubfolders: () =>
|
|
||||||
!!folder.subFolders().find(
|
|
||||||
folder => folder.printableUnreadCount() | folder.hasSubscribedUnreadMessagesSubfolders()
|
|
||||||
)
|
|
||||||
/*
|
|
||||||
!!folder.subFolders().filter(
|
|
||||||
folder => folder.printableUnreadCount() | folder.hasSubscribedUnreadMessagesSubfolders()
|
|
||||||
).length
|
|
||||||
*/
|
|
||||||
// ,href: () => folder.canBeSelected() && mailBox(folder.fullNameHash)
|
|
||||||
});
|
|
||||||
|
|
||||||
folder.addSubscribables({
|
|
||||||
editing: value => value && folder.nameForEdit(folder.name()),
|
|
||||||
|
|
||||||
unreadEmails: unread => FolderType.Inbox === folder.type() && fireEvent('mailbox.inbox-unread-count', unread)
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
return folder;
|
return folder;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,12 +6,12 @@ export class IdentityModel extends AbstractModel {
|
||||||
* @param {string} id
|
* @param {string} id
|
||||||
* @param {string} email
|
* @param {string} email
|
||||||
*/
|
*/
|
||||||
constructor(id, email) {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
addObservablesTo(this, {
|
addObservablesTo(this, {
|
||||||
id: id || '',
|
id: '',
|
||||||
email: email,
|
email: '',
|
||||||
name: '',
|
name: '',
|
||||||
|
|
||||||
replyTo: '',
|
replyTo: '',
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,10 @@
|
||||||
import ko from 'ko';
|
import ko from 'ko';
|
||||||
|
|
||||||
import { MessagePriority } from 'Common/EnumsUser';
|
|
||||||
import { i18n } from 'Common/Translator';
|
import { i18n } from 'Common/Translator';
|
||||||
|
|
||||||
import { doc, SettingsGet } from 'Common/Globals';
|
import { doc, SettingsGet } from 'Common/Globals';
|
||||||
import { encodeHtml, plainToHtml, htmlToPlain, cleanHtml } from 'Common/Html';
|
import { encodeHtml, plainToHtml, htmlToPlain, cleanHtml } from 'Common/Html';
|
||||||
import { arrayLength, forEachObjectEntry } from 'Common/Utils';
|
import { forEachObjectEntry, b64EncodeJSONSafe } from 'Common/Utils';
|
||||||
import { serverRequestRaw, proxy } from 'Common/Links';
|
import { serverRequestRaw, proxy } from 'Common/Links';
|
||||||
import { addObservablesTo, addComputablesTo } from 'External/ko';
|
import { addObservablesTo, addComputablesTo } from 'External/ko';
|
||||||
|
|
||||||
|
|
@ -19,17 +18,12 @@ import { AbstractModel } from 'Knoin/AbstractModel';
|
||||||
|
|
||||||
import PreviewHTML from 'Html/PreviewMessage.html';
|
import PreviewHTML from 'Html/PreviewMessage.html';
|
||||||
|
|
||||||
//import { MessageFlagsCache } from 'Common/Cache';
|
import { LanguageStore } from 'Stores/Language';
|
||||||
|
|
||||||
import Remote from 'Remote/User/Fetch';
|
import Remote from 'Remote/User/Fetch';
|
||||||
|
|
||||||
const
|
const
|
||||||
hcont = Element.fromHTML('<div area="hidden" style="position:absolute;left:-5000px"></div>'),
|
msgHtml = msg => cleanHtml(msg.html(), msg.attachments(), '#rl-msg-' + msg.hash),
|
||||||
getRealHeight = el => {
|
|
||||||
hcont.innerHTML = el.outerHTML;
|
|
||||||
const result = hcont.clientHeight;
|
|
||||||
hcont.innerHTML = '';
|
|
||||||
return result;
|
|
||||||
},
|
|
||||||
|
|
||||||
toggleTag = (message, keyword) => {
|
toggleTag = (message, keyword) => {
|
||||||
const lower = keyword.toLowerCase(),
|
const lower = keyword.toLowerCase(),
|
||||||
|
|
@ -38,13 +32,12 @@ const
|
||||||
Remote.request('MessageSetKeyword', iError => {
|
Remote.request('MessageSetKeyword', iError => {
|
||||||
if (!iError) {
|
if (!iError) {
|
||||||
isSet ? flags.remove(lower) : flags.push(lower);
|
isSet ? flags.remove(lower) : flags.push(lower);
|
||||||
// MessageFlagsCache.setFor(message.folder, message.uid, flags());
|
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
Folder: message.folder,
|
folder: message.folder,
|
||||||
Uids: message.uid,
|
uids: message.uid,
|
||||||
Keyword: keyword,
|
keyword: keyword,
|
||||||
SetAction: isSet ? 0 : 1
|
setAction: isSet ? 0 : 1
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -58,13 +51,29 @@ const
|
||||||
unic[email.email] || localEmails.has(email.email) || localEmails.set(email.email, email)
|
unic[email.email] || localEmails.has(email.email) || localEmails.set(email.email, email)
|
||||||
);
|
);
|
||||||
|
|
||||||
doc.body.append(hcont);
|
|
||||||
|
|
||||||
export class MessageModel extends AbstractModel {
|
export class MessageModel extends AbstractModel {
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
this._reset();
|
this.folder = '';
|
||||||
|
this.uid = 0;
|
||||||
|
this.hash = '';
|
||||||
|
this.from = new EmailCollectionModel;
|
||||||
|
this.to = new EmailCollectionModel;
|
||||||
|
this.cc = new EmailCollectionModel;
|
||||||
|
this.bcc = new EmailCollectionModel;
|
||||||
|
this.sender = new EmailCollectionModel;
|
||||||
|
this.replyTo = new EmailCollectionModel;
|
||||||
|
this.deliveredTo = new EmailCollectionModel;
|
||||||
|
this.body = null;
|
||||||
|
this.draftInfo = [];
|
||||||
|
this.dkim = [];
|
||||||
|
this.spf = [];
|
||||||
|
this.dmarc = [];
|
||||||
|
this.messageId = '';
|
||||||
|
this.inReplyTo = '';
|
||||||
|
this.references = '';
|
||||||
|
this.autocrypt = {};
|
||||||
|
|
||||||
addObservablesTo(this, {
|
addObservablesTo(this, {
|
||||||
subject: '',
|
subject: '',
|
||||||
|
|
@ -75,8 +84,9 @@ export class MessageModel extends AbstractModel {
|
||||||
spamResult: '',
|
spamResult: '',
|
||||||
isSpam: false,
|
isSpam: false,
|
||||||
hasVirus: null, // or boolean when scanned
|
hasVirus: null, // or boolean when scanned
|
||||||
dateTimeStampInUTC: 0,
|
dateTimestamp: 0,
|
||||||
priority: MessagePriority.Normal,
|
internalTimestamp: 0,
|
||||||
|
priority: 3, // Normal
|
||||||
|
|
||||||
senderEmailsString: '',
|
senderEmailsString: '',
|
||||||
senderClearEmailsString: '',
|
senderClearEmailsString: '',
|
||||||
|
|
@ -101,35 +111,28 @@ export class MessageModel extends AbstractModel {
|
||||||
|
|
||||||
readReceipt: '',
|
readReceipt: '',
|
||||||
|
|
||||||
hasUnseenSubMessage: false,
|
// rfc8621
|
||||||
hasFlaggedSubMessage: false
|
id: '',
|
||||||
|
// threadId: ''
|
||||||
});
|
});
|
||||||
|
|
||||||
this.attachments = ko.observableArray(new AttachmentCollectionModel);
|
this.attachments = ko.observableArray(new AttachmentCollectionModel);
|
||||||
this.threads = ko.observableArray();
|
this.threads = ko.observableArray();
|
||||||
|
this.threadUnseen = ko.observableArray();
|
||||||
this.unsubsribeLinks = ko.observableArray();
|
this.unsubsribeLinks = ko.observableArray();
|
||||||
this.flags = ko.observableArray();
|
this.flags = ko.observableArray();
|
||||||
|
|
||||||
addComputablesTo(this, {
|
addComputablesTo(this, {
|
||||||
attachmentIconClass: () =>
|
attachmentIconClass: () =>
|
||||||
this.encrypted() ? 'icon-lock' : FileInfo.getAttachmentsIconClass(this.attachments()),
|
this.encrypted() ? 'icon-lock' : FileInfo.getAttachmentsIconClass(this.attachments()),
|
||||||
threadsLen: () => this.threads().length,
|
threadsLen: () => rl.app.messageList.threadUid() ? 0 : this.threads().length,
|
||||||
listAttachments: () => this.attachments()
|
threadUnseenLen: () => rl.app.messageList.threadUid() ? 0 : this.threadUnseen().length,
|
||||||
.filter(item => SettingsUserStore.listInlineAttachments() || !item.isLinked()),
|
|
||||||
hasAttachments: () => this.listAttachments().length,
|
|
||||||
|
|
||||||
isUnseen: () => !this.flags().includes('\\seen'),
|
isUnseen: () => !this.flags().includes('\\seen'),
|
||||||
isFlagged: () => this.flags().includes('\\flagged'),
|
isFlagged: () => this.flags().includes('\\flagged'),
|
||||||
isReadReceipt: () => this.flags().includes('$mdnsent'),
|
|
||||||
// isJunk: () => this.flags().includes('$junk') && !this.flags().includes('$nonjunk'),
|
// isJunk: () => this.flags().includes('$junk') && !this.flags().includes('$nonjunk'),
|
||||||
// isPhishing: () => this.flags().includes('$phishing'),
|
// isPhishing: () => this.flags().includes('$phishing'),
|
||||||
|
|
||||||
tagsToHTML: () => this.flags().map(value =>
|
|
||||||
isAllowedKeyword(value)
|
|
||||||
? '<span class="focused msgflag-'+value+'">' + i18n('MESSAGE_TAGS/'+value,0,value) + '</span>'
|
|
||||||
: ''
|
|
||||||
).join(' '),
|
|
||||||
|
|
||||||
tagOptions: () => {
|
tagOptions: () => {
|
||||||
const tagOptions = [];
|
const tagOptions = [];
|
||||||
FolderUserStore.currentFolder().permanentFlags.forEach(value => {
|
FolderUserStore.currentFolder().permanentFlags.forEach(value => {
|
||||||
|
|
@ -145,8 +148,41 @@ export class MessageModel extends AbstractModel {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return tagOptions
|
return tagOptions
|
||||||
|
},
|
||||||
|
|
||||||
|
whitelistOptions: () => {
|
||||||
|
let options = [];
|
||||||
|
if ('match' === SettingsUserStore.viewImages()) {
|
||||||
|
let from = this.from[0],
|
||||||
|
list = SettingsUserStore.viewImagesWhitelist(),
|
||||||
|
counts = {};
|
||||||
|
this.html().match(/src=["'][^"']+/g)?.forEach(m => {
|
||||||
|
m = m.replace(/^.+(:\/\/[^/]+).+$/, '$1');
|
||||||
|
if (counts[m]) {
|
||||||
|
++counts[m];
|
||||||
|
} else {
|
||||||
|
counts[m] = 1;
|
||||||
|
options.push(m);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
options = options.filter(txt => !list.includes(txt)).sort((a,b) => (counts[a] < counts[b])
|
||||||
|
? 1
|
||||||
|
: (counts[a] > counts[b] ? -1 : a.localeCompare(b))
|
||||||
|
);
|
||||||
|
from && options.unshift(from.email);
|
||||||
|
}
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get requestHash() {
|
||||||
|
return b64EncodeJSONSafe({
|
||||||
|
folder: this.folder,
|
||||||
|
uid: this.uid,
|
||||||
|
mimeType: 'message/rfc822',
|
||||||
|
fileName: (this.subject() || 'message-' + this.hash) + '.eml',
|
||||||
|
accountHash: SettingsGet('accountHash')
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -154,67 +190,6 @@ export class MessageModel extends AbstractModel {
|
||||||
toggleTag(this, keyword);
|
toggleTag(this, keyword);
|
||||||
}
|
}
|
||||||
|
|
||||||
_reset() {
|
|
||||||
this.folder = '';
|
|
||||||
this.uid = 0;
|
|
||||||
this.hash = '';
|
|
||||||
this.requestHash = '';
|
|
||||||
this.emails = [];
|
|
||||||
this.from = new EmailCollectionModel;
|
|
||||||
this.to = new EmailCollectionModel;
|
|
||||||
this.cc = new EmailCollectionModel;
|
|
||||||
this.bcc = new EmailCollectionModel;
|
|
||||||
this.replyTo = new EmailCollectionModel;
|
|
||||||
this.deliveredTo = new EmailCollectionModel;
|
|
||||||
this.body = null;
|
|
||||||
this.draftInfo = [];
|
|
||||||
this.messageId = '';
|
|
||||||
this.inReplyTo = '';
|
|
||||||
this.references = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
clear() {
|
|
||||||
this._reset();
|
|
||||||
this.subject('');
|
|
||||||
this.html('');
|
|
||||||
this.plain('');
|
|
||||||
this.size(0);
|
|
||||||
this.spamScore(0);
|
|
||||||
this.spamResult('');
|
|
||||||
this.isSpam(false);
|
|
||||||
this.hasVirus(null);
|
|
||||||
this.dateTimeStampInUTC(0);
|
|
||||||
this.priority(MessagePriority.Normal);
|
|
||||||
|
|
||||||
this.senderEmailsString('');
|
|
||||||
this.senderClearEmailsString('');
|
|
||||||
|
|
||||||
this.deleted(false);
|
|
||||||
|
|
||||||
this.selected(false);
|
|
||||||
this.checked(false);
|
|
||||||
|
|
||||||
this.isHtml(false);
|
|
||||||
this.hasImages(false);
|
|
||||||
this.hasExternals(false);
|
|
||||||
this.attachments(new AttachmentCollectionModel);
|
|
||||||
|
|
||||||
this.pgpSigned(null);
|
|
||||||
this.pgpVerified(null);
|
|
||||||
|
|
||||||
this.pgpEncrypted(null);
|
|
||||||
this.pgpDecrypted(false);
|
|
||||||
|
|
||||||
this.priority(MessagePriority.Normal);
|
|
||||||
this.readReceipt('');
|
|
||||||
|
|
||||||
this.threads([]);
|
|
||||||
this.unsubsribeLinks([]);
|
|
||||||
|
|
||||||
this.hasUnseenSubMessage(false);
|
|
||||||
this.hasFlaggedSubMessage(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
spamStatus() {
|
spamStatus() {
|
||||||
let spam = this.spamResult();
|
let spam = this.spamResult();
|
||||||
return spam ? i18n(this.isSpam() ? 'GLOBAL/SPAM' : 'GLOBAL/NOT_SPAM') + ': ' + spam : '';
|
return spam ? i18n(this.isSpam() ? 'GLOBAL/SPAM' : 'GLOBAL/NOT_SPAM') + ': ' + spam : '';
|
||||||
|
|
@ -240,84 +215,15 @@ export class MessageModel extends AbstractModel {
|
||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
*/
|
*/
|
||||||
revivePropertiesFromJson(json) {
|
revivePropertiesFromJson(json) {
|
||||||
if ('Priority' in json && ![MessagePriority.High, MessagePriority.Low].includes(json.Priority)) {
|
|
||||||
json.Priority = MessagePriority.Normal;
|
|
||||||
}
|
|
||||||
if (super.revivePropertiesFromJson(json)) {
|
if (super.revivePropertiesFromJson(json)) {
|
||||||
// this.foundCIDs = isArray(json.FoundCIDs) ? json.FoundCIDs : [];
|
// this.foundCIDs = isArray(json.FoundCIDs) ? json.FoundCIDs : [];
|
||||||
// this.attachments(AttachmentCollectionModel.reviveFromJson(json.Attachments, this.foundCIDs));
|
// this.attachments(AttachmentCollectionModel.reviveFromJson(json.attachments, this.foundCIDs));
|
||||||
|
|
||||||
this.computeSenderEmail();
|
this.computeSenderEmail();
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @returns {boolean}
|
|
||||||
*/
|
|
||||||
hasUnsubsribeLinks() {
|
|
||||||
return this.unsubsribeLinks().length;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
getFirstUnsubsribeLink() {
|
|
||||||
return this.unsubsribeLinks()[0] || '';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {boolean} friendlyView
|
|
||||||
* @param {boolean=} wrapWithLink
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
fromToLine(friendlyView, wrapWithLink) {
|
|
||||||
return this.from.toString(friendlyView, wrapWithLink);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
fromDkimData() {
|
|
||||||
let result = ['none', ''];
|
|
||||||
if (1 === arrayLength(this.from) && this.from[0]?.dkimStatus) {
|
|
||||||
result = [this.from[0].dkimStatus, this.from[0].dkimValue || ''];
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {boolean} friendlyView
|
|
||||||
* @param {boolean=} wrapWithLink
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
toToLine(friendlyView, wrapWithLink) {
|
|
||||||
return this.to.toString(friendlyView, wrapWithLink);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {boolean} friendlyView
|
|
||||||
* @param {boolean=} wrapWithLink
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
ccToLine(friendlyView, wrapWithLink) {
|
|
||||||
return this.cc.toString(friendlyView, wrapWithLink);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
bccToLine() {
|
|
||||||
return this.bcc.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
replyToToLine() {
|
|
||||||
return this.replyTo.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return string
|
* @return string
|
||||||
*/
|
*/
|
||||||
|
|
@ -329,20 +235,22 @@ export class MessageModel extends AbstractModel {
|
||||||
checked: this.checked(),
|
checked: this.checked(),
|
||||||
unseen: this.isUnseen(),
|
unseen: this.isUnseen(),
|
||||||
focused: this.focused(),
|
focused: this.focused(),
|
||||||
priorityHigh: this.priority() === MessagePriority.High,
|
priorityHigh: this.priority() === 1,
|
||||||
withAttachments: !!this.attachments().length,
|
withAttachments: !!this.attachments().length,
|
||||||
// hasChildrenMessage: 1 < this.threadsLen(),
|
// hasChildrenMessage: 1 < this.threadsLen()
|
||||||
hasUnseenSubMessage: this.hasUnseenSubMessage(),
|
|
||||||
hasFlaggedSubMessage: this.hasFlaggedSubMessage()
|
|
||||||
}, (key, value) => value && classes.push(key));
|
}, (key, value) => value && classes.push(key));
|
||||||
flags && this.flags().forEach(value => classes.push('msgflag-'+value));
|
flags && this.flags().forEach(value => classes.push('msgflag-'+value));
|
||||||
return classes.join(' ');
|
return classes.join(' ');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
indent() {
|
||||||
|
return this.level ? 'margin-left:'+this.level+'em' : null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
viewLink() {
|
viewRaw() {
|
||||||
return serverRequestRaw('ViewAsPlain', this.requestHash);
|
return serverRequestRaw('ViewAsPlain', this.requestHash);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -386,83 +294,77 @@ export class MessageModel extends AbstractModel {
|
||||||
return [[...toResult.values()], [...ccResult.values()]];
|
return [[...toResult.values()], [...ccResult.values()]];
|
||||||
}
|
}
|
||||||
|
|
||||||
viewHtml() {
|
viewBody(html) {
|
||||||
const body = this.body;
|
|
||||||
if (body && this.html()) {
|
|
||||||
let result = cleanHtml(this.html(), this.attachments());
|
|
||||||
this.hasExternals(result.hasExternals);
|
|
||||||
this.hasImages(body.rlHasImages = !!result.hasExternals);
|
|
||||||
|
|
||||||
body.innerHTML = result.html;
|
|
||||||
|
|
||||||
body.classList.toggle('html', 1);
|
|
||||||
body.classList.toggle('plain', 0);
|
|
||||||
|
|
||||||
if (SettingsUserStore.showImages()) {
|
|
||||||
this.showExternalImages();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.isHtml(true);
|
|
||||||
this.initView();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
viewPlain() {
|
|
||||||
const body = this.body;
|
const body = this.body;
|
||||||
if (body) {
|
if (body) {
|
||||||
body.classList.toggle('html', 0);
|
if (html) {
|
||||||
body.classList.toggle('plain', 1);
|
let result = msgHtml(this);
|
||||||
|
this.hasExternals(result.hasExternals);
|
||||||
|
this.hasImages(!!result.hasExternals);
|
||||||
|
body.innerHTML = result.html;
|
||||||
|
if (!this.isSpam() && FolderUserStore.spamFolder() != this.folder) {
|
||||||
|
if ('always' === SettingsUserStore.viewImages()) {
|
||||||
|
this.showExternalImages();
|
||||||
|
}
|
||||||
|
if ('match' === SettingsUserStore.viewImages()) {
|
||||||
|
this.showExternalImages(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
body.innerHTML = plainToHtml(
|
body.innerHTML = plainToHtml(
|
||||||
(this.plain()
|
(this.plain()
|
||||||
? this.plain()
|
? this.plain()
|
||||||
.replace(/-----BEGIN PGP (SIGNED MESSAGE-----(\r?\n[a-z][^\r\n]+)+|SIGNATURE-----[\s\S]*)/, '')
|
.replace(/-----BEGIN PGP (SIGNED MESSAGE-----(\r?\n[^\r\n]+)+|SIGNATURE-----[\s\S]*)/sg, '')
|
||||||
.trim()
|
.trim()
|
||||||
: htmlToPlain(body.innerHTML)
|
: htmlToPlain(body.innerHTML)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
this.isHtml(false);
|
|
||||||
this.hasImages(false);
|
this.hasImages(false);
|
||||||
this.initView();
|
}
|
||||||
|
body.classList.toggle('html', html);
|
||||||
|
body.classList.toggle('plain', !html);
|
||||||
|
this.isHtml(html);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
initView() {
|
viewHtml() {
|
||||||
// init BlockquoteSwitcher
|
return this.html() && this.viewBody(true);
|
||||||
this.body.querySelectorAll('blockquote').forEach(node => {
|
|
||||||
if (node.textContent.trim()) {
|
|
||||||
let h = node.clientHeight || getRealHeight(node);
|
|
||||||
if (0 === h || 100 < h) {
|
|
||||||
const el = Element.fromHTML('<details class="sm-bq-switcher"><summary>•••</summary></details>');
|
|
||||||
node.replaceWith(el);
|
|
||||||
el.append(node);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
});
|
viewPlain() {
|
||||||
|
return this.viewBody(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
viewPopupMessage(print) {
|
viewPopupMessage(print) {
|
||||||
const timeStampInUTC = this.dateTimeStampInUTC() || 0,
|
const
|
||||||
ccLine = this.ccToLine(),
|
timeStampInUTC = this.dateTimestamp() || 0,
|
||||||
|
ccLine = this.cc.toString(),
|
||||||
|
bccLine = this.bcc.toString(),
|
||||||
m = 0 < timeStampInUTC ? new Date(timeStampInUTC * 1000) : null,
|
m = 0 < timeStampInUTC ? new Date(timeStampInUTC * 1000) : null,
|
||||||
win = open(''),
|
win = open('', 'sm-msg-'+this.requestHash
|
||||||
sdoc = win.document;
|
/*,newWindow ? 'innerWidth=' + elementById('V-MailMessageView').clientWidth : ''*/
|
||||||
let subject = encodeHtml(this.subject()),
|
),
|
||||||
|
sdoc = win.document,
|
||||||
|
subject = encodeHtml(this.subject()),
|
||||||
mode = this.isHtml() ? 'div' : 'pre',
|
mode = this.isHtml() ? 'div' : 'pre',
|
||||||
cc = ccLine ? `<div>${encodeHtml(i18n('GLOBAL/CC'))}: ${encodeHtml(ccLine)}</div>` : '',
|
to = `<div>${encodeHtml(i18n('GLOBAL/TO'))}: ${encodeHtml(this.to)}</div>`
|
||||||
|
+ (ccLine ? `<div>${encodeHtml(i18n('GLOBAL/CC'))}: ${encodeHtml(ccLine)}</div>` : '')
|
||||||
|
+ (bccLine ? `<div>${encodeHtml(i18n('GLOBAL/BCC'))}: ${encodeHtml(bccLine)}</div>` : ''),
|
||||||
style = getComputedStyle(doc.querySelector('.messageView')),
|
style = getComputedStyle(doc.querySelector('.messageView')),
|
||||||
prop = property => style.getPropertyValue(property);
|
prop = property => style.getPropertyValue(property);
|
||||||
|
let attachments = '';
|
||||||
|
this.attachments.forEach(attachment => {
|
||||||
|
attachments += `<a href="${attachment.linkDownload()}">${attachment.fileName}</a>`;
|
||||||
|
});
|
||||||
sdoc.write(PreviewHTML
|
sdoc.write(PreviewHTML
|
||||||
.replace('<title>', '<title>'+subject)
|
.replace('<title>', '<title>'+subject)
|
||||||
// eslint-disable-next-line max-len
|
// eslint-disable-next-line max-len
|
||||||
.replace('<body>', `<body style="background-color:${prop('background-color')};color:${prop('color')}"><header><h1>${subject}</h1><time>${encodeHtml(m ? m.format('LLL') : '')}</time><div>${encodeHtml(this.fromToLine())}</div><div>${encodeHtml(i18n('GLOBAL/TO'))}: ${encodeHtml(this.toToLine())}</div>${cc}</header><${mode}>${this.bodyAsHTML()}</${mode}>`)
|
.replace('<body>', `<body style="background-color:${prop('background-color')};color:${prop('color')}"><header><h1>${subject}</h1><time>${encodeHtml(m ? m.format('LLL',0,LanguageStore.hourCycle()) : '')}</time><div>${encodeHtml(this.from)}</div>${to}</header><${mode}>${this.bodyAsHTML()}</${mode}>`)
|
||||||
|
.replace('</body>', `<div id="attachments">${attachments}</div></body>`)
|
||||||
);
|
);
|
||||||
sdoc.close();
|
sdoc.close();
|
||||||
|
print && setTimeout(() => win.print(), 100);
|
||||||
if (print) {
|
|
||||||
setTimeout(() => win.print(), 100);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -484,75 +386,72 @@ export class MessageModel extends AbstractModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {MessageModel} message
|
|
||||||
* @returns {MessageModel}
|
* @returns {MessageModel}
|
||||||
*/
|
*//*
|
||||||
static fromMessageListItem(message) {
|
clone() {
|
||||||
let self = new MessageModel();
|
let self = new MessageModel();
|
||||||
|
// Clone message values
|
||||||
if (message) {
|
forEachObjectEntry(this, (key, value) => {
|
||||||
self.folder = message.folder;
|
if (ko.isObservable(value)) {
|
||||||
self.uid = message.uid;
|
ko.isComputed(value) || self[key](value());
|
||||||
self.hash = message.hash;
|
} else if (!isFunction(value)) {
|
||||||
self.requestHash = message.requestHash;
|
self[key] = value;
|
||||||
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());
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
self.computeSenderEmail();
|
self.computeSenderEmail();
|
||||||
|
|
||||||
return self;
|
return self;
|
||||||
}
|
}*/
|
||||||
|
|
||||||
showExternalImages() {
|
showExternalImages(regex) {
|
||||||
const body = this.body;
|
const body = this.body;
|
||||||
if (body && this.hasImages()) {
|
if (body && this.hasImages()) {
|
||||||
this.hasImages(false);
|
if (regex) {
|
||||||
body.rlHasImages = false;
|
regex = [];
|
||||||
|
SettingsUserStore.viewImagesWhitelist().trim().split(/[\s\r\n,;]+/g).forEach(rule => {
|
||||||
let attr = 'data-x-src',
|
rule = rule.split('+');
|
||||||
src, useProxy = !!SettingsGet('UseLocalProxyForExternalImages');
|
rule[0] = rule[0].trim();
|
||||||
|
if (rule[0]
|
||||||
|
&& (!rule.includes('spf') || 'pass' === this.spf[0]?.[0])
|
||||||
|
&& (!rule.includes('dkim') || 'pass' === this.dkim[0]?.[0])
|
||||||
|
&& (!rule.includes('dmarc') || 'pass' === this.dmarc[0]?.[0])
|
||||||
|
) {
|
||||||
|
regex.push(rule[0].replace(/[/\-\\^$*+?.()|[\]{}]/g, '\\$&'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
regex = regex.join('|').replace(/\|+/g, '|');
|
||||||
|
if (regex) {
|
||||||
|
console.log('whitelist images = '+regex);
|
||||||
|
regex = new RegExp(regex);
|
||||||
|
if (this.from[0]?.email.match(regex)) {
|
||||||
|
regex = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let hasImages = false,
|
||||||
|
isValid = src => {
|
||||||
|
if (null == regex || (regex && src.match(regex))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
hasImages = true;
|
||||||
|
},
|
||||||
|
attr = 'data-x-src',
|
||||||
|
src, useProxy = !!SettingsGet('useLocalProxyForExternalImages');
|
||||||
body.querySelectorAll('img[' + attr + ']').forEach(node => {
|
body.querySelectorAll('img[' + attr + ']').forEach(node => {
|
||||||
src = node.getAttribute(attr);
|
src = node.getAttribute(attr);
|
||||||
|
if (isValid(src)) {
|
||||||
node.src = useProxy ? proxy(src) : src;
|
node.src = useProxy ? proxy(src) : src;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
body.querySelectorAll('[data-x-style-url]').forEach(node => {
|
body.querySelectorAll('[data-x-style-url]').forEach(node => {
|
||||||
JSON.parse(node.dataset.xStyleUrl).forEach(data =>
|
JSON.parse(node.dataset.xStyleUrl).forEach(data => {
|
||||||
|
if (isValid(data[1])) {
|
||||||
node.style[data[0]] = "url('" + (useProxy ? proxy(data[1]) : data[1]) + "')"
|
node.style[data[0]] = "url('" + (useProxy ? proxy(data[1]) : data[1]) + "')"
|
||||||
);
|
}
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
this.hasImages(hasImages);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -567,7 +466,7 @@ export class MessageModel extends AbstractModel {
|
||||||
);
|
);
|
||||||
return clone.innerHTML;
|
return clone.innerHTML;
|
||||||
}
|
}
|
||||||
let result = cleanHtml(this.html(), this.attachments())
|
let result = msgHtml(this);
|
||||||
return result.html || plainToHtml(this.plain());
|
return result.html || plainToHtml(this.plain());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,6 @@
|
||||||
import { AbstractCollectionModel } from 'Model/AbstractCollection';
|
import { AbstractCollectionModel } from 'Model/AbstractCollection';
|
||||||
import { MessageModel } from 'Model/Message';
|
import { MessageModel } from 'Model/Message';
|
||||||
|
import { MessageUserStore } from 'Stores/User/Message';
|
||||||
import {
|
|
||||||
MessageFlagsCache
|
|
||||||
} from 'Common/Cache';
|
|
||||||
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
|
@ -12,19 +9,16 @@ export class MessageCollectionModel extends AbstractCollectionModel
|
||||||
/*
|
/*
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.Filtered
|
this.filtered
|
||||||
this.Folder
|
this.folder
|
||||||
this.FolderHash
|
|
||||||
this.FolderInfo
|
|
||||||
this.totalEmails
|
this.totalEmails
|
||||||
this.unreadEmails
|
this.totalThreads
|
||||||
this.MessageResultCount
|
this.threadUid
|
||||||
this.UidNext
|
this.newMessages
|
||||||
this.ThreadUid
|
this.offset
|
||||||
this.NewMessages
|
this.limit
|
||||||
this.Offset
|
this.search
|
||||||
this.Limit
|
this.limited
|
||||||
this.Search
|
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
@ -32,12 +26,14 @@ export class MessageCollectionModel extends AbstractCollectionModel
|
||||||
* @param {?Object} json
|
* @param {?Object} json
|
||||||
* @returns {MessageCollectionModel}
|
* @returns {MessageCollectionModel}
|
||||||
*/
|
*/
|
||||||
static reviveFromJson(object, cached) {
|
static reviveFromJson(object/*, cached*/) {
|
||||||
|
let msg = MessageUserStore.message();
|
||||||
return super.reviveFromJson(object, message => {
|
return super.reviveFromJson(object, message => {
|
||||||
message = MessageModel.reviveFromJson(message);
|
// If message is currently viewed, use that.
|
||||||
|
// Maybe then use msg.revivePropertiesFromJson(message) ?
|
||||||
|
message = (msg && msg.hash === message.hash) ? msg : MessageModel.reviveFromJson(message);
|
||||||
if (message) {
|
if (message) {
|
||||||
message.deleted(false);
|
message.deleted(false);
|
||||||
cached ? MessageFlagsCache.initMessage(message) : MessageFlagsCache.store(message);
|
|
||||||
return message;
|
return message;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { Notification } from 'Common/Enums';
|
import { Notifications } from 'Common/Enums';
|
||||||
import { isArray, pInt, pString } from 'Common/Utils';
|
import { isArray, pInt, pString } from 'Common/Utils';
|
||||||
import { serverRequest } from 'Common/Links';
|
import { serverRequest } from 'Common/Links';
|
||||||
import { getNotification } from 'Common/Translator';
|
import { getNotification } from 'Common/Translator';
|
||||||
|
|
@ -9,17 +9,18 @@ const getURL = (add = '') => serverRequest('Json') + pString(add),
|
||||||
|
|
||||||
checkResponseError = data => {
|
checkResponseError = data => {
|
||||||
const err = data ? data.ErrorCode : null;
|
const err = data ? data.ErrorCode : null;
|
||||||
if (Notification.InvalidToken === err) {
|
if (Notifications.InvalidToken === err) {
|
||||||
alert(getNotification(err));
|
console.error(getNotification(err));
|
||||||
|
// alert(getNotification(err));
|
||||||
rl.logoutReload();
|
rl.logoutReload();
|
||||||
} else if ([
|
} else if ([
|
||||||
Notification.AuthError,
|
Notifications.AuthError,
|
||||||
Notification.ConnectionError,
|
Notifications.ConnectionError,
|
||||||
Notification.DomainNotAllowed,
|
Notifications.DomainNotAllowed,
|
||||||
Notification.AccountNotAllowed,
|
Notifications.AccountNotAllowed,
|
||||||
Notification.MailServerError,
|
Notifications.MailServerError,
|
||||||
Notification.UnknownNotification,
|
Notifications.UnknownNotification,
|
||||||
Notification.UnknownError
|
Notifications.UnknownError
|
||||||
].includes(err)
|
].includes(err)
|
||||||
) {
|
) {
|
||||||
if (7 < ++iJsonErrorCount) {
|
if (7 < ++iJsonErrorCount) {
|
||||||
|
|
@ -31,32 +32,34 @@ checkResponseError = data => {
|
||||||
oRequests = {},
|
oRequests = {},
|
||||||
|
|
||||||
abort = (sAction, sReason, bClearOnly) => {
|
abort = (sAction, sReason, bClearOnly) => {
|
||||||
if (oRequests[sAction]) {
|
let controller = oRequests[sAction];
|
||||||
if (!bClearOnly && oRequests[sAction].abort) {
|
|
||||||
// oRequests[sAction].__aborted = true;
|
|
||||||
oRequests[sAction].abort(sReason || 'AbortError');
|
|
||||||
}
|
|
||||||
|
|
||||||
oRequests[sAction] = null;
|
oRequests[sAction] = null;
|
||||||
delete oRequests[sAction];
|
if (controller) {
|
||||||
|
clearTimeout(controller.timeoutId);
|
||||||
|
bClearOnly || controller.abort(sReason || 'AbortError');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
fetchJSON = (action, sGetAdd, params, timeout, jsonCallback) => {
|
fetchJSON = (action, sUrl, params, timeout, jsonCallback) => {
|
||||||
params = params || {};
|
if (params) {
|
||||||
if (params instanceof FormData) {
|
if (params instanceof FormData) {
|
||||||
params.set('Action', action);
|
params.set('Action', action);
|
||||||
} else {
|
} else {
|
||||||
params.Action = action;
|
params.Action = action;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
// Don't abort, read https://github.com/the-djmaze/snappymail/issues/487
|
// Don't abort, read https://github.com/the-djmaze/snappymail/issues/487
|
||||||
// abort(action);
|
// abort(action, 0, 1);
|
||||||
const controller = new AbortController(),
|
const controller = new AbortController(),
|
||||||
signal = controller.signal;
|
signal = controller.signal;
|
||||||
oRequests[action] = controller;
|
oRequests[action] = controller;
|
||||||
// Currently there is no way to combine multiple signals, so AbortSignal.timeout() not possible
|
// Currently there is no way to combine multiple signals, so AbortSignal.timeout() not possible
|
||||||
timeout && setTimeout(() => abort(action, 'TimeoutError'), timeout);
|
controller.timeoutId = timeout && setTimeout(() => abort(action, 'TimeoutError'), timeout);
|
||||||
return rl.fetchJSON(getURL(sGetAdd), {signal: signal}, sGetAdd ? null : params).then(jsonCallback).catch(err => {
|
return rl.fetchJSON(sUrl, {signal: signal}, params).then(data => {
|
||||||
|
abort(action, 0, 1);
|
||||||
|
return jsonCallback ? jsonCallback(data) : Promise.resolve(data);
|
||||||
|
}).catch(err => {
|
||||||
|
clearTimeout(controller.timeoutId);
|
||||||
err.aborted = signal.aborted;
|
err.aborted = signal.aborted;
|
||||||
err.reason = signal.reason;
|
err.reason = signal.reason;
|
||||||
return Promise.reject(err);
|
return Promise.reject(err);
|
||||||
|
|
@ -67,14 +70,14 @@ class FetchError extends Error
|
||||||
{
|
{
|
||||||
constructor(code, message) {
|
constructor(code, message) {
|
||||||
super(message);
|
super(message);
|
||||||
this.code = code || Notification.JsonFalse;
|
this.code = code || Notifications.JsonFalse;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class AbstractFetchRemote
|
export class AbstractFetchRemote
|
||||||
{
|
{
|
||||||
abort(sAction) {
|
abort(sAction, sReason) {
|
||||||
abort(sAction);
|
abort(sAction, sReason);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -123,31 +126,17 @@ export class AbstractFetchRemote
|
||||||
* @param {?number=} iTimeout
|
* @param {?number=} iTimeout
|
||||||
* @param {string=} sGetAdd = ''
|
* @param {string=} sGetAdd = ''
|
||||||
*/
|
*/
|
||||||
request(sAction, fCallback, params, iTimeout, sGetAdd, abortActions) {
|
request(sAction, fCallback, params, iTimeout, sGetAdd) {
|
||||||
params = params || {};
|
params = params || {};
|
||||||
|
|
||||||
const start = Date.now();
|
const start = Date.now();
|
||||||
|
|
||||||
abortActions && console.error('abortActions is obsolete');
|
fetchJSON(sAction, getURL(sGetAdd),
|
||||||
|
sGetAdd ? null : (params || {}),
|
||||||
fetchJSON(sAction, sGetAdd,
|
|
||||||
params,
|
|
||||||
undefined === iTimeout ? 30000 : pInt(iTimeout),
|
undefined === iTimeout ? 30000 : pInt(iTimeout),
|
||||||
data => {
|
data => {
|
||||||
let cached = false;
|
|
||||||
if (data?.Time) {
|
|
||||||
cached = pInt(data.Time) > Date.now() - start;
|
|
||||||
}
|
|
||||||
|
|
||||||
let iError = 0;
|
let iError = 0;
|
||||||
if (sAction && oRequests[sAction]) {
|
if (data) {
|
||||||
if (oRequests[sAction].__aborted) {
|
|
||||||
iError = 2;
|
|
||||||
}
|
|
||||||
abort(sAction, 0, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!iError && data) {
|
|
||||||
/*
|
/*
|
||||||
if (sAction !== data.Action) {
|
if (sAction !== data.Action) {
|
||||||
console.log(sAction + ' !== ' + data.Action);
|
console.log(sAction + ' !== ' + data.Action);
|
||||||
|
|
@ -157,16 +146,19 @@ export class AbstractFetchRemote
|
||||||
iJsonErrorCount = 0;
|
iJsonErrorCount = 0;
|
||||||
} else {
|
} else {
|
||||||
checkResponseError(data);
|
checkResponseError(data);
|
||||||
iError = data.ErrorCode || Notification.UnknownError
|
iError = data.ErrorCode || Notifications.UnknownError
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fCallback && fCallback(
|
fCallback && fCallback(
|
||||||
iError,
|
iError,
|
||||||
data,
|
data,
|
||||||
cached,
|
/**
|
||||||
sAction,
|
* Responses like "304 Not Modified" are returned as "200 OK"
|
||||||
params
|
* This is an attempt to detect if the request comes from cache.
|
||||||
|
* But when client has wrong date/time, it will fail.
|
||||||
|
*/
|
||||||
|
data?.epoch && data.epoch < Math.floor(start / 1000) - 60
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
@ -195,19 +187,23 @@ export class AbstractFetchRemote
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get(action, url) {
|
||||||
|
return fetchJSON(action, url);
|
||||||
|
}
|
||||||
|
|
||||||
post(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, getURL(), params || {}, pInt(timeOut, 30000),
|
||||||
data => {
|
data => {
|
||||||
abort(action, 0, 1);
|
abort(action, 0, 1);
|
||||||
|
|
||||||
if (!data) {
|
if (!data) {
|
||||||
return Promise.reject(new FetchError(Notification.JsonParse));
|
return Promise.reject(new FetchError(Notifications.JsonParse));
|
||||||
}
|
}
|
||||||
/*
|
/*
|
||||||
let isCached = false, type = '';
|
let isCached = false, type = '';
|
||||||
if (data?.Time) {
|
if (data?.epoch) {
|
||||||
isCached = pInt(data.Time) > microtime() - start;
|
isCached = data.epoch > microtime() - start;
|
||||||
}
|
}
|
||||||
// backward capability
|
// backward capability
|
||||||
switch (true) {
|
switch (true) {
|
||||||
|
|
|
||||||
|
|
@ -9,57 +9,11 @@ import { SUB_QUERY_PREFIX } from 'Common/Links';
|
||||||
|
|
||||||
import { AppUserStore } from 'Stores/User/App';
|
import { AppUserStore } from 'Stores/User/App';
|
||||||
import { SettingsUserStore } from 'Stores/User/Settings';
|
import { SettingsUserStore } from 'Stores/User/Settings';
|
||||||
import { FolderUserStore } from 'Stores/User/Folder';
|
|
||||||
|
|
||||||
import { AbstractFetchRemote } from 'Remote/AbstractFetch';
|
import { AbstractFetchRemote } from 'Remote/AbstractFetch';
|
||||||
|
|
||||||
class RemoteUserFetch extends AbstractFetchRemote {
|
class RemoteUserFetch extends AbstractFetchRemote {
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {Function} fCallback
|
|
||||||
* @param {object} params
|
|
||||||
* @param {boolean=} bSilent = false
|
|
||||||
*/
|
|
||||||
messageList(fCallback, params, bSilent = false) {
|
|
||||||
const
|
|
||||||
sFolderFullName = pString(params.Folder),
|
|
||||||
folder = getFolderFromCacheList(sFolderFullName),
|
|
||||||
folderHash = folder?.hash || '';
|
|
||||||
|
|
||||||
params = Object.assign({
|
|
||||||
Offset: 0,
|
|
||||||
Limit: SettingsUserStore.messagesPerPage(),
|
|
||||||
Search: '',
|
|
||||||
UidNext: folder?.uidNext || 0, // Used to check for new messages
|
|
||||||
Sort: FolderUserStore.sortMode(),
|
|
||||||
Hash: folderHash + SettingsGet('AccountHash')
|
|
||||||
}, params);
|
|
||||||
params.Folder = sFolderFullName;
|
|
||||||
if (AppUserStore.threadsAllowed() && SettingsUserStore.useThreads()) {
|
|
||||||
params.UseThreads = 1;
|
|
||||||
} else {
|
|
||||||
params.ThreadUid = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
let sGetAdd = '';
|
|
||||||
|
|
||||||
if (folderHash && (!params.Search || !params.Search.includes('is:'))) {
|
|
||||||
sGetAdd = 'MessageList/' +
|
|
||||||
SUB_QUERY_PREFIX +
|
|
||||||
'/' +
|
|
||||||
b64EncodeJSONSafe(params);
|
|
||||||
params = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
bSilent || this.abort('MessageList');
|
|
||||||
this.request('MessageList',
|
|
||||||
fCallback,
|
|
||||||
params,
|
|
||||||
60000, // 60 seconds before aborting
|
|
||||||
sGetAdd
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {?Function} fCallback
|
* @param {?Function} fCallback
|
||||||
* @param {string} sFolderFullName
|
* @param {string} sFolderFullName
|
||||||
|
|
@ -82,7 +36,7 @@ class RemoteUserFetch extends AbstractFetchRemote {
|
||||||
sFolderFullName,
|
sFolderFullName,
|
||||||
iUid,
|
iUid,
|
||||||
AppUserStore.threadsAllowed() && SettingsUserStore.useThreads() ? 1 : 0,
|
AppUserStore.threadsAllowed() && SettingsUserStore.useThreads() ? 1 : 0,
|
||||||
SettingsGet('AccountHash')
|
SettingsGet('accountHash')
|
||||||
])
|
])
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -114,9 +68,9 @@ class RemoteUserFetch extends AbstractFetchRemote {
|
||||||
/*
|
/*
|
||||||
folderMove(sPrevFolderFullName, sNewFolderFullName, bSubscribe) {
|
folderMove(sPrevFolderFullName, sNewFolderFullName, bSubscribe) {
|
||||||
return this.post('FolderMove', FolderUserStore.foldersRenaming, {
|
return this.post('FolderMove', FolderUserStore.foldersRenaming, {
|
||||||
Folder: sPrevFolderFullName,
|
folder: sPrevFolderFullName,
|
||||||
NewFolder: sNewFolderFullName,
|
newFolder: sNewFolderFullName,
|
||||||
Subscribe: bSubscribe ? 1 : 0
|
subscribe: bSubscribe ? 1 : 0
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -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 { createElement, elementById } from 'Common/Globals';
|
import { createElement/*, elementById*/, fireEvent } from 'Common/Globals';
|
||||||
|
|
||||||
import { AbstractScreen } from 'Knoin/AbstractScreen';
|
import { AbstractScreen } from 'Knoin/AbstractScreen';
|
||||||
import { i18nToNodes } from 'Common/Translator';
|
import { i18nToNodes } from 'Common/Translator';
|
||||||
|
|
@ -25,35 +25,43 @@ export class AbstractSettingsScreen extends AbstractScreen {
|
||||||
let settingsScreen = null,
|
let settingsScreen = null,
|
||||||
viewModelDom = null,
|
viewModelDom = null,
|
||||||
RoutedSettingsViewModel = VIEW_MODELS.find(
|
RoutedSettingsViewModel = VIEW_MODELS.find(
|
||||||
SettingsViewModel => subName === SettingsViewModel.__rlSettingsData.route
|
SettingsViewModel => subName === SettingsViewModel.route
|
||||||
);
|
);
|
||||||
|
|
||||||
if (RoutedSettingsViewModel) {
|
if (RoutedSettingsViewModel) {
|
||||||
const vmPlace = elementById('rl-settings-subscreen');
|
// const vmPlace = elementById('V-SettingsPane') || elementById('V-AdminPane);
|
||||||
if (RoutedSettingsViewModel.__vm) {
|
const vmPlace = this.viewModels[1].__dom,
|
||||||
settingsScreen = RoutedSettingsViewModel.__vm;
|
SettingsViewModelClass = RoutedSettingsViewModel.vmc;
|
||||||
|
if (SettingsViewModelClass.__vm) {
|
||||||
|
settingsScreen = SettingsViewModelClass.__vm;
|
||||||
|
viewModelDom = settingsScreen.viewModelDom;
|
||||||
} else if (vmPlace) {
|
} else if (vmPlace) {
|
||||||
viewModelDom = createElement('div',{
|
viewModelDom = createElement('div',{
|
||||||
id: 'V-Settings-' + RoutedSettingsViewModel.name.replace(/(User|Admin)Settings/,''),
|
id: 'V-Settings-' + SettingsViewModelClass.name.replace(/(User|Admin)Settings/,''),
|
||||||
hidden: ''
|
hidden: ''
|
||||||
})
|
})
|
||||||
vmPlace.append(viewModelDom);
|
vmPlace.append(viewModelDom);
|
||||||
|
|
||||||
settingsScreen = new RoutedSettingsViewModel();
|
settingsScreen = new SettingsViewModelClass();
|
||||||
settingsScreen.viewModelDom = viewModelDom;
|
settingsScreen.viewModelDom = viewModelDom;
|
||||||
|
settingsScreen.viewModelTemplateID = RoutedSettingsViewModel.template;
|
||||||
|
|
||||||
RoutedSettingsViewModel.__dom = viewModelDom;
|
SettingsViewModelClass.__dom = viewModelDom;
|
||||||
RoutedSettingsViewModel.__vm = settingsScreen;
|
SettingsViewModelClass.__vm = settingsScreen;
|
||||||
|
|
||||||
|
fireEvent('rl-view-model.create', settingsScreen);
|
||||||
|
|
||||||
ko.applyBindingAccessorsToNode(
|
ko.applyBindingAccessorsToNode(
|
||||||
viewModelDom,
|
viewModelDom,
|
||||||
{
|
{
|
||||||
template: () => ({ name: RoutedSettingsViewModel.__rlSettingsData.template })
|
template: () => ({ name: RoutedSettingsViewModel.template })
|
||||||
},
|
},
|
||||||
settingsScreen
|
settingsScreen
|
||||||
);
|
);
|
||||||
|
|
||||||
settingsScreen.onBuild?.(viewModelDom);
|
settingsScreen.onBuild?.(viewModelDom);
|
||||||
|
|
||||||
|
fireEvent('rl-view-model', settingsScreen);
|
||||||
} else {
|
} else {
|
||||||
console.log('Cannot find sub settings view model position: SettingsSubScreen');
|
console.log('Cannot find sub settings view model position: SettingsSubScreen');
|
||||||
}
|
}
|
||||||
|
|
@ -68,13 +76,13 @@ export class AbstractSettingsScreen extends AbstractScreen {
|
||||||
|
|
||||||
// show
|
// show
|
||||||
settingsScreen.beforeShow?.();
|
settingsScreen.beforeShow?.();
|
||||||
i18nToNodes(settingsScreen.viewModelDom);
|
i18nToNodes(viewModelDom);
|
||||||
settingsScreen.viewModelDom.hidden = false;
|
viewModelDom.hidden = false;
|
||||||
settingsScreen.onShow?.();
|
settingsScreen.onShow?.();
|
||||||
|
|
||||||
this.menu.forEach(item => {
|
this.menu.forEach(item => {
|
||||||
item.selected(
|
item.selected(
|
||||||
item.route === RoutedSettingsViewModel.__rlSettingsData.route
|
item.route === RoutedSettingsViewModel.route
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -96,15 +104,17 @@ export class AbstractSettingsScreen extends AbstractScreen {
|
||||||
}
|
}
|
||||||
|
|
||||||
onBuild() {
|
onBuild() {
|
||||||
VIEW_MODELS.forEach(SettingsViewModel => this.menu.push(SettingsViewModel.__rlSettingsData));
|
// TODO: issue on account switch
|
||||||
|
// When current domain has sieve but the new has not, or current has not and the new has
|
||||||
|
// SettingsViewModel.disabled() || this.menu.push()
|
||||||
|
VIEW_MODELS.forEach(SettingsViewModel => this.menu.push(SettingsViewModel));
|
||||||
}
|
}
|
||||||
|
|
||||||
routes() {
|
routes() {
|
||||||
const DefaultViewModel = VIEW_MODELS.find(
|
const DefaultViewModel = VIEW_MODELS.find(
|
||||||
SettingsViewModel => SettingsViewModel.__rlSettingsData.isDefault
|
SettingsViewModel => SettingsViewModel.isDefault
|
||||||
),
|
),
|
||||||
defaultRoute =
|
defaultRoute = DefaultViewModel?.route || 'general',
|
||||||
DefaultViewModel ? DefaultViewModel.__rlSettingsData.route : 'general',
|
|
||||||
rules = {
|
rules = {
|
||||||
subname: /^(.*)$/,
|
subname: /^(.*)$/,
|
||||||
normalize_: (rquest, vals) => {
|
normalize_: (rquest, vals) => {
|
||||||
|
|
@ -131,13 +141,12 @@ export class AbstractSettingsScreen extends AbstractScreen {
|
||||||
*/
|
*/
|
||||||
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/, '');
|
let name = SettingsViewModelClass.name.replace(/(User|Admin)Settings/, '');
|
||||||
SettingsViewModelClass.__rlSettingsData = {
|
VIEW_MODELS.push({
|
||||||
label: labelName || 'SETTINGS_LABELS/LABEL_' + name.toUpperCase() + '_NAME',
|
vmc: SettingsViewModelClass,
|
||||||
|
label: labelName || 'SETTINGS_LABELS/' + name.toUpperCase(),
|
||||||
route: route || name.toLowerCase(),
|
route: route || name.toLowerCase(),
|
||||||
selected: ko.observable(false),
|
selected: ko.observable(false),
|
||||||
template: template || SettingsViewModelClass.name,
|
template: template || SettingsViewModelClass.name,
|
||||||
isDefault: !!isDefault
|
isDefault: !!isDefault
|
||||||
};
|
});
|
||||||
|
|
||||||
VIEW_MODELS.push(SettingsViewModelClass);
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,6 @@ export class LoginAdminScreen extends AbstractScreen {
|
||||||
}
|
}
|
||||||
|
|
||||||
onShow() {
|
onShow() {
|
||||||
rl.setWindowTitle();
|
rl.setTitle();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,6 @@ export class SettingsAdminScreen extends AbstractSettingsScreen {
|
||||||
}
|
}
|
||||||
|
|
||||||
onShow() {
|
onShow() {
|
||||||
rl.setWindowTitle();
|
rl.setTitle();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,6 @@ export class LoginUserScreen extends AbstractScreen {
|
||||||
}
|
}
|
||||||
|
|
||||||
onShow() {
|
onShow() {
|
||||||
rl.setWindowTitle();
|
rl.setTitle();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,14 @@
|
||||||
import { Scope } from 'Common/Enums';
|
import { ScopeMessageList } from 'Common/Enums';
|
||||||
import { Layout, ClientSideKeyNameMessageListSize } from 'Common/EnumsUser';
|
import { doc, createElement, Settings } from 'Common/Globals';
|
||||||
import { doc, createElement, leftPanelDisabled, moveAction, Settings, elementById } from 'Common/Globals';
|
|
||||||
import { pString, pInt } from 'Common/Utils';
|
import { pString, pInt } from 'Common/Utils';
|
||||||
import { setLayoutResizer } from 'Common/UtilsUser';
|
import { moveAction } from 'Common/UtilsUser';
|
||||||
import { getFolderFromCacheList, getFolderFullName, getFolderInboxName } from 'Common/Cache';
|
import { getFolderFromHashMap, getFolderInboxName } from 'Common/Cache';
|
||||||
import { i18n, initOnStartOrLangChange } from 'Common/Translator';
|
import { i18n, initOnStartOrLangChange } 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 { MessagelistUserStore } from 'Stores/User/Messagelist';
|
import { MessagelistUserStore } from 'Stores/User/Messagelist';
|
||||||
import { ThemeStore } from 'Stores/Theme';
|
|
||||||
|
|
||||||
import { SystemDropDownUserView } from 'View/User/SystemDropDown';
|
import { SystemDropDownUserView } from 'View/User/SystemDropDown';
|
||||||
import { MailFolderList } from 'View/User/MailBox/FolderList';
|
import { MailFolderList } from 'View/User/MailBox/FolderList';
|
||||||
|
|
@ -46,7 +43,7 @@ export class MailBoxUserScreen extends AbstractScreen {
|
||||||
const count = Settings.app('listPermanentFiltered') ? 0 : FolderUserStore.foldersInboxUnreadCount(),
|
const count = Settings.app('listPermanentFiltered') ? 0 : FolderUserStore.foldersInboxUnreadCount(),
|
||||||
email = AccountUserStore.email();
|
email = AccountUserStore.email();
|
||||||
|
|
||||||
rl.setWindowTitle(
|
rl.setTitle(
|
||||||
(email
|
(email
|
||||||
? '' + (0 < count ? '(' + count + ') ' : ' ') + email + ' - '
|
? '' + (0 < count ? '(' + count + ') ' : ' ') + email + ' - '
|
||||||
: ''
|
: ''
|
||||||
|
|
@ -59,11 +56,8 @@ export class MailBoxUserScreen extends AbstractScreen {
|
||||||
*/
|
*/
|
||||||
onShow() {
|
onShow() {
|
||||||
this.updateWindowTitle();
|
this.updateWindowTitle();
|
||||||
|
|
||||||
AppUserStore.focusedState('none');
|
AppUserStore.focusedState('none');
|
||||||
AppUserStore.focusedState(Scope.MessageList);
|
AppUserStore.focusedState(ScopeMessageList);
|
||||||
|
|
||||||
ThemeStore.isMobile() && leftPanelDisabled(true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -73,7 +67,7 @@ export class MailBoxUserScreen extends AbstractScreen {
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
onRoute(folderHash, page, search, messageUid) {
|
onRoute(folderHash, page, search, messageUid) {
|
||||||
const folder = getFolderFromCacheList(getFolderFullName(folderHash.replace(/~([\d]+)$/, '')));
|
const folder = getFolderFromHashMap(folderHash.replace(/~([\d]+)$/, ''));
|
||||||
if (folder) {
|
if (folder) {
|
||||||
FolderUserStore.currentFolder(folder);
|
FolderUserStore.currentFolder(folder);
|
||||||
MessagelistUserStore.page(1 > page ? 1 : page);
|
MessagelistUserStore.page(1 > page ? 1 : page);
|
||||||
|
|
@ -101,7 +95,7 @@ export class MailBoxUserScreen extends AbstractScreen {
|
||||||
FolderUserStore.foldersInboxUnreadCount(e.detail);
|
FolderUserStore.foldersInboxUnreadCount(e.detail);
|
||||||
/* // Disabled in SystemDropDown.html
|
/* // Disabled in SystemDropDown.html
|
||||||
const email = AccountUserStore.email();
|
const email = AccountUserStore.email();
|
||||||
AccountUserStore.accounts.forEach(item =>
|
AccountUserStore.forEach(item =>
|
||||||
email === item?.email && item?.count(e.detail)
|
email === item?.email && item?.count(e.detail)
|
||||||
);
|
);
|
||||||
*/
|
*/
|
||||||
|
|
@ -113,24 +107,6 @@ export class MailBoxUserScreen extends AbstractScreen {
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
onBuild() {
|
onBuild() {
|
||||||
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)
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,10 @@
|
||||||
import { Scope } from 'Common/Enums';
|
import { ScopeSettings } from 'Common/Enums';
|
||||||
import { keyScope, leftPanelDisabled, SettingsCapa } from 'Common/Globals';
|
import { keyScope, 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';
|
||||||
|
|
||||||
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 { ThemeStore } from 'Stores/Theme';
|
|
||||||
|
|
||||||
import { AbstractSettingsScreen, settingsAddViewModel } from 'Screen/AbstractSettings';
|
import { AbstractSettingsScreen, settingsAddViewModel } from 'Screen/AbstractSettings';
|
||||||
|
|
||||||
|
|
@ -23,7 +22,7 @@ import { SettingsPaneUserView } from 'View/User/Settings/Pane';
|
||||||
|
|
||||||
export class SettingsUserScreen extends AbstractSettingsScreen {
|
export class SettingsUserScreen extends AbstractSettingsScreen {
|
||||||
constructor() {
|
constructor() {
|
||||||
super([SystemDropDownUserView, SettingsMenuUserView, SettingsPaneUserView]);
|
super([SettingsMenuUserView, SettingsPaneUserView, SystemDropDownUserView]);
|
||||||
|
|
||||||
const views = [
|
const views = [
|
||||||
UserSettingsGeneral
|
UserSettingsGeneral
|
||||||
|
|
@ -37,6 +36,8 @@ export class SettingsUserScreen extends AbstractSettingsScreen {
|
||||||
views.push(UserSettingsAccounts);
|
views.push(UserSettingsAccounts);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: issue on account switch
|
||||||
|
// When current domain has sieve but the new has not, or current has not and the new has
|
||||||
if (SettingsCapa('Sieve')) {
|
if (SettingsCapa('Sieve')) {
|
||||||
views.push(UserSettingsFilters);
|
views.push(UserSettingsFilters);
|
||||||
}
|
}
|
||||||
|
|
@ -66,12 +67,11 @@ export class SettingsUserScreen extends AbstractSettingsScreen {
|
||||||
|
|
||||||
onShow() {
|
onShow() {
|
||||||
this.setSettingsTitle();
|
this.setSettingsTitle();
|
||||||
keyScope(Scope.Settings);
|
keyScope(ScopeSettings);
|
||||||
ThemeStore.isMobile() && leftPanelDisabled(true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setSettingsTitle() {
|
setSettingsTitle() {
|
||||||
const sEmail = AccountUserStore.email();
|
const sEmail = AccountUserStore.email();
|
||||||
rl.setWindowTitle((sEmail ? sEmail + ' - ' : '') + this.sSettingsTitle);
|
rl.setTitle((sEmail ? sEmail + ' - ' : '') + this.sSettingsTitle);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,9 @@ export class AdminSettingsAbout /*extends AbstractViewSettings*/ {
|
||||||
coreWarning: false,
|
coreWarning: false,
|
||||||
coreVersion: '',
|
coreVersion: '',
|
||||||
coreVersionCompare: -2,
|
coreVersionCompare: -2,
|
||||||
|
load1: 0,
|
||||||
|
load5: 0,
|
||||||
|
load15: 0,
|
||||||
errorDesc: ''
|
errorDesc: ''
|
||||||
});
|
});
|
||||||
this.coreChecking = ko.observable(false).extend({ throttle: 100 });
|
this.coreChecking = ko.observable(false).extend({ throttle: 100 });
|
||||||
|
|
@ -51,18 +54,21 @@ export class AdminSettingsAbout /*extends AbstractViewSettings*/ {
|
||||||
}
|
}
|
||||||
|
|
||||||
onBuild() {
|
onBuild() {
|
||||||
Remote.request('AdminPHPExtensions', (iError, data) => iError || this.phpextensions(data.Result));
|
|
||||||
|
|
||||||
// beforeShow() {
|
// beforeShow() {
|
||||||
this.coreChecking(true);
|
this.coreChecking(true);
|
||||||
Remote.request('AdminUpdateInfo', (iError, data) => {
|
Remote.request('AdminInfo', (iError, data) => {
|
||||||
this.coreChecking(false);
|
this.coreChecking(false);
|
||||||
if (!iError && data?.Result) {
|
data = data?.Result;
|
||||||
|
if (!iError && data) {
|
||||||
|
this.load1(data.system.load?.[0]);
|
||||||
|
this.load5(data.system.load?.[1]);
|
||||||
|
this.load15(data.system.load?.[2]);
|
||||||
|
this.phpextensions(data.php);
|
||||||
this.coreReal(true);
|
this.coreReal(true);
|
||||||
this.coreUpdatable(!!data.Result.Updatable);
|
this.coreUpdatable(!!data.core.updatable);
|
||||||
this.coreWarning(!!data.Result.Warning);
|
this.coreWarning(!!data.core.warning);
|
||||||
this.coreVersion(data.Result.Version || '');
|
this.coreVersion(data.core.version || '');
|
||||||
this.coreVersionCompare(data.Result.VersionCompare);
|
this.coreVersionCompare(data.core.versionCompare);
|
||||||
} else {
|
} else {
|
||||||
this.coreReal(false);
|
this.coreReal(false);
|
||||||
this.coreWarning(false);
|
this.coreWarning(false);
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,8 @@ import { AbstractViewSettings } from 'Knoin/AbstractViews';
|
||||||
export class AdminSettingsBranding extends AbstractViewSettings {
|
export class AdminSettingsBranding extends AbstractViewSettings {
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.addSetting('Title');
|
this.addSetting('title');
|
||||||
this.addSetting('LoadingDescription');
|
this.addSetting('loadingDescription');
|
||||||
this.addSetting('FaviconUrl');
|
this.addSetting('faviconUrl');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,16 +13,20 @@ export class AdminSettingsContacts extends AbstractViewSettings {
|
||||||
super();
|
super();
|
||||||
this.defaultOptionsAfterRender = defaultOptionsAfterRender;
|
this.defaultOptionsAfterRender = defaultOptionsAfterRender;
|
||||||
|
|
||||||
this.addSetting('ContactsPdoDsn');
|
this.addSetting('contactsPdoDsn');
|
||||||
this.addSetting('ContactsPdoUser');
|
this.addSetting('contactsPdoUser');
|
||||||
this.addSetting('ContactsPdoPassword');
|
this.addSetting('contactsPdoPassword');
|
||||||
this.addSetting('ContactsPdoType', () => {
|
this.addSetting('contactsPdoType', () => {
|
||||||
this.testContactsSuccess(false);
|
this.testContactsSuccess(false);
|
||||||
this.testContactsError(false);
|
this.testContactsError(false);
|
||||||
this.testContactsErrorMessage('');
|
this.testContactsErrorMessage('');
|
||||||
});
|
});
|
||||||
|
|
||||||
this.addSettings(['ContactsEnable','ContactsSync']);
|
this.addSettings(['contactsEnable','contactsSync']);
|
||||||
|
|
||||||
|
this.addSetting('contactsMySQLSSLCA');
|
||||||
|
this.addSetting('contactsMySQLSSLVerify');
|
||||||
|
this.addSetting('contactsMySQLSSLCiphers');
|
||||||
|
|
||||||
addObservablesTo(this, {
|
addObservablesTo(this, {
|
||||||
testing: false,
|
testing: false,
|
||||||
|
|
@ -31,6 +35,8 @@ export class AdminSettingsContacts extends AbstractViewSettings {
|
||||||
testContactsErrorMessage: ''
|
testContactsErrorMessage: ''
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.addSetting('contactsSuggestionsLimit');
|
||||||
|
|
||||||
const supportedTypes = SettingsGet('supportedPdoDrivers') || [],
|
const supportedTypes = SettingsGet('supportedPdoDrivers') || [],
|
||||||
types = [{
|
types = [{
|
||||||
id:'sqlite',
|
id:'sqlite',
|
||||||
|
|
@ -90,10 +96,13 @@ export class AdminSettingsContacts extends AbstractViewSettings {
|
||||||
|
|
||||||
this.testing(false);
|
this.testing(false);
|
||||||
}, {
|
}, {
|
||||||
ContactsPdoType: this.contactsPdoType(),
|
PdoType: this.contactsPdoType(),
|
||||||
ContactsPdoDsn: this.contactsPdoDsn(),
|
PdoDsn: this.contactsPdoDsn(),
|
||||||
ContactsPdoUser: this.contactsPdoUser(),
|
PdoUser: this.contactsPdoUser(),
|
||||||
ContactsPdoPassword: this.contactsPdoPassword()
|
PdoPassword: this.contactsPdoPassword(),
|
||||||
|
MySQLSSLCA: this.contactsMySQLSSLCA(),
|
||||||
|
MySQLSSLVerify: this.contactsMySQLSSLVerify(),
|
||||||
|
MySQLSSLCiphers: this.contactsMySQLSSLCiphers()
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,10 +11,25 @@ import { DomainAliasPopupView } from 'View/Popup/DomainAlias';
|
||||||
export class AdminSettingsDomains /*extends AbstractViewSettings*/ {
|
export class AdminSettingsDomains /*extends AbstractViewSettings*/ {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.domains = DomainAdminStore;
|
this.domains = DomainAdminStore;
|
||||||
|
this.username = ko.observable('');
|
||||||
this.domainForDeletion = ko.observable(null).askDeleteHelper();
|
this.domainForDeletion = ko.observable(null).askDeleteHelper();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
testUsername() {
|
||||||
|
Remote.request('AdminDomainMatch',
|
||||||
|
(iError, oData) => {
|
||||||
|
if (oData?.Result?.domain) {
|
||||||
|
alert(`${oData.Result.email} matched domain: ${oData.Result.domain.name}`);
|
||||||
|
} else {
|
||||||
|
alert('No domain match');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
username: this.username
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
createDomain() {
|
createDomain() {
|
||||||
showScreenPopup(DomainPopupView);
|
showScreenPopup(DomainPopupView);
|
||||||
}
|
}
|
||||||
|
|
@ -26,15 +41,15 @@ export class AdminSettingsDomains /*extends AbstractViewSettings*/ {
|
||||||
deleteDomain(domain) {
|
deleteDomain(domain) {
|
||||||
DomainAdminStore.remove(domain);
|
DomainAdminStore.remove(domain);
|
||||||
Remote.request('AdminDomainDelete', DomainAdminStore.fetch, {
|
Remote.request('AdminDomainDelete', DomainAdminStore.fetch, {
|
||||||
Name: domain.name
|
name: domain.name
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
disableDomain(domain) {
|
disableDomain(domain) {
|
||||||
domain.disabled(!domain.disabled());
|
domain.disabled(!domain.disabled());
|
||||||
Remote.request('AdminDomainDisable', DomainAdminStore.fetch, {
|
Remote.request('AdminDomainDisable', DomainAdminStore.fetch, {
|
||||||
Name: domain.name,
|
name: domain.name,
|
||||||
Disabled: domain.disabled() ? 1 : 0
|
disabled: domain.disabled() ? 1 : 0
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -44,7 +59,7 @@ export class AdminSettingsDomains /*extends AbstractViewSettings*/ {
|
||||||
el && ko.dataFor(el) && Remote.request('AdminDomainLoad',
|
el && ko.dataFor(el) && Remote.request('AdminDomainLoad',
|
||||||
(iError, oData) => iError || showScreenPopup(DomainPopupView, [oData.Result]),
|
(iError, oData) => iError || showScreenPopup(DomainPopupView, [oData.Result]),
|
||||||
{
|
{
|
||||||
Name: ko.dataFor(el).name
|
name: ko.dataFor(el).name
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,7 @@
|
||||||
import ko from 'ko';
|
import ko from 'ko';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
isArray,
|
isArray
|
||||||
changeTheme,
|
|
||||||
convertThemeName
|
|
||||||
} from 'Common/Utils';
|
} from 'Common/Utils';
|
||||||
|
|
||||||
import { addObservablesTo, addSubscribablesTo, addComputablesTo } from 'External/ko';
|
import { addObservablesTo, addSubscribablesTo, addComputablesTo } from 'External/ko';
|
||||||
|
|
@ -17,7 +15,7 @@ import { showScreenPopup } from 'Knoin/Knoin';
|
||||||
|
|
||||||
import Remote from 'Remote/Admin/Fetch';
|
import Remote from 'Remote/Admin/Fetch';
|
||||||
|
|
||||||
import { ThemeStore } from 'Stores/Theme';
|
import { ThemeStore, convertThemeName, changeTheme } 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';
|
||||||
|
|
||||||
|
|
@ -30,16 +28,14 @@ export class AdminSettingsGeneral extends AbstractViewSettings {
|
||||||
|
|
||||||
const aLanguagesAdmin = Settings.app('languagesAdmin');
|
const aLanguagesAdmin = Settings.app('languagesAdmin');
|
||||||
this.languagesAdmin = ko.observableArray(isArray(aLanguagesAdmin) ? aLanguagesAdmin : []);
|
this.languagesAdmin = ko.observableArray(isArray(aLanguagesAdmin) ? aLanguagesAdmin : []);
|
||||||
this.languageAdmin = ko.observable(SettingsGet('LanguageAdmin'));
|
this.languageAdmin = ko.observable(SettingsGet('languageAdmin'));
|
||||||
|
|
||||||
this.theme = ThemeStore.theme;
|
this.theme = ThemeStore.theme;
|
||||||
this.themes = ThemeStore.themes;
|
this.themes = ThemeStore.themes;
|
||||||
|
|
||||||
this.addSettings(['AllowLanguagesOnSettings']);
|
this.addSettings(['allowLanguagesOnSettings']);
|
||||||
|
|
||||||
addObservablesTo(this, {
|
addObservablesTo(this, {
|
||||||
attachmentLimitTrigger: SaveSettingStatus.Idle,
|
|
||||||
themeTrigger: SaveSettingStatus.Idle,
|
|
||||||
capaThemes: SettingsCapa('Themes'),
|
capaThemes: SettingsCapa('Themes'),
|
||||||
capaUserBackground: SettingsCapa('UserBackground'),
|
capaUserBackground: SettingsCapa('UserBackground'),
|
||||||
capaAdditionalAccounts: SettingsCapa('AdditionalAccounts'),
|
capaAdditionalAccounts: SettingsCapa('AdditionalAccounts'),
|
||||||
|
|
@ -57,14 +53,14 @@ export class AdminSettingsGeneral extends AbstractViewSettings {
|
||||||
*/
|
*/
|
||||||
|
|
||||||
this.attachmentLimit = ko
|
this.attachmentLimit = ko
|
||||||
.observable(SettingsGet('AttachmentLimit') / (1024 * 1024))
|
.observable(SettingsGet('attachmentLimit') / (1024 * 1024))
|
||||||
.extend({ debounce: 500 });
|
.extend({ debounce: 500 });
|
||||||
|
|
||||||
this.addSetting('Language');
|
this.addSetting('language');
|
||||||
this.addSetting('AttachmentLimit');
|
this.addSetting('attachmentLimit');
|
||||||
this.addSetting('Theme', value => changeTheme(value, this.themeTrigger));
|
this.addSetting('Theme', value => changeTheme(value, this.themeTrigger));
|
||||||
|
|
||||||
this.uploadData = SettingsGet('PhpUploadSizes');
|
this.uploadData = SettingsGet('phpUploadSizes');
|
||||||
this.uploadDataDesc =
|
this.uploadDataDesc =
|
||||||
(this.uploadData?.upload_max_filesize || this.uploadData?.post_max_size)
|
(this.uploadData?.upload_max_filesize || this.uploadData?.post_max_size)
|
||||||
? [
|
? [
|
||||||
|
|
@ -93,9 +89,9 @@ export class AdminSettingsGeneral extends AbstractViewSettings {
|
||||||
addSubscribablesTo(this, {
|
addSubscribablesTo(this, {
|
||||||
languageAdmin: value => {
|
languageAdmin: value => {
|
||||||
this.languageAdminTrigger(SaveSettingStatus.Saving);
|
this.languageAdminTrigger(SaveSettingStatus.Saving);
|
||||||
translatorReload(true, value)
|
translatorReload(value, 1)
|
||||||
.then(fReloadLanguageHelper(SaveSettingStatus.Success), fReloadLanguageHelper(SaveSettingStatus.Failed))
|
.then(fReloadLanguageHelper(SaveSettingStatus.Success), fReloadLanguageHelper(SaveSettingStatus.Failed))
|
||||||
.then(() => Remote.saveSetting('LanguageAdmin', value));
|
.then(() => Remote.saveSetting('languageAdmin', value));
|
||||||
},
|
},
|
||||||
|
|
||||||
capaAdditionalAccounts: fSaveHelper('CapaAdditionalAccounts'),
|
capaAdditionalAccounts: fSaveHelper('CapaAdditionalAccounts'),
|
||||||
|
|
@ -118,7 +114,7 @@ export class AdminSettingsGeneral extends AbstractViewSettings {
|
||||||
showScreenPopup(LanguagesPopupView, [
|
showScreenPopup(LanguagesPopupView, [
|
||||||
this.languageAdmin,
|
this.languageAdmin,
|
||||||
this.languagesAdmin(),
|
this.languagesAdmin(),
|
||||||
SettingsGet('UserLanguageAdmin')
|
SettingsGet('languageUsers')
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import { AbstractViewSettings } from 'Knoin/AbstractViews';
|
||||||
export class AdminSettingsLogin extends AbstractViewSettings {
|
export class AdminSettingsLogin extends AbstractViewSettings {
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.addSetting('LoginDefaultDomain');
|
this.addSetting('loginDefaultDomain');
|
||||||
this.addSettings(['DetermineUserLanguage','DetermineUserDomain','AllowLanguagesOnLogin']);
|
this.addSettings(['determineUserLanguage','determineUserDomain','allowLanguagesOnLogin']);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import ko from 'ko';
|
import ko from 'ko';
|
||||||
|
|
||||||
import { Notification } from 'Common/Enums';
|
import { Notifications } from 'Common/Enums';
|
||||||
import { getNotification } from 'Common/Translator';
|
import { getNotification } from 'Common/Translator';
|
||||||
|
|
||||||
import { PackageAdminStore } from 'Stores/Admin/Package';
|
import { PackageAdminStore } from 'Stores/Admin/Package';
|
||||||
|
|
@ -15,7 +15,7 @@ export class AdminSettingsPackages extends AbstractViewSettings {
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
this.addSettings(['EnabledPlugins']);
|
this.addSettings(['pluginsEnable']);
|
||||||
|
|
||||||
addObservablesTo(this, {
|
addObservablesTo(this, {
|
||||||
packagesError: ''
|
packagesError: ''
|
||||||
|
|
@ -46,7 +46,7 @@ export class AdminSettingsPackages extends AbstractViewSettings {
|
||||||
data && Remote.request('AdminPluginLoad',
|
data && Remote.request('AdminPluginLoad',
|
||||||
(iError, data) => iError || showScreenPopup(PluginPopupView, [data.Result]),
|
(iError, data) => iError || showScreenPopup(PluginPopupView, [data.Result]),
|
||||||
{
|
{
|
||||||
Id: data.id
|
id: data.id
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
// disablePlugin
|
// disablePlugin
|
||||||
|
|
@ -67,7 +67,7 @@ export class AdminSettingsPackages extends AbstractViewSettings {
|
||||||
|
|
||||||
if (iError) {
|
if (iError) {
|
||||||
this.packagesError(
|
this.packagesError(
|
||||||
getNotification(install ? Notification.CantInstallPackage : Notification.CantDeletePackage)
|
getNotification(install ? Notifications.CantInstallPackage : Notifications.CantDeletePackage)
|
||||||
+ (data.ErrorMessage ? ':\n' + data.ErrorMessage : '')
|
+ (data.ErrorMessage ? ':\n' + data.ErrorMessage : '')
|
||||||
);
|
);
|
||||||
} else if (data.Result.Reload) {
|
} else if (data.Result.Reload) {
|
||||||
|
|
@ -84,7 +84,7 @@ export class AdminSettingsPackages extends AbstractViewSettings {
|
||||||
Remote.request('AdminPackageDelete',
|
Remote.request('AdminPackageDelete',
|
||||||
this.requestHelper(packageToDelete, false),
|
this.requestHelper(packageToDelete, false),
|
||||||
{
|
{
|
||||||
Id: packageToDelete.id
|
id: packageToDelete.id
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -96,9 +96,9 @@ export class AdminSettingsPackages extends AbstractViewSettings {
|
||||||
Remote.request('AdminPackageInstall',
|
Remote.request('AdminPackageInstall',
|
||||||
this.requestHelper(packageToInstall, true),
|
this.requestHelper(packageToInstall, true),
|
||||||
{
|
{
|
||||||
Id: packageToInstall.id,
|
id: packageToInstall.id,
|
||||||
Type: packageToInstall.type,
|
type: packageToInstall.type,
|
||||||
File: packageToInstall.file
|
file: packageToInstall.file
|
||||||
},
|
},
|
||||||
60000
|
60000
|
||||||
);
|
);
|
||||||
|
|
@ -113,15 +113,15 @@ export class AdminSettingsPackages extends AbstractViewSettings {
|
||||||
if (iError) {
|
if (iError) {
|
||||||
plugin.enabled(disable);
|
plugin.enabled(disable);
|
||||||
this.packagesError(
|
this.packagesError(
|
||||||
(Notification.UnsupportedPluginPackage === iError && data?.ErrorMessage)
|
(Notifications.UnsupportedPluginPackage === iError && data?.ErrorMessage)
|
||||||
? data.ErrorMessage
|
? data.ErrorMessage
|
||||||
: getNotification(iError)
|
: getNotification(iError)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
// PackageAdminStore.fetch();
|
// PackageAdminStore.fetch();
|
||||||
}, {
|
}, {
|
||||||
Id: plugin.id,
|
id: plugin.id,
|
||||||
Disabled: disable ? 1 : 0
|
disabled: disable ? 1 : 0
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,12 +10,12 @@ export class AdminSettingsSecurity extends AbstractViewSettings {
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
this.addSettings(['UseLocalProxyForExternalImages','VerifySslCertificate','AllowSelfSigned']);
|
this.addSettings(['useLocalProxyForExternalImages']);
|
||||||
|
|
||||||
this.weakPassword = rl.app.weakPassword;
|
this.weakPassword = rl.app.weakPassword;
|
||||||
|
|
||||||
addObservablesTo(this, {
|
addObservablesTo(this, {
|
||||||
adminLogin: SettingsGet('AdminLogin'),
|
adminLogin: SettingsGet('adminLogin'),
|
||||||
adminLoginError: false,
|
adminLoginError: false,
|
||||||
adminPassword: '',
|
adminPassword: '',
|
||||||
adminPasswordNew: '',
|
adminPasswordNew: '',
|
||||||
|
|
@ -68,7 +68,7 @@ export class AdminSettingsSecurity extends AbstractViewSettings {
|
||||||
capaOpenPGP: value => Remote.saveSetting('CapaOpenPGP', value)
|
capaOpenPGP: value => Remote.saveSetting('CapaOpenPGP', value)
|
||||||
});
|
});
|
||||||
|
|
||||||
this.adminTOTP(SettingsGet('AdminTOTP'));
|
this.adminTOTP(SettingsGet('adminTOTP'));
|
||||||
|
|
||||||
decorateKoCommands(this, {
|
decorateKoCommands(this, {
|
||||||
saveAdminUserCommand: self => self.adminLogin().trim() && self.adminPassword()
|
saveAdminUserCommand: self => self.adminLogin().trim() && self.adminPassword()
|
||||||
|
|
@ -102,10 +102,10 @@ export class AdminSettingsSecurity extends AbstractViewSettings {
|
||||||
this.weakPassword(!!data.Result.Weak);
|
this.weakPassword(!!data.Result.Weak);
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
'Login': this.adminLogin(),
|
Login: this.adminLogin(),
|
||||||
'Password': this.adminPassword(),
|
Password: this.adminPassword(),
|
||||||
'NewPassword': this.adminPasswordNew(),
|
newPassword: this.adminPasswordNew(),
|
||||||
'TOTP': this.adminTOTP()
|
TOTP: this.adminTOTP()
|
||||||
});
|
});
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,11 @@
|
||||||
import ko from 'ko';
|
import ko from 'ko';
|
||||||
|
|
||||||
|
//import { koComputable } from 'External/ko';
|
||||||
import { SettingsCapa, SettingsGet } from 'Common/Globals';
|
import { SettingsCapa, SettingsGet } 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';
|
||||||
|
import { SettingsUserStore } from 'Stores/User/Settings';
|
||||||
import Remote from 'Remote/User/Fetch';
|
import Remote from 'Remote/User/Fetch';
|
||||||
|
|
||||||
import { showScreenPopup } from 'Knoin/Knoin';
|
import { showScreenPopup } from 'Knoin/Knoin';
|
||||||
|
|
@ -16,13 +18,18 @@ export class UserSettingsAccounts /*extends AbstractViewSettings*/ {
|
||||||
this.allowAdditionalAccount = SettingsCapa('AdditionalAccounts');
|
this.allowAdditionalAccount = SettingsCapa('AdditionalAccounts');
|
||||||
this.allowIdentities = SettingsCapa('Identities');
|
this.allowIdentities = SettingsCapa('Identities');
|
||||||
|
|
||||||
this.accounts = AccountUserStore.accounts;
|
this.accounts = AccountUserStore;
|
||||||
this.loading = AccountUserStore.loading;
|
this.loading = AccountUserStore.loading;
|
||||||
this.identities = IdentityUserStore;
|
this.identities = IdentityUserStore;
|
||||||
this.mainEmail = SettingsGet('MainEmail');
|
this.mainEmail = SettingsGet('mainEmail');
|
||||||
|
|
||||||
this.accountForDeletion = ko.observable(null).askDeleteHelper();
|
this.accountForDeletion = ko.observable(null).askDeleteHelper();
|
||||||
this.identityForDeletion = ko.observable(null).askDeleteHelper();
|
this.identityForDeletion = ko.observable(null).askDeleteHelper();
|
||||||
|
|
||||||
|
this.showUnread = SettingsUserStore.showUnreadCount;
|
||||||
|
SettingsUserStore.showUnreadCount.subscribe(value => Remote.saveSetting('ShowUnreadCount', value));
|
||||||
|
|
||||||
|
// this.additionalAccounts = koComputable(() => AccountUserStore.filter(account => account.isAdditional()));
|
||||||
}
|
}
|
||||||
|
|
||||||
addNewAccount() {
|
addNewAccount() {
|
||||||
|
|
@ -60,7 +67,7 @@ export class UserSettingsAccounts /*extends AbstractViewSettings*/ {
|
||||||
rl.app.accountsAndIdentities();
|
rl.app.accountsAndIdentities();
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
EmailToDelete: accountToRemove.email
|
emailToDelete: accountToRemove.email
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -74,14 +81,14 @@ export class UserSettingsAccounts /*extends AbstractViewSettings*/ {
|
||||||
this.identityForDeletion(null);
|
this.identityForDeletion(null);
|
||||||
IdentityUserStore.remove(oIdentity => identityToRemove === oIdentity);
|
IdentityUserStore.remove(oIdentity => identityToRemove === oIdentity);
|
||||||
Remote.request('IdentityDelete', () => rl.app.accountsAndIdentities(), {
|
Remote.request('IdentityDelete', () => rl.app.accountsAndIdentities(), {
|
||||||
IdToDelete: identityToRemove.id()
|
idToDelete: identityToRemove.id()
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
accountsAndIdentitiesAfterMove() {
|
accountsAndIdentitiesAfterMove() {
|
||||||
Remote.request('AccountsAndIdentitiesSortOrder', null, {
|
Remote.request('AccountsAndIdentitiesSortOrder', null, {
|
||||||
Accounts: AccountUserStore.getEmailAddresses().filter(v => v != SettingsGet('MainEmail')),
|
Accounts: AccountUserStore.getEmailAddresses().filter(v => v != SettingsGet('mainEmail')),
|
||||||
Identities: IdentityUserStore.map(item => (item ? item.id() : ""))
|
Identities: IdentityUserStore.map(item => (item ? item.id() : ""))
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { koComputable, addObservablesTo } from 'External/ko';
|
import { koComputable, addObservablesTo } from 'External/ko';
|
||||||
import { FolderUserStore } from 'Stores/User/Folder';
|
import { FolderUserStore } from 'Stores/User/Folder';
|
||||||
import { SettingsGet } from 'Common/Globals';
|
import { SettingsGet/*, SettingsCapa*/ } from 'Common/Globals';
|
||||||
|
|
||||||
//export class UserSettingsFilters /*extends AbstractViewSettings*/ {
|
//export class UserSettingsFilters /*extends AbstractViewSettings*/ {
|
||||||
export class UserSettingsFilters /*extends AbstractViewSettings*/ {
|
export class UserSettingsFilters /*extends AbstractViewSettings*/ {
|
||||||
|
|
@ -27,6 +27,14 @@ export class UserSettingsFilters /*extends AbstractViewSettings*/ {
|
||||||
this.scriptForDeletion = ko.observable(null).askDeleteHelper();
|
this.scriptForDeletion = ko.observable(null).askDeleteHelper();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
// TODO: issue on account switch
|
||||||
|
// When current domain has sieve but the new has not, or current has not and the new has
|
||||||
|
disabled() {
|
||||||
|
return !SettingsCapa('Sieve');
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
addScript() {
|
addScript() {
|
||||||
this.editScript();
|
this.editScript();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,11 @@
|
||||||
import ko from 'ko';
|
import ko from 'ko';
|
||||||
|
|
||||||
import { Notification } from 'Common/Enums';
|
import { Notifications } from 'Common/Enums';
|
||||||
import { FolderMetadataKeys } from 'Common/EnumsUser';
|
import { FolderMetadataKeys } from 'Common/EnumsUser';
|
||||||
import { getNotification } from 'Common/Translator';
|
import { getNotification } from 'Common/Translator';
|
||||||
|
|
||||||
import { setFolder, getFolderFromCacheList, removeFolderFromCacheList } from 'Common/Cache';
|
import { getFolderFromCacheList, removeFolderFromCacheList } from 'Common/Cache';
|
||||||
import { defaultOptionsAfterRender } from 'Common/Utils';
|
import { defaultOptionsAfterRender } from 'Common/Utils';
|
||||||
import { sortFolders } from 'Common/Folders';
|
|
||||||
import { initOnStartOrLangChange, i18n } from 'Common/Translator';
|
import { initOnStartOrLangChange, i18n } from 'Common/Translator';
|
||||||
|
|
||||||
import { FolderUserStore } from 'Stores/User/Folder';
|
import { FolderUserStore } from 'Stores/User/Folder';
|
||||||
|
|
@ -18,7 +17,6 @@ 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';
|
|
||||||
|
|
||||||
const folderForDeletion = ko.observable(null).askDeleteHelper();
|
const folderForDeletion = ko.observable(null).askDeleteHelper();
|
||||||
|
|
||||||
|
|
@ -52,52 +50,10 @@ export class UserSettingsFolders /*extends AbstractViewSettings*/ {
|
||||||
|
|
||||||
this.folderForDeletion = folderForDeletion;
|
this.folderForDeletion = folderForDeletion;
|
||||||
|
|
||||||
this.folderForEdit = ko.observable(null).extend({ toggleSubscribeProperty: [this, 'editing'] });
|
|
||||||
|
|
||||||
SettingsUserStore.hideUnsubscribed.subscribe(value => Remote.saveSetting('HideUnsubscribed', value));
|
SettingsUserStore.hideUnsubscribed.subscribe(value => Remote.saveSetting('HideUnsubscribed', value));
|
||||||
SettingsUserStore.unhideKolabFolders.subscribe(value => Remote.saveSetting('UnhideKolabFolders', value));
|
SettingsUserStore.unhideKolabFolders.subscribe(value => Remote.saveSetting('UnhideKolabFolders', value));
|
||||||
}
|
}
|
||||||
|
|
||||||
folderEditOnEnter(folder) {
|
|
||||||
const nameToEdit = folder?.nameForEdit().trim();
|
|
||||||
if (nameToEdit && folder.name() !== nameToEdit) {
|
|
||||||
Remote.abort('Folders').post('FolderRename', FolderUserStore.foldersRenaming, {
|
|
||||||
Folder: folder.fullName,
|
|
||||||
NewFolderName: nameToEdit,
|
|
||||||
Subscribe: folder.isSubscribed() ? 1 : 0
|
|
||||||
})
|
|
||||||
.then(data => {
|
|
||||||
folder.name(nameToEdit/*data.Name*/);
|
|
||||||
if (folder.subFolders.length) {
|
|
||||||
Remote.setTrigger(FolderUserStore.foldersLoading, true);
|
|
||||||
// clearTimeout(Remote.foldersTimeout);
|
|
||||||
// Remote.foldersTimeout = setTimeout(loadFolders, 500);
|
|
||||||
setTimeout(loadFolders, 500);
|
|
||||||
// TODO: rename all subfolders with folder.delimiter to prevent reload?
|
|
||||||
} else {
|
|
||||||
removeFolderFromCacheList(folder.fullName);
|
|
||||||
folder.fullName = data.Result.FullName;
|
|
||||||
setFolder(folder);
|
|
||||||
const parent = getFolderFromCacheList(folder.parentName);
|
|
||||||
sortFolders(parent ? parent.subFolders : FolderUserStore.folderList);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
FolderUserStore.folderListError(
|
|
||||||
getNotification(error.code, '', Notification.CantRenameFolder)
|
|
||||||
+ '.\n' + error.message);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// this.folderForEdit(null);
|
|
||||||
folder.editing(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
folderEditOnEsc(folder) {
|
|
||||||
// this.folderForEdit(null);
|
|
||||||
folder?.editing(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
onShow() {
|
onShow() {
|
||||||
FolderUserStore.folderListError('');
|
FolderUserStore.folderListError('');
|
||||||
}
|
}
|
||||||
|
|
@ -119,17 +75,17 @@ export class UserSettingsFolders /*extends AbstractViewSettings*/ {
|
||||||
&& folderToRemove.askDelete()
|
&& folderToRemove.askDelete()
|
||||||
) {
|
) {
|
||||||
if (0 < folderToRemove.totalEmails()) {
|
if (0 < folderToRemove.totalEmails()) {
|
||||||
// FolderUserStore.folderListError(getNotification(Notification.CantDeleteNonEmptyFolder));
|
// FolderUserStore.folderListError(getNotification(Notifications.CantDeleteNonEmptyFolder));
|
||||||
folderToRemove.errorMsg(getNotification(Notification.CantDeleteNonEmptyFolder));
|
folderToRemove.errorMsg(getNotification(Notifications.CantDeleteNonEmptyFolder));
|
||||||
} else {
|
} else {
|
||||||
folderForDeletion(null);
|
folderForDeletion(null);
|
||||||
|
|
||||||
if (folderToRemove) {
|
if (folderToRemove) {
|
||||||
Remote.abort('Folders').post('FolderDelete', FolderUserStore.foldersDeleting, {
|
Remote.abort('Folders').post('FolderDelete', FolderUserStore.foldersDeleting, {
|
||||||
Folder: folderToRemove.fullName
|
folder: folderToRemove.fullName
|
||||||
}).then(
|
}).then(
|
||||||
() => {
|
() => {
|
||||||
// folderToRemove.flags.push('\\nonexistent');
|
// folderToRemove.attributes.push('\\nonexistent');
|
||||||
folderToRemove.selectable(false);
|
folderToRemove.selectable(false);
|
||||||
// folderToRemove.isSubscribed(false);
|
// folderToRemove.isSubscribed(false);
|
||||||
// folderToRemove.checkable(false);
|
// folderToRemove.checkable(false);
|
||||||
|
|
@ -141,7 +97,7 @@ export class UserSettingsFolders /*extends AbstractViewSettings*/ {
|
||||||
},
|
},
|
||||||
error => {
|
error => {
|
||||||
FolderUserStore.folderListError(
|
FolderUserStore.folderListError(
|
||||||
getNotification(error.code, '', Notification.CantDeleteFolder)
|
getNotification(error.code, '', Notifications.CantDeleteFolder)
|
||||||
+ '.\n' + error.message
|
+ '.\n' + error.message
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -151,13 +107,17 @@ export class UserSettingsFolders /*extends AbstractViewSettings*/ {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
hideError() {
|
||||||
|
this.folderListError('');
|
||||||
|
}
|
||||||
|
|
||||||
toggleFolderKolabType(folder, event) {
|
toggleFolderKolabType(folder, event) {
|
||||||
let type = event.target.value;
|
let type = event.target.value;
|
||||||
// TODO: append '.default' ?
|
// TODO: append '.default' ?
|
||||||
Remote.request('FolderSetMetadata', null, {
|
Remote.request('FolderSetMetadata', null, {
|
||||||
Folder: folder.fullName,
|
folder: folder.fullName,
|
||||||
Key: FolderMetadataKeys.KolabFolderType,
|
key: FolderMetadataKeys.KolabFolderType,
|
||||||
Value: type
|
value: type
|
||||||
});
|
});
|
||||||
folder.kolabType(type);
|
folder.kolabType(type);
|
||||||
}
|
}
|
||||||
|
|
@ -165,8 +125,8 @@ export class UserSettingsFolders /*extends AbstractViewSettings*/ {
|
||||||
toggleFolderSubscription(folder) {
|
toggleFolderSubscription(folder) {
|
||||||
let subscribe = !folder.isSubscribed();
|
let subscribe = !folder.isSubscribed();
|
||||||
Remote.request('FolderSubscribe', null, {
|
Remote.request('FolderSubscribe', null, {
|
||||||
Folder: folder.fullName,
|
folder: folder.fullName,
|
||||||
Subscribe: subscribe ? 1 : 0
|
subscribe: subscribe ? 1 : 0
|
||||||
});
|
});
|
||||||
folder.isSubscribed(subscribe);
|
folder.isSubscribed(subscribe);
|
||||||
}
|
}
|
||||||
|
|
@ -174,8 +134,8 @@ export class UserSettingsFolders /*extends AbstractViewSettings*/ {
|
||||||
toggleFolderCheckable(folder) {
|
toggleFolderCheckable(folder) {
|
||||||
let checkable = !folder.checkable();
|
let checkable = !folder.checkable();
|
||||||
Remote.request('FolderCheckable', null, {
|
Remote.request('FolderCheckable', null, {
|
||||||
Folder: folder.fullName,
|
folder: folder.fullName,
|
||||||
Checkable: checkable ? 1 : 0
|
checkable: checkable ? 1 : 0
|
||||||
});
|
});
|
||||||
folder.checkable(checkable);
|
folder.checkable(checkable);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,8 @@ import ko from 'ko';
|
||||||
|
|
||||||
import { SMAudio } from 'Common/Audio';
|
import { SMAudio } from 'Common/Audio';
|
||||||
import { SaveSettingStatus } from 'Common/Enums';
|
import { SaveSettingStatus } from 'Common/Enums';
|
||||||
import { EditorDefaultType, Layout } from 'Common/EnumsUser';
|
import { LayoutSideView, LayoutBottomView } from 'Common/EnumsUser';
|
||||||
|
import { setRefreshFoldersInterval } from 'Common/Folders';
|
||||||
import { Settings, SettingsGet } from 'Common/Globals';
|
import { Settings, SettingsGet } from 'Common/Globals';
|
||||||
import { isArray } from 'Common/Utils';
|
import { isArray } from 'Common/Utils';
|
||||||
import { addSubscribablesTo, addComputablesTo } from 'External/ko';
|
import { addSubscribablesTo, addComputablesTo } from 'External/ko';
|
||||||
|
|
@ -16,7 +17,6 @@ import { LanguageStore } from 'Stores/Language';
|
||||||
import { SettingsUserStore } from 'Stores/User/Settings';
|
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 { MessagelistUserStore } from 'Stores/User/Messagelist';
|
import { MessagelistUserStore } from 'Stores/User/Messagelist';
|
||||||
|
|
||||||
import Remote from 'Remote/User/Fetch';
|
import Remote from 'Remote/User/Fetch';
|
||||||
|
|
@ -30,23 +30,26 @@ export class UserSettingsGeneral extends AbstractViewSettings {
|
||||||
|
|
||||||
this.language = LanguageStore.language;
|
this.language = LanguageStore.language;
|
||||||
this.languages = LanguageStore.languages;
|
this.languages = LanguageStore.languages;
|
||||||
|
this.hourCycle = LanguageStore.hourCycle;
|
||||||
|
|
||||||
this.soundNotification = SMAudio.notifications;
|
this.soundNotification = SMAudio.notifications;
|
||||||
this.notificationSound = ko.observable(SettingsGet('NotificationSound'));
|
this.notificationSound = ko.observable(SettingsGet('NotificationSound'));
|
||||||
this.notificationSounds = ko.observableArray(SettingsGet('NewMailSounds'));
|
this.notificationSounds = ko.observableArray(SettingsGet('newMailSounds'));
|
||||||
|
|
||||||
this.desktopNotification = NotificationUserStore.enabled;
|
this.desktopNotifications = NotificationUserStore.enabled;
|
||||||
this.isDesktopNotificationAllowed = NotificationUserStore.allowed;
|
this.isDesktopNotificationAllowed = NotificationUserStore.allowed;
|
||||||
|
|
||||||
this.threadsAllowed = AppUserStore.threadsAllowed;
|
this.threadsAllowed = AppUserStore.threadsAllowed;
|
||||||
|
|
||||||
['layout', 'messageReadDelay', 'messagesPerPage',
|
['layout', 'messageReadDelay', 'messagesPerPage', 'checkMailInterval',
|
||||||
'editorDefaultType', 'requestReadReceipt', 'requestDsn', 'pgpSign', 'pgpEncrypt',
|
'editorDefaultType', 'requestReadReceipt', 'requestDsn', 'requireTLS', 'pgpSign', 'pgpEncrypt',
|
||||||
'viewHTML', 'showImages', 'removeColors', 'hideDeleted', 'listInlineAttachments',
|
'viewHTML', 'viewImages', 'viewImagesWhitelist', 'removeColors', 'allowStyles', 'allowDraftAutosave',
|
||||||
'useCheckboxesInList', 'useThreads', 'replySameFolder', 'msgDefaultAction'
|
'hideDeleted', 'listInlineAttachments', 'simpleAttachmentsList', 'collapseBlockquotes', 'maxBlockquotesLevel',
|
||||||
|
'useCheckboxesInList', 'listGrouped', 'useThreads', 'replySameFolder', 'msgDefaultAction', 'allowSpellcheck',
|
||||||
|
'showNextMessage'
|
||||||
].forEach(name => this[name] = SettingsUserStore[name]);
|
].forEach(name => this[name] = SettingsUserStore[name]);
|
||||||
|
|
||||||
this.allowLanguagesOnSettings = !!SettingsGet('AllowLanguagesOnSettings');
|
this.allowLanguagesOnSettings = !!SettingsGet('allowLanguagesOnSettings');
|
||||||
|
|
||||||
this.languageTrigger = ko.observable(SaveSettingStatus.Idle);
|
this.languageTrigger = ko.observable(SaveSettingStatus.Idle);
|
||||||
|
|
||||||
|
|
@ -68,8 +71,8 @@ export class UserSettingsGeneral extends AbstractViewSettings {
|
||||||
editorDefaultTypes: () => {
|
editorDefaultTypes: () => {
|
||||||
translateTrigger();
|
translateTrigger();
|
||||||
return [
|
return [
|
||||||
{ id: EditorDefaultType.Html, name: i18n('SETTINGS_GENERAL/EDITOR_HTML') },
|
{ id: 'Html', name: i18n('SETTINGS_GENERAL/EDITOR_HTML') },
|
||||||
{ id: EditorDefaultType.Plain, name: i18n('SETTINGS_GENERAL/EDITOR_PLAIN') }
|
{ id: 'Plain', name: i18n('SETTINGS_GENERAL/EDITOR_PLAIN') }
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -84,9 +87,9 @@ export class UserSettingsGeneral extends AbstractViewSettings {
|
||||||
layoutTypes: () => {
|
layoutTypes: () => {
|
||||||
translateTrigger();
|
translateTrigger();
|
||||||
return [
|
return [
|
||||||
{ id: Layout.NoPreview, name: i18n('SETTINGS_GENERAL/LAYOUT_NO_SPLIT') },
|
{ id: 0, name: i18n('SETTINGS_GENERAL/LAYOUT_NO_SPLIT') },
|
||||||
{ id: Layout.SidePreview, name: i18n('SETTINGS_GENERAL/LAYOUT_VERTICAL_SPLIT') },
|
{ id: LayoutSideView, name: i18n('SETTINGS_GENERAL/LAYOUT_VERTICAL_SPLIT') },
|
||||||
{ id: Layout.BottomPreview, name: i18n('SETTINGS_GENERAL/LAYOUT_HORIZONTAL_SPLIT') }
|
{ id: LayoutBottomView, name: i18n('SETTINGS_GENERAL/LAYOUT_HORIZONTAL_SPLIT') }
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -95,12 +98,14 @@ export class UserSettingsGeneral extends AbstractViewSettings {
|
||||||
this.addSetting('MsgDefaultAction');
|
this.addSetting('MsgDefaultAction');
|
||||||
this.addSetting('MessageReadDelay');
|
this.addSetting('MessageReadDelay');
|
||||||
this.addSetting('MessagesPerPage');
|
this.addSetting('MessagesPerPage');
|
||||||
|
this.addSetting('CheckMailInterval');
|
||||||
this.addSetting('Layout');
|
this.addSetting('Layout');
|
||||||
|
this.addSetting('MaxBlockquotesLevel');
|
||||||
|
|
||||||
this.addSettings(['ViewHTML', 'ShowImages', 'HideDeleted', 'ListInlineAttachments',
|
this.addSettings(['ViewHTML', 'ViewImages', 'ViewImagesWhitelist', 'HideDeleted', 'RemoveColors', 'AllowStyles',
|
||||||
'UseCheckboxesInList', 'ReplySameFolder',
|
'ListInlineAttachments', 'simpleAttachmentsList', 'UseCheckboxesInList', 'listGrouped', 'ReplySameFolder',
|
||||||
'requestReadReceipt', 'requestDsn', 'pgpSign', 'pgpEncrypt',
|
'requestReadReceipt', 'requestDsn', 'requireTLS', 'pgpSign', 'pgpEncrypt', 'allowSpellcheck',
|
||||||
'DesktopNotifications', 'SoundNotification']);
|
'DesktopNotifications', 'SoundNotification', 'CollapseBlockquotes', 'AllowDraftAutosave', 'showNextMessage']);
|
||||||
|
|
||||||
const fReloadLanguageHelper = (saveSettingsStep) => () => {
|
const fReloadLanguageHelper = (saveSettingsStep) => () => {
|
||||||
this.languageTrigger(saveSettingsStep);
|
this.languageTrigger(saveSettingsStep);
|
||||||
|
|
@ -110,18 +115,13 @@ export class UserSettingsGeneral extends AbstractViewSettings {
|
||||||
addSubscribablesTo(this, {
|
addSubscribablesTo(this, {
|
||||||
language: value => {
|
language: value => {
|
||||||
this.languageTrigger(SaveSettingStatus.Saving);
|
this.languageTrigger(SaveSettingStatus.Saving);
|
||||||
translatorReload(false, value)
|
translatorReload(value)
|
||||||
.then(fReloadLanguageHelper(SaveSettingStatus.Success), fReloadLanguageHelper(SaveSettingStatus.Failed))
|
.then(fReloadLanguageHelper(SaveSettingStatus.Success), fReloadLanguageHelper(SaveSettingStatus.Failed))
|
||||||
.then(() => Remote.saveSetting('Language', value));
|
.then(() => Remote.saveSetting('language', value));
|
||||||
},
|
},
|
||||||
|
|
||||||
removeColors: value => {
|
hourCycle: value =>
|
||||||
let dom = MessageUserStore.bodiesDom();
|
Remote.saveSetting('hourCycle', value),
|
||||||
if (dom) {
|
|
||||||
dom.innerHTML = '';
|
|
||||||
}
|
|
||||||
Remote.saveSetting('RemoveColors', value);
|
|
||||||
},
|
|
||||||
|
|
||||||
notificationSound: value => {
|
notificationSound: value => {
|
||||||
Remote.saveSetting('NotificationSound', value);
|
Remote.saveSetting('NotificationSound', value);
|
||||||
|
|
@ -131,6 +131,10 @@ export class UserSettingsGeneral extends AbstractViewSettings {
|
||||||
useThreads: value => {
|
useThreads: value => {
|
||||||
MessagelistUserStore([]);
|
MessagelistUserStore([]);
|
||||||
Remote.saveSetting('UseThreads', value);
|
Remote.saveSetting('UseThreads', value);
|
||||||
|
},
|
||||||
|
|
||||||
|
checkMailInterval: () => {
|
||||||
|
setRefreshFoldersInterval(SettingsUserStore.checkMailInterval());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { koComputable } from 'External/ko';
|
import { koComputable } from 'External/ko';
|
||||||
|
|
||||||
import { SettingsCapa } from 'Common/Globals';
|
import { SettingsCapa } from 'Common/Globals';
|
||||||
import { i18n, translateTrigger } from 'Common/Translator';
|
import { i18n, translateTrigger, relativeTime } from 'Common/Translator';
|
||||||
|
|
||||||
import { AbstractViewSettings } from 'Knoin/AbstractViews';
|
import { AbstractViewSettings } from 'Knoin/AbstractViews';
|
||||||
|
|
||||||
|
|
@ -10,30 +10,29 @@ import { SettingsUserStore } from 'Stores/User/Settings';
|
||||||
import { GnuPGUserStore } from 'Stores/User/GnuPG';
|
import { GnuPGUserStore } from 'Stores/User/GnuPG';
|
||||||
import { OpenPGPUserStore } from 'Stores/User/OpenPGP';
|
import { OpenPGPUserStore } from 'Stores/User/OpenPGP';
|
||||||
|
|
||||||
import Remote from 'Remote/User/Fetch';
|
|
||||||
|
|
||||||
import { showScreenPopup } from 'Knoin/Knoin';
|
import { showScreenPopup } from 'Knoin/Knoin';
|
||||||
|
|
||||||
import { OpenPgpImportPopupView } from 'View/Popup/OpenPgpImport';
|
import { OpenPgpImportPopupView } from 'View/Popup/OpenPgpImport';
|
||||||
import { OpenPgpGeneratePopupView } from 'View/Popup/OpenPgpGenerate';
|
import { OpenPgpGeneratePopupView } from 'View/Popup/OpenPgpGenerate';
|
||||||
|
|
||||||
|
//import Remote from 'Remote/User/Fetch';
|
||||||
|
|
||||||
export class UserSettingsSecurity extends AbstractViewSettings {
|
export class UserSettingsSecurity extends AbstractViewSettings {
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
let i18nLogout = (key, params) => i18n('SETTINGS_SECURITY/AUTOLOGIN_' + key, params);
|
|
||||||
this.autoLogout = SettingsUserStore.autoLogout;
|
this.autoLogout = SettingsUserStore.autoLogout;
|
||||||
this.autoLogoutOptions = koComputable(() => {
|
this.autoLogoutOptions = koComputable(() => {
|
||||||
translateTrigger();
|
translateTrigger();
|
||||||
return [
|
return [
|
||||||
{ id: 0, name: i18nLogout('NEVER_OPTION_NAME') },
|
{ id: 0, name: i18n('SETTINGS_SECURITY/AUTOLOGIN_NEVER_OPTION_NAME') },
|
||||||
{ id: 5, name: i18nLogout('MINUTES_OPTION_NAME', { MINUTES: 5 }) },
|
{ id: 5, name: relativeTime(300) },
|
||||||
{ id: 10, name: i18nLogout('MINUTES_OPTION_NAME', { MINUTES: 10 }) },
|
{ id: 10, name: relativeTime(600) },
|
||||||
{ id: 30, name: i18nLogout('MINUTES_OPTION_NAME', { MINUTES: 30 }) },
|
{ id: 30, name: relativeTime(1800) },
|
||||||
{ id: 60, name: i18nLogout('MINUTES_OPTION_NAME', { MINUTES: 60 }) },
|
{ id: 60, name: relativeTime(3600) },
|
||||||
{ id: 60 * 2, name: i18nLogout('HOURS_OPTION_NAME', { HOURS: 2 }) },
|
{ id: 120, name: relativeTime(7200) },
|
||||||
{ id: 60 * 5, name: i18nLogout('HOURS_OPTION_NAME', { HOURS: 5 }) },
|
{ id: 300, name: relativeTime(18000) },
|
||||||
{ id: 60 * 10, name: i18nLogout('HOURS_OPTION_NAME', { HOURS: 10 }) }
|
{ id: 600, name: relativeTime(36000) }
|
||||||
];
|
];
|
||||||
});
|
});
|
||||||
this.addSetting('AutoLogout');
|
this.addSetting('AutoLogout');
|
||||||
|
|
@ -47,10 +46,6 @@ export class UserSettingsSecurity extends AbstractViewSettings {
|
||||||
this.canOpenPGP = SettingsCapa('OpenPGP');
|
this.canOpenPGP = SettingsCapa('OpenPGP');
|
||||||
this.canGnuPG = GnuPGUserStore.isSupported();
|
this.canGnuPG = GnuPGUserStore.isSupported();
|
||||||
this.canMailvelope = !!window.mailvelope;
|
this.canMailvelope = !!window.mailvelope;
|
||||||
|
|
||||||
this.allowDraftAutosave = SettingsUserStore.allowDraftAutosave;
|
|
||||||
|
|
||||||
this.allowDraftAutosave.subscribe(value => Remote.saveSetting('AllowDraftAutosave', value))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
addOpenPgpKey() {
|
addOpenPgpKey() {
|
||||||
|
|
@ -67,5 +62,11 @@ export class UserSettingsSecurity extends AbstractViewSettings {
|
||||||
* The iframe will be injected into the container identified by selector.
|
* The iframe will be injected into the container identified by selector.
|
||||||
*/
|
*/
|
||||||
window.mailvelope && mailvelope.createSettingsContainer('#mailvelope-settings'/*[, keyring], options*/);
|
window.mailvelope && mailvelope.createSettingsContainer('#mailvelope-settings'/*[, keyring], options*/);
|
||||||
|
/**
|
||||||
|
* https://github.com/the-djmaze/snappymail/issues/973
|
||||||
|
Remote.request('GetStoredPGPKeys', (iError, data) => {
|
||||||
|
console.dir([iError, data]);
|
||||||
|
});
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
import { addObservablesTo } from 'External/ko';
|
import { addObservablesTo } from 'External/ko';
|
||||||
|
|
||||||
import { SaveSettingStatus, UploadErrorCode } from 'Common/Enums';
|
import { SaveSettingStatus, UploadErrorCode } from 'Common/Enums';
|
||||||
import { changeTheme, convertThemeName } from 'Common/Utils';
|
|
||||||
import { themePreviewLink, serverRequest } from 'Common/Links';
|
import { themePreviewLink, serverRequest } from 'Common/Links';
|
||||||
import { i18n } from 'Common/Translator';
|
import { i18n } from 'Common/Translator';
|
||||||
import { SettingsCapa } from 'Common/Globals';
|
import { SettingsCapa } from 'Common/Globals';
|
||||||
|
|
||||||
import { ThemeStore } from 'Stores/Theme';
|
import { ThemeStore, convertThemeName, changeTheme } from 'Stores/Theme';
|
||||||
|
import { addSubscribablesTo } from 'External/ko';
|
||||||
|
|
||||||
import Remote from 'Remote/User/Fetch';
|
import Remote from 'Remote/User/Fetch';
|
||||||
|
|
||||||
|
|
@ -25,20 +25,22 @@ export class UserSettingsThemes /*extends AbstractViewSettings*/ {
|
||||||
this.fontSansSerif = ThemeStore.fontSansSerif;
|
this.fontSansSerif = ThemeStore.fontSansSerif;
|
||||||
this.fontSerif = ThemeStore.fontSerif;
|
this.fontSerif = ThemeStore.fontSerif;
|
||||||
this.fontMono = ThemeStore.fontMono;
|
this.fontMono = ThemeStore.fontMono;
|
||||||
ThemeStore.fontSansSerif.subscribe(value => {
|
addSubscribablesTo(ThemeStore, {
|
||||||
|
fontSansSerif: value => {
|
||||||
Remote.saveSettings(null, {
|
Remote.saveSettings(null, {
|
||||||
fontSansSerif: value
|
fontSansSerif: value
|
||||||
});
|
});
|
||||||
});
|
},
|
||||||
ThemeStore.fontSerif.subscribe(value => {
|
fontSerif: value => {
|
||||||
Remote.saveSettings(null, {
|
Remote.saveSettings(null, {
|
||||||
fontSerif: value
|
fontSerif: value
|
||||||
});
|
});
|
||||||
});
|
},
|
||||||
ThemeStore.fontMono.subscribe(value => {
|
fontMono: value => {
|
||||||
Remote.saveSettings(null, {
|
Remote.saveSettings(null, {
|
||||||
fontMono: value
|
fontMono: value
|
||||||
});
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.theme = ThemeStore.theme;
|
this.theme = ThemeStore.theme;
|
||||||
|
|
@ -61,6 +63,10 @@ export class UserSettingsThemes /*extends AbstractViewSettings*/ {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setTheme(theme) {
|
||||||
|
ThemeStore.theme(theme.name);
|
||||||
|
}
|
||||||
|
|
||||||
onBuild() {
|
onBuild() {
|
||||||
const currentTheme = ThemeStore.theme();
|
const currentTheme = ThemeStore.theme();
|
||||||
|
|
||||||
|
|
@ -89,8 +95,8 @@ export class UserSettingsThemes /*extends AbstractViewSettings*/ {
|
||||||
})
|
})
|
||||||
.on('onComplete', (id, result, data) => {
|
.on('onComplete', (id, result, data) => {
|
||||||
themeBackground.loading(false);
|
themeBackground.loading(false);
|
||||||
themeBackground.name(data?.Result?.Name || '');
|
themeBackground.name(data?.Result?.name || '');
|
||||||
themeBackground.hash(data?.Result?.Hash || '');
|
themeBackground.hash(data?.Result?.hash || '');
|
||||||
if (!themeBackground.name() || !themeBackground.hash()) {
|
if (!themeBackground.name() || !themeBackground.hash()) {
|
||||||
let errorMsg = '';
|
let errorMsg = '';
|
||||||
if (data.ErrorCode) {
|
if (data.ErrorCode) {
|
||||||
|
|
|
||||||
|
|
@ -82,11 +82,9 @@ export class AbstractModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
revivePropertiesFromJson(json) {
|
revivePropertiesFromJson(json) {
|
||||||
let model = this.constructor;
|
const model = this.constructor,
|
||||||
if (!model.validJson(json)) {
|
valid = model.validJson(json);
|
||||||
return false;
|
valid && forEachObjectEntry(json, (key, value) => {
|
||||||
}
|
|
||||||
forEachObjectEntry(json, (key, value) => {
|
|
||||||
if ('@' !== key[0]) try {
|
if ('@' !== key[0]) try {
|
||||||
key = key[0].toLowerCase() + key.slice(1);
|
key = key[0].toLowerCase() + key.slice(1);
|
||||||
switch (typeof this[key])
|
switch (typeof this[key])
|
||||||
|
|
@ -104,9 +102,12 @@ export class AbstractModel {
|
||||||
case 'string':
|
case 'string':
|
||||||
this[key] = typeCast(this[key], value);
|
this[key] = typeCast(this[key], value);
|
||||||
break;
|
break;
|
||||||
// fall through
|
|
||||||
case 'undefined':
|
case 'undefined':
|
||||||
default:
|
console.log(`Undefined ${model.name}.${key} not set`);
|
||||||
|
// this[key] = value;
|
||||||
|
break;
|
||||||
|
// default:
|
||||||
|
// console.log((typeof this[key])+` ${model.name}.${key} not revived`);
|
||||||
// console.log((typeof this[key])+' '+(model.name)+'.'+key+' not revived');
|
// console.log((typeof this[key])+' '+(model.name)+'.'+key+' not revived');
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
@ -114,7 +115,7 @@ export class AbstractModel {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return true;
|
return valid;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -53,10 +53,10 @@ export class FilterModel extends AbstractModel {
|
||||||
actionValueFourth: '',
|
actionValueFourth: '',
|
||||||
actionValueFourthError: false,
|
actionValueFourthError: false,
|
||||||
|
|
||||||
actionMarkAsRead: false,
|
markAsRead: false,
|
||||||
|
|
||||||
actionKeep: true,
|
keep: true,
|
||||||
actionNoStop: false,
|
stop: true,
|
||||||
|
|
||||||
actionType: FilterAction.MoveTo
|
actionType: FilterAction.MoveTo
|
||||||
});
|
});
|
||||||
|
|
@ -181,24 +181,24 @@ export class FilterModel extends AbstractModel {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
toJson() {
|
toJSON() {
|
||||||
return {
|
return {
|
||||||
// '@Object': 'Object/Filter',
|
// '@Object': 'Object/Filter',
|
||||||
ID: this.id,
|
ID: this.id,
|
||||||
Enabled: this.enabled() ? 1 : 0,
|
Enabled: this.enabled() ? 1 : 0,
|
||||||
Name: this.name(),
|
Name: this.name,
|
||||||
Conditions: this.conditions.map(item => item.toJson()),
|
Conditions: this.conditions,
|
||||||
ConditionsType: this.conditionsType(),
|
ConditionsType: this.conditionsType,
|
||||||
|
|
||||||
ActionType: this.actionType(),
|
ActionType: this.actionType(),
|
||||||
ActionValue: this.actionValue(),
|
ActionValue: this.actionValue,
|
||||||
ActionValueSecond: this.actionValueSecond(),
|
ActionValueSecond: this.actionValueSecond,
|
||||||
ActionValueThird: this.actionValueThird(),
|
ActionValueThird: this.actionValueThird,
|
||||||
ActionValueFourth: this.actionValueFourth(),
|
ActionValueFourth: this.actionValueFourth,
|
||||||
|
|
||||||
Keep: this.actionKeep() ? 1 : 0,
|
Keep: this.keep() ? 1 : 0,
|
||||||
Stop: this.actionNoStop() ? 0 : 1,
|
Stop: this.stop() ? 1 : 0,
|
||||||
MarkAsRead: this.actionMarkAsRead() ? 1 : 0
|
MarkAsRead: this.markAsRead() ? 1 : 0
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -210,25 +210,20 @@ export class FilterModel extends AbstractModel {
|
||||||
this.conditions.remove(oConditionToDelete);
|
this.conditions.remove(oConditionToDelete);
|
||||||
}
|
}
|
||||||
|
|
||||||
setRecipients() {
|
|
||||||
// this.actionValueFourth(AccountUserStore.getEmailAddresses().join(', '));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @static
|
* @static
|
||||||
* @param {FetchJsonFilter} json
|
* @param {FetchJsonFilter} json
|
||||||
* @returns {?FilterModel}
|
* @returns {?FilterModel}
|
||||||
*/
|
*/
|
||||||
static reviveFromJson(json) {
|
static reviveFromJson(json) {
|
||||||
|
json.id = json.ID;
|
||||||
|
delete json.ID;
|
||||||
const filter = super.reviveFromJson(json);
|
const filter = super.reviveFromJson(json);
|
||||||
if (filter) {
|
if (filter) {
|
||||||
filter.id = '' + (filter.id || '');
|
filter.id = '' + (filter.id || '');
|
||||||
filter.conditions(
|
filter.conditions(
|
||||||
(json.Conditions || []).map(aData => FilterConditionModel.reviveFromJson(aData)).filter(v => v)
|
(json.Conditions || []).map(aData => FilterConditionModel.reviveFromJson(aData)).filter(v => v)
|
||||||
);
|
);
|
||||||
filter.actionKeep(0 != json.Keep);
|
|
||||||
filter.actionNoStop(0 == json.Stop);
|
|
||||||
filter.actionMarkAsRead(1 == json.MarkAsRead);
|
|
||||||
}
|
}
|
||||||
return filter;
|
return filter;
|
||||||
}
|
}
|
||||||
|
|
@ -245,7 +240,7 @@ export class FilterModel extends AbstractModel {
|
||||||
|
|
||||||
filter.conditionsType(this.conditionsType());
|
filter.conditionsType(this.conditionsType());
|
||||||
|
|
||||||
filter.actionMarkAsRead(this.actionMarkAsRead());
|
filter.markAsRead(this.markAsRead());
|
||||||
|
|
||||||
filter.actionType(this.actionType());
|
filter.actionType(this.actionType());
|
||||||
|
|
||||||
|
|
@ -256,8 +251,8 @@ export class FilterModel extends AbstractModel {
|
||||||
filter.actionValueThird(this.actionValueThird());
|
filter.actionValueThird(this.actionValueThird());
|
||||||
filter.actionValueFourth(this.actionValueFourth());
|
filter.actionValueFourth(this.actionValueFourth());
|
||||||
|
|
||||||
filter.actionKeep(this.actionKeep());
|
filter.keep(this.keep());
|
||||||
filter.actionNoStop(this.actionNoStop());
|
filter.stop(this.stop());
|
||||||
|
|
||||||
filter.conditions(this.conditions.map(item => item.cloneSelf()));
|
filter.conditions(this.conditions.map(item => item.cloneSelf()));
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -81,13 +81,13 @@ export class FilterConditionModel extends AbstractModel {
|
||||||
|
|
||||||
// static reviveFromJson(json) {}
|
// static reviveFromJson(json) {}
|
||||||
|
|
||||||
toJson() {
|
toJSON() {
|
||||||
return {
|
return {
|
||||||
// '@Object': 'Object/FilterCondition',
|
// '@Object': 'Object/FilterCondition',
|
||||||
Field: this.field(),
|
Field: this.field,
|
||||||
Type: this.type(),
|
Type: this.type,
|
||||||
Value: this.value(),
|
Value: this.value,
|
||||||
ValueSecond: this.valueSecond()
|
ValueSecond: this.valueSecond
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -125,7 +125,7 @@ function filtersToSieveScript(filters)
|
||||||
// actions
|
// actions
|
||||||
block ? result.push('{') : (sTab = '');
|
block ? result.push('{') : (sTab = '');
|
||||||
|
|
||||||
if (filter.actionMarkAsRead() && ['None','MoveTo','Forward'].includes(filter.actionType())) {
|
if (filter.markAsRead() && ['None','MoveTo','Forward'].includes(filter.actionType())) {
|
||||||
require.imap4flags = 1;
|
require.imap4flags = 1;
|
||||||
result.push(sTab + 'addflag "\\\\Seen";');
|
result.push(sTab + 'addflag "\\\\Seen";');
|
||||||
}
|
}
|
||||||
|
|
@ -182,7 +182,7 @@ function filtersToSieveScript(filters)
|
||||||
break; }
|
break; }
|
||||||
case 'Forward':
|
case 'Forward':
|
||||||
if (value) {
|
if (value) {
|
||||||
if (filter.actionKeep()) {
|
if (filter.keep()) {
|
||||||
require.fileinto = 1;
|
require.fileinto = 1;
|
||||||
result.push(sTab + 'fileinto "INBOX";');
|
result.push(sTab + 'fileinto "INBOX";');
|
||||||
}
|
}
|
||||||
|
|
@ -201,7 +201,7 @@ function filtersToSieveScript(filters)
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
filter.actionNoStop() || result.push(sTab + 'stop;');
|
filter.stop() && result.push(sTab + 'stop;');
|
||||||
|
|
||||||
block && result.push('}');
|
block && result.push('}');
|
||||||
|
|
||||||
|
|
@ -213,7 +213,7 @@ function filtersToSieveScript(filters)
|
||||||
'/*',
|
'/*',
|
||||||
'BEGIN:FILTER:' + filter.id,
|
'BEGIN:FILTER:' + filter.id,
|
||||||
'BEGIN:HEADER',
|
'BEGIN:HEADER',
|
||||||
btoa(unescape(encodeURIComponent(JSON.stringify(filter.toJson())))).match(split).join(eol) + 'END:HEADER',
|
btoa(unescape(encodeURIComponent(JSON.stringify(filter)))).match(split).join(eol) + 'END:HEADER',
|
||||||
'*/',
|
'*/',
|
||||||
filter.enabled() ? '' : '/* @Filter is disabled ',
|
filter.enabled() ? '' : '/* @Filter is disabled ',
|
||||||
filterToString(filter, require),
|
filterToString(filter, require),
|
||||||
|
|
@ -290,13 +290,12 @@ export class SieveScriptModel extends AbstractModel
|
||||||
return !this.nameError();
|
return !this.nameError();
|
||||||
}
|
}
|
||||||
|
|
||||||
toJson() {
|
toJSON() {
|
||||||
return {
|
return {
|
||||||
name: this.name(),
|
name: this.name,
|
||||||
active: this.active() ? 1 : 0,
|
active: this.active,
|
||||||
body: this.body()
|
body: this.body
|
||||||
// body: this.allowFilters() ? this.body() : this.filtersToRaw()
|
// body: this.allowFilters() ? this.body() : this.filtersToRaw()
|
||||||
// filters: this.filters.map(item => item.toJson())
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -316,13 +315,7 @@ export class SieveScriptModel extends AbstractModel
|
||||||
const script = super.reviveFromJson(json);
|
const script = super.reviveFromJson(json);
|
||||||
if (script) {
|
if (script) {
|
||||||
if (script.allowFilters()) {
|
if (script.allowFilters()) {
|
||||||
script.filters(
|
script.filters(sieveScriptToFilters(script.body()));
|
||||||
Array.isArray(json.filters) && json.filters.length
|
|
||||||
? json.filters.map(aData => FilterModel.reviveFromJson(aData)).filter(v => v)
|
|
||||||
: sieveScriptToFilters(script.body())
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
script.filters([]);
|
|
||||||
}
|
}
|
||||||
script.canBeDeleted(SIEVE_FILE_NAME !== json.name);
|
script.canBeDeleted(SIEVE_FILE_NAME !== json.name);
|
||||||
script.exists(true);
|
script.exists(true);
|
||||||
|
|
|
||||||
|
|
@ -25,13 +25,13 @@ export const
|
||||||
arr.map(item => item.toString?.() || item).join(separator),
|
arr.map(item => item.toString?.() || item).join(separator),
|
||||||
/*
|
/*
|
||||||
getNotificationMessage = code => {
|
getNotificationMessage = code => {
|
||||||
let key = getKeyByValue(Notification, code);
|
let key = getKeyByValue(Notifications, code);
|
||||||
return key ? I18N_DATA.NOTIFICATIONS[i18nKey(key).replace('_NOTIFICATION', '_ERROR')] : '';
|
return key ? I18N_DATA.NOTIFICATIONS[i18nKey(key).replace('_NOTIFICATION', '_ERROR')] : '';
|
||||||
rl.i18n('NOTIFICATIONS/')
|
rl.i18n('NOTIFICATIONS/')
|
||||||
},
|
},
|
||||||
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 (Notifications.ClientViewError === code && message) {
|
||||||
return message;
|
return message;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,31 +17,29 @@ const
|
||||||
|
|
||||||
// import { folderListOptionsBuilder } from 'Common/Folders';
|
// import { folderListOptionsBuilder } from 'Common/Folders';
|
||||||
/**
|
/**
|
||||||
* @param {Array=} aDisabled
|
|
||||||
* @param {Array=} aHeaderLines
|
|
||||||
* @param {Function=} fRenameCallback
|
|
||||||
* @returns {Array}
|
* @returns {Array}
|
||||||
*/
|
*/
|
||||||
folderListOptionsBuilder = (
|
folderListOptionsBuilder = () => {
|
||||||
aDisabled,
|
|
||||||
aHeaderLines,
|
|
||||||
fRenameCallback
|
|
||||||
) => {
|
|
||||||
const
|
const
|
||||||
aResult = [],
|
aResult = [{
|
||||||
|
id: '',
|
||||||
|
name: '',
|
||||||
|
system: false,
|
||||||
|
disabled: false
|
||||||
|
}],
|
||||||
sDeepPrefix = '\u00A0\u00A0\u00A0',
|
sDeepPrefix = '\u00A0\u00A0\u00A0',
|
||||||
showUnsubscribed = true/*!SettingsUserStore.hideUnsubscribed()*/,
|
showUnsubscribed = true/*!SettingsUserStore.hideUnsubscribed()*/,
|
||||||
|
|
||||||
|
disabled = rl.settings.get('sieveAllowFileintoInbox') ? '' : 'INBOX',
|
||||||
|
|
||||||
foldersWalk = folders => {
|
foldersWalk = folders => {
|
||||||
folders.forEach(oItem => {
|
folders.forEach(oItem => {
|
||||||
if (showUnsubscribed || oItem.hasSubscriptions() || !oItem.exists) {
|
if (showUnsubscribed || oItem.hasSubscriptions() || !oItem.exists) {
|
||||||
aResult.push({
|
aResult.push({
|
||||||
id: oItem.fullName,
|
id: oItem.fullName,
|
||||||
name:
|
name: sDeepPrefix.repeat(oItem.deep) + oItem.detailedName(),
|
||||||
sDeepPrefix.repeat(oItem.deep) +
|
|
||||||
fRenameCallback(oItem),
|
|
||||||
system: false,
|
system: false,
|
||||||
disabled: !oItem.selectable() || aDisabled.includes(oItem.fullName)
|
disabled: !oItem.selectable() || disabled == oItem.fullName
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -52,18 +50,6 @@ const
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
fRenameCallback = fRenameCallback || (oItem => oItem.name());
|
|
||||||
Array.isArray(aDisabled) || (aDisabled = []);
|
|
||||||
|
|
||||||
Array.isArray(aHeaderLines) && aHeaderLines.forEach(line =>
|
|
||||||
aResult.push({
|
|
||||||
id: line[0],
|
|
||||||
name: line[1],
|
|
||||||
system: false,
|
|
||||||
disabled: false
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
// FolderUserStore.folderList()
|
// FolderUserStore.folderList()
|
||||||
foldersWalk(window.Sieve.folderList() || []);
|
foldersWalk(window.Sieve.folderList() || []);
|
||||||
|
|
||||||
|
|
@ -82,13 +68,7 @@ export class FilterPopupView extends rl.pluginPopupView {
|
||||||
});
|
});
|
||||||
|
|
||||||
this.defaultOptionsAfterRender = defaultOptionsAfterRender;
|
this.defaultOptionsAfterRender = defaultOptionsAfterRender;
|
||||||
this.folderSelectList = koComputable(() =>
|
this.folderSelectList = koComputable(() => folderListOptionsBuilder());
|
||||||
folderListOptionsBuilder(
|
|
||||||
[rl.settings.get('SieveAllowFileintoInbox') ? '' : 'INBOX'],
|
|
||||||
[['', '']],
|
|
||||||
item => item?.localName() || ''
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
this.selectedFolderValue.subscribe(() => this.filter().actionValueError(false));
|
this.selectedFolderValue.subscribe(() => this.filter().actionValueError(false));
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -86,7 +86,7 @@ export class SieveScriptPopupView extends rl.pluginPopupView {
|
||||||
// this.close();
|
// this.close();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
script.toJson()
|
script.toJSON()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,8 +27,8 @@ try {
|
||||||
setItem: (key, value) => {
|
setItem: (key, value) => {
|
||||||
data[key] = ''+value; // forces the value to a string
|
data[key] = ''+value; // forces the value to a string
|
||||||
document.cookie = sName+'='+encodeURIComponent(JSON.stringify(data))
|
document.cookie = sName+'='+encodeURIComponent(JSON.stringify(data))
|
||||||
+"; expires="+((new Date(Date.now()+(365*24*60*60*1000))).toGMTString())
|
+";expires="+((new Date(Date.now()+(365*24*60*60*1000))).toGMTString())
|
||||||
+"; path=/; samesite=strict";
|
+";path=/;samesite=strict";
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
1
dev/Storage/Passphrases.js
Normal file
1
dev/Storage/Passphrases.js
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
export const Passphrases = new Map();
|
||||||
|
|
@ -20,6 +20,6 @@ DomainAdminStore.fetch = () => {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
IncludeAliases: 1
|
includeAliases: 1
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import ko from 'ko';
|
import ko from 'ko';
|
||||||
import { isArray } from 'Common/Utils';
|
import { isArray } from 'Common/Utils';
|
||||||
|
import { getNotification } from 'Common/Translator';
|
||||||
import Remote from 'Remote/Admin/Fetch';
|
import Remote from 'Remote/Admin/Fetch';
|
||||||
|
|
||||||
export const PackageAdminStore = ko.observableArray();
|
export const PackageAdminStore = ko.observableArray();
|
||||||
|
|
@ -16,6 +17,11 @@ PackageAdminStore.fetch = () => {
|
||||||
PackageAdminStore.loading(false);
|
PackageAdminStore.loading(false);
|
||||||
if (iError) {
|
if (iError) {
|
||||||
PackageAdminStore.real(false);
|
PackageAdminStore.real(false);
|
||||||
|
PackageAdminStore.error(getNotification(iError));
|
||||||
|
// let error = getNotification(iError);
|
||||||
|
// if (data.message) { error = data.message + error; }
|
||||||
|
// if (data.reason) { error = data.reason + " " + error; }
|
||||||
|
// PackageAdminStore.error(error);
|
||||||
} else {
|
} else {
|
||||||
PackageAdminStore.real(!!data.Result.Real);
|
PackageAdminStore.real(!!data.Result.Real);
|
||||||
PackageAdminStore.error(data.Result.Error);
|
PackageAdminStore.error(data.Result.Error);
|
||||||
|
|
|
||||||
|
|
@ -6,11 +6,13 @@ export const LanguageStore = {
|
||||||
language: ko.observable(''),
|
language: ko.observable(''),
|
||||||
languages: ko.observableArray(),
|
languages: ko.observableArray(),
|
||||||
userLanguage: ko.observable(''),
|
userLanguage: ko.observable(''),
|
||||||
|
hourCycle: ko.observable(''),
|
||||||
|
|
||||||
populate: function() {
|
populate: function() {
|
||||||
const aLanguages = Settings.app('languages');
|
const aLanguages = Settings.app('languages');
|
||||||
this.languages(isArray(aLanguages) ? aLanguages : []);
|
this.languages(isArray(aLanguages) ? aLanguages : []);
|
||||||
this.language(SettingsGet('Language'));
|
this.language(SettingsGet('language'));
|
||||||
this.userLanguage(SettingsGet('UserLanguage'));
|
this.userLanguage(SettingsGet('userLanguage'));
|
||||||
|
this.hourCycle(SettingsGet('hourCycle'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,17 @@
|
||||||
import ko from 'ko';
|
import ko from 'ko';
|
||||||
import { doc, $htmlCL, elementById, leftPanelDisabled, Settings, SettingsGet } from 'Common/Globals';
|
import { $htmlCL, appEl, elementById, leftPanelDisabled, Settings, SettingsGet } from 'Common/Globals';
|
||||||
import { isArray } from 'Common/Utils';
|
import { isArray, arrayLength } from 'Common/Utils';
|
||||||
import { serverRequestRaw } from 'Common/Links';
|
import { cssLink, serverRequestRaw } from 'Common/Links';
|
||||||
|
import { SaveSettingStatus } from 'Common/Enums';
|
||||||
|
import { addSubscribablesTo } from 'External/ko';
|
||||||
|
|
||||||
export const ThemeStore = {
|
let __themeTimer = 0;
|
||||||
|
|
||||||
|
export const
|
||||||
|
// Also see Styles/_Values.less @maxMobileWidth
|
||||||
|
isMobile = matchMedia('(max-width: 799px)'),
|
||||||
|
|
||||||
|
ThemeStore = {
|
||||||
theme: ko.observable(''),
|
theme: ko.observable(''),
|
||||||
themes: ko.observableArray(),
|
themes: ko.observableArray(),
|
||||||
userBackgroundName: ko.observable(''),
|
userBackgroundName: ko.observable(''),
|
||||||
|
|
@ -11,30 +19,57 @@ export const ThemeStore = {
|
||||||
fontSansSerif: ko.observable(''),
|
fontSansSerif: ko.observable(''),
|
||||||
fontSerif: ko.observable(''),
|
fontSerif: ko.observable(''),
|
||||||
fontMono: ko.observable(''),
|
fontMono: ko.observable(''),
|
||||||
isMobile: ko.observable($htmlCL.contains('rl-mobile')),
|
isMobile: ko.observable(false)
|
||||||
|
},
|
||||||
|
|
||||||
populate: () => {
|
initThemes = () => {
|
||||||
const themes = Settings.app('themes');
|
const theme = SettingsGet('Theme'),
|
||||||
|
themes = Settings.app('themes');
|
||||||
|
|
||||||
ThemeStore.themes(isArray(themes) ? themes : []);
|
ThemeStore.themes(isArray(themes) ? themes : []);
|
||||||
ThemeStore.theme(SettingsGet('Theme'));
|
ThemeStore.theme(theme);
|
||||||
|
changeTheme(theme);
|
||||||
if (!ThemeStore.isMobile()) {
|
if (!ThemeStore.isMobile()) {
|
||||||
ThemeStore.userBackgroundName(SettingsGet('UserBackgroundName'));
|
ThemeStore.userBackgroundName(SettingsGet('userBackgroundName'));
|
||||||
ThemeStore.userBackgroundHash(SettingsGet('UserBackgroundHash'));
|
ThemeStore.userBackgroundHash(SettingsGet('userBackgroundHash'));
|
||||||
}
|
}
|
||||||
ThemeStore.fontSansSerif(SettingsGet('fontSansSerif'));
|
ThemeStore.fontSansSerif(SettingsGet('fontSansSerif'));
|
||||||
ThemeStore.fontSerif(SettingsGet('fontSerif'));
|
ThemeStore.fontSerif(SettingsGet('fontSerif'));
|
||||||
ThemeStore.fontMono(SettingsGet('fontMono'));
|
ThemeStore.fontMono(SettingsGet('fontMono'));
|
||||||
|
|
||||||
leftPanelDisabled(ThemeStore.isMobile());
|
leftPanelDisabled(ThemeStore.isMobile());
|
||||||
|
},
|
||||||
|
|
||||||
|
changeTheme = (value, themeTrigger = ()=>0) => {
|
||||||
|
const themeStyle = elementById('app-theme-style'),
|
||||||
|
clearTimer = () => {
|
||||||
|
__themeTimer = setTimeout(() => themeTrigger(SaveSettingStatus.Idle), 1000);
|
||||||
|
},
|
||||||
|
url = cssLink(value);
|
||||||
|
|
||||||
|
if (themeStyle.dataset.name != value) {
|
||||||
|
clearTimeout(__themeTimer);
|
||||||
|
|
||||||
|
themeTrigger(SaveSettingStatus.Saving);
|
||||||
|
|
||||||
|
rl.app.Remote.abort('theme').get('theme', url)
|
||||||
|
.then(data => {
|
||||||
|
if (2 === arrayLength(data)) {
|
||||||
|
themeStyle.textContent = data[1];
|
||||||
|
themeStyle.dataset.name = value;
|
||||||
|
themeTrigger(SaveSettingStatus.Success);
|
||||||
}
|
}
|
||||||
};
|
clearTimer();
|
||||||
|
}, clearTimer);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
ThemeStore.isMobile.subscribe(value => $htmlCL.toggle('rl-mobile', value));
|
convertThemeName = theme => theme.replace(/@[a-z]+$/, '').replace(/([A-Z])/g, ' $1').trim();
|
||||||
|
|
||||||
ThemeStore.fontSansSerif.subscribe(value => {
|
addSubscribablesTo(ThemeStore, {
|
||||||
|
fontSansSerif: value => {
|
||||||
if (null != value) {
|
if (null != value) {
|
||||||
let cl = elementById('rl-app').classList;
|
let cl = appEl.classList;
|
||||||
cl.forEach(name => {
|
cl.forEach(name => {
|
||||||
if (name.startsWith('font') && !/font(Serif|Mono)/.test(name)) {
|
if (name.startsWith('font') && !/font(Serif|Mono)/.test(name)) {
|
||||||
cl.remove(name);
|
cl.remove(name);
|
||||||
|
|
@ -42,28 +77,33 @@ ThemeStore.fontSansSerif.subscribe(value => {
|
||||||
});
|
});
|
||||||
value && cl.add('font'+value);
|
value && cl.add('font'+value);
|
||||||
}
|
}
|
||||||
});
|
},
|
||||||
ThemeStore.fontSerif.subscribe(value => {
|
|
||||||
|
fontSerif: value => {
|
||||||
if (null != value) {
|
if (null != value) {
|
||||||
let cl = elementById('rl-app').classList;
|
let cl = appEl.classList;
|
||||||
cl.forEach(name => name.startsWith('fontSerif') && cl.remove(name));
|
cl.forEach(name => name.startsWith('fontSerif') && cl.remove(name));
|
||||||
value && cl.add('fontSerif'+value);
|
value && cl.add('fontSerif'+value);
|
||||||
}
|
}
|
||||||
});
|
},
|
||||||
ThemeStore.fontMono.subscribe(value => {
|
|
||||||
|
fontMono: value => {
|
||||||
if (null != value) {
|
if (null != value) {
|
||||||
let cl = elementById('rl-app').classList;
|
let cl = appEl.classList;
|
||||||
cl.forEach(name => name.startsWith('fontMono') && cl.remove(name));
|
cl.forEach(name => name.startsWith('fontMono') && cl.remove(name));
|
||||||
value && cl.add('fontMono'+value);
|
value && cl.add('fontMono'+value);
|
||||||
}
|
}
|
||||||
});
|
},
|
||||||
|
|
||||||
ThemeStore.userBackgroundHash.subscribe(value => {
|
userBackgroundHash: value => {
|
||||||
if (value) {
|
appEl.classList.toggle('UserBackground', !!value);
|
||||||
$htmlCL.add('UserBackground');
|
appEl.style.backgroundImage = value ? "url("+serverRequestRaw('UserBackground', value)+")" : null;
|
||||||
doc.body.style.backgroundImage = "url("+serverRequestRaw('UserBackground', value)+")";
|
|
||||||
} else {
|
|
||||||
$htmlCL.remove('UserBackground');
|
|
||||||
doc.body.removeAttribute('style');
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
isMobile.onchange = e => {
|
||||||
|
ThemeStore.isMobile(e.matches);
|
||||||
|
$htmlCL.toggle('rl-mobile', e.matches);
|
||||||
|
leftPanelDisabled(e.matches);
|
||||||
|
};
|
||||||
|
isMobile.onchange(isMobile);
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,10 @@
|
||||||
import { addObservablesTo, koArrayWithDestroy } from 'External/ko';
|
import { addObservablesTo, koArrayWithDestroy } from 'External/ko';
|
||||||
|
|
||||||
export const AccountUserStore = {
|
export const AccountUserStore = koArrayWithDestroy();
|
||||||
accounts: koArrayWithDestroy(),
|
|
||||||
loading: ko.observable(false).extend({ debounce: 100 }),
|
|
||||||
|
|
||||||
getEmailAddresses: () => AccountUserStore.accounts.map(item => item.email)
|
AccountUserStore.loading = ko.observable(false).extend({ debounce: 100 });
|
||||||
};
|
|
||||||
|
AccountUserStore.getEmailAddresses = () => AccountUserStore.map(item => item.email);
|
||||||
|
|
||||||
addObservablesTo(AccountUserStore, {
|
addObservablesTo(AccountUserStore, {
|
||||||
email: '',
|
email: '',
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
import { keyScope, leftPanelDisabled, SettingsGet, elementById } from 'Common/Globals';
|
import { keyScope, leftPanelDisabled, SettingsGet, elementById } from 'Common/Globals';
|
||||||
import { addObservablesTo } from 'External/ko';
|
import { addObservablesTo } from 'External/ko';
|
||||||
import { ThemeStore } from 'Stores/Theme';
|
import { ThemeStore } from 'Stores/Theme';
|
||||||
|
import { arePopupsVisible } from 'Knoin/Knoin';
|
||||||
|
|
||||||
export const AppUserStore = {
|
export const AppUserStore = {
|
||||||
allowContacts: () => !!SettingsGet('ContactsIsAllowed')
|
allowContacts: () => !!SettingsGet('contactsAllowed')
|
||||||
};
|
};
|
||||||
|
|
||||||
addObservablesTo(AppUserStore, {
|
addObservablesTo(AppUserStore, {
|
||||||
|
|
@ -15,10 +16,9 @@ addObservablesTo(AppUserStore, {
|
||||||
AppUserStore.focusedState.subscribe(value => {
|
AppUserStore.focusedState.subscribe(value => {
|
||||||
['FolderList','MessageList','MessageView'].forEach(name => {
|
['FolderList','MessageList','MessageView'].forEach(name => {
|
||||||
if (name === value) {
|
if (name === value) {
|
||||||
keyScope(value);
|
arePopupsVisible() || keyScope(value);
|
||||||
ThemeStore.isMobile() && leftPanelDisabled('FolderList' !== value);
|
ThemeStore.isMobile() && leftPanelDisabled('FolderList' !== value);
|
||||||
}
|
}
|
||||||
let dom = elementById('V-Mail'+name);
|
elementById('V-Mail'+name).classList.toggle('focused', name === value);
|
||||||
dom?.classList.toggle('focused', name === value);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue