mirror of
https://github.com/the-djmaze/snappymail.git
synced 2026-07-11 00:14:50 +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
|
||||
|
||||
[security]
|
||||
; Enable CSRF protection (http://en.wikipedia.org/wiki/Cross-site_request_forgery)
|
||||
csrf_protection = On
|
||||
custom_server_signature = "SnappyMail"
|
||||
x_xss_protection_header = "1; mode=block"
|
||||
openpgp = Off
|
||||
|
||||
; Access settings
|
||||
allow_admin_panel = On
|
||||
|
||||
; Login and password for web admin panel
|
||||
admin_login = "admin"
|
||||
admin_password = ""
|
||||
admin_totp = ""
|
||||
|
||||
; Access settings
|
||||
allow_admin_panel = On
|
||||
hide_x_mailer_header = On
|
||||
admin_panel_host = ""
|
||||
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 = ""
|
||||
|
||||
; Report CSP errors to PHP and/or SnappyMail Log
|
||||
csp_report = Off
|
||||
|
||||
; A valid cipher method from https://php.net/openssl_get_cipher_methods
|
||||
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]
|
||||
allow_update = Off
|
||||
|
||||
|
|
@ -93,20 +111,35 @@ cafile = ""
|
|||
capath = ""
|
||||
|
||||
; 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]
|
||||
quota = On
|
||||
|
||||
; Allow clear folder and delete messages without moving to trash
|
||||
dangerous_actions = On
|
||||
message_actions = On
|
||||
|
||||
; Allow download attachments as Zip (and optionally others)
|
||||
attachments_actions = On
|
||||
|
||||
[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 = ""
|
||||
|
||||
; Allow language selection on webmail login screen
|
||||
allow_languages_on_login = On
|
||||
|
||||
; Detect language from browser header `Accept-Language`
|
||||
determine_user_language = On
|
||||
|
||||
; Like default_domain but then HTTP_HOST/SERVER_NAME without www.
|
||||
determine_user_domain = Off
|
||||
login_lowercase = On
|
||||
|
||||
|
|
@ -123,7 +156,7 @@ sign_me_auto = "DefaultOff"
|
|||
; Enable plugin support
|
||||
enable = Off
|
||||
|
||||
; List of enabled plugins
|
||||
; Comma-separated list of enabled plugins
|
||||
enabled_list = ""
|
||||
|
||||
[defaults]
|
||||
|
|
@ -145,6 +178,9 @@ mail_reply_same_folder = Off
|
|||
; Enable logging
|
||||
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).
|
||||
; 0 = Emergency
|
||||
; 1 = Alert
|
||||
|
|
@ -201,6 +237,8 @@ auth_syslog = On
|
|||
[debug]
|
||||
; Special option required for development purposes
|
||||
enable = Off
|
||||
javascript = Off
|
||||
css = Off
|
||||
|
||||
[cache]
|
||||
; The section controls caching of the entire application.
|
||||
|
|
@ -208,6 +246,9 @@ enable = Off
|
|||
; Enables caching in the system
|
||||
enable = On
|
||||
|
||||
; Path where cache files will be stored
|
||||
path = ""
|
||||
|
||||
; Additional caching key. If changed, cache is purged
|
||||
index = "v1"
|
||||
|
||||
|
|
@ -225,55 +266,41 @@ http_expires = 3600
|
|||
|
||||
; Caching message UIDs when searching and sorting (threading)
|
||||
server_uids = On
|
||||
system_data = On
|
||||
|
||||
[imap]
|
||||
use_sort = On
|
||||
use_force_selection = Off
|
||||
use_thread = On
|
||||
use_move = Off
|
||||
use_expunge_all_on_delete = Off
|
||||
body_text_limit = 555000
|
||||
message_list_fast_simple_search = On
|
||||
message_list_count_limit_trigger = 0
|
||||
message_list_date_filter = 0
|
||||
message_list_permanent_filter = ""
|
||||
message_all_headers = Off
|
||||
large_thread_limit = 50
|
||||
folder_list_limit = 200
|
||||
show_login_alert = On
|
||||
use_list_status = On
|
||||
timeout = 300
|
||||
disable_metadata = Off
|
||||
fetch_new_messages = On
|
||||
|
||||
[labs]
|
||||
cache_system_data = On
|
||||
; Display message RFC 2822 date and time header, instead of the arrival internal date.
|
||||
date_from_headers = On
|
||||
autocreate_system_folders = 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
|
||||
allow_html_editor_biti_buttons = Off
|
||||
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_timeout = 60
|
||||
sieve_auth_plain_initial = On
|
||||
sieve_allow_fileinto_inbox = Off
|
||||
sieve_timeout = 10
|
||||
sasl_allow_plain = On
|
||||
sasl_allow_scram_sha = Off
|
||||
sasl_allow_cram_md5 = Off
|
||||
|
||||
; PHP mail() remove To and Subject headers
|
||||
mail_func_clear_headers = On
|
||||
|
||||
; PHP mail() set -f emailaddress
|
||||
mail_func_additional_parameters = Off
|
||||
folders_spec_limit = 50
|
||||
curl_proxy = ""
|
||||
curl_proxy_auth = ""
|
||||
force_https = Off
|
||||
custom_login_link = ""
|
||||
custom_logout_link = ""
|
||||
allow_external_login = Off
|
||||
http_client_ip_check_proxy = Off
|
||||
fast_cache_memcache_host = "127.0.0.1"
|
||||
fast_cache_memcache_port = 11211
|
||||
|
|
@ -283,12 +310,11 @@ use_local_proxy_for_external_images = On
|
|||
image_exif_auto_rotate = Off
|
||||
cookie_default_path = ""
|
||||
cookie_default_secure = Off
|
||||
check_new_messages = On
|
||||
replace_env_in_configuration = ""
|
||||
boundary_prefix = ""
|
||||
dev_email = ""
|
||||
dev_password = ""
|
||||
|
||||
[version]
|
||||
current = "2.20.0"
|
||||
saved = "Fri, 30 Sep 2022 12:34:56 +0000"
|
||||
current = "2.28.4"
|
||||
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]
|
||||
- PHP 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)**
|
||||
Place them here (few lines) or as attachments (many lines)
|
||||
|
|
|
|||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -17,6 +17,7 @@
|
|||
/data
|
||||
/MULTIPLY
|
||||
/include.php
|
||||
/issues
|
||||
.idea/
|
||||
.env
|
||||
/test
|
||||
|
|
|
|||
|
|
@ -26,7 +26,6 @@
|
|||
# Header set Strict-Transport-Security "max-age=31536000"
|
||||
Header set imagetoolbar "no"
|
||||
# Header set X-Content-Type-Options "nosniff"
|
||||
# Header set X-Frame-Options "DENY"
|
||||
# Header set X-XSS-Protection "1; mode=block"
|
||||
Header set 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**
|
||||
|
||||
1. Edit data/\_data_/\_default_/configs/application.ini
|
||||
2. Set 'cache_system_data' to Off
|
||||
2. Set `[cache] system_data` to Off
|
||||
|
||||
**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)**.
|
||||
http://www.gnu.org/licenses/agpl-3.0.html
|
||||
|
||||
Copyright (c) 2020 - 2022 SnappyMail
|
||||
Copyright (c) 2020 - 2023 SnappyMail
|
||||
Copyright (c) 2013 - 2022 RainLoop
|
||||
|
||||
## Modifications
|
||||
|
||||
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
|
||||
* Auth failed attempts written to syslog
|
||||
* Added Fail2ban instructions
|
||||
|
|
@ -57,10 +57,11 @@ This fork of RainLoop has the following changes:
|
|||
* Removed background video support
|
||||
* Removed Sentry (Application Monitoring and Error Tracking Software)
|
||||
* Removed Spyc yaml
|
||||
* Removed OwnCloud
|
||||
* Replaced gulp-uglify with gulp-terser
|
||||
* CRLF => LF line endings
|
||||
* Embed boot.js and boot.css into index.html
|
||||
* Ongoing removal of old JavaScript code (things are native these days)
|
||||
* Removal of old JavaScript code (things are native these days)
|
||||
* Added modified [Squire](https://github.com/neilj/Squire) HTML editor as replacement for CKEditor
|
||||
* Updated [Sabre/VObject](https://github.com/sabre-io/vobject)
|
||||
* Split Admin specific JavaScript code from User code
|
||||
|
|
@ -105,7 +106,6 @@ Supported are:
|
|||
The result is faster and smaller download code (good for mobile networks).
|
||||
|
||||
* Added dev/prototype.js for some additional features
|
||||
* boot.js without webpack overhead
|
||||
* Modified Jua.js to be without jQuery
|
||||
* Replaced Autolinker with simple https/email detection
|
||||
* 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 inputosaurus with own code
|
||||
* Replaced keymaster with own shortcuts handler
|
||||
* Replaced OpenPGP.js v2 with OpenPGP.js v5
|
||||
* Removed ifvisible.js
|
||||
* Removed pikaday
|
||||
* Removed underscore
|
||||
|
|
@ -140,28 +141,28 @@ RainLoop 1.17 vs SnappyMail
|
|||
|
||||
|js/* |RainLoop |Snappy |
|
||||
|--------------- |--------: |--------: |
|
||||
|admin.js |2.170.153 | 81.545 |
|
||||
|app.js |4.207.787 | 406.032 |
|
||||
|boot.js | 868.735 | 2.050 |
|
||||
|libs.js | 658.812 | 192.965 |
|
||||
|sieve.js | 0 | 86.121 |
|
||||
|admin.js |2.170.153 | 80.102 |
|
||||
|app.js |4.207.787 | 407.874 |
|
||||
|boot.js | 868.735 | 4.142 |
|
||||
|libs.js | 658.812 | 187.076 |
|
||||
|sieve.js | 0 | 85.141 |
|
||||
|polyfills.js | 334.608 | 0 |
|
||||
|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 |
|
||||
|--------------- |--------: |--------: |------: |------: |--------: |--------: |
|
||||
|admin.min.js | 256.831 | 40.255 | 73.606 | 13.435 | 60.877 | 12.047 |
|
||||
|app.min.js | 515.367 | 188.046 |139.456 | 62.537 |110.485 | 53.693 |
|
||||
|boot.min.js | 84.659 | 1.252 | 26.998 | 778 | 23.643 | 628 |
|
||||
|libs.min.js | 584.772 | 92.055 |180.901 | 34.113 |155.182 | 30.641 |
|
||||
|sieve.min.js | 0 | 41.926 | 0 | 10.484 | 0 | 9.451 |
|
||||
|admin.min.js | 256.831 | 39.350 | 73.606 | 13.163 | 60.877 | 11.805 |
|
||||
|app.min.js | 515.367 | 186.311 |139.456 | 62.929 |110.485 | 54.076 |
|
||||
|boot.min.js | 84.659 | 2.084 | 26.998 | 1.202 | 23.643 | 1.003 |
|
||||
|libs.min.js | 584.772 | 90.808 |180.901 | 33.754 |155.182 | 30.224 |
|
||||
|sieve.min.js | 0 | 41.399 | 0 | 10.394 | 0 | 9.356 |
|
||||
|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+sieve |1.217.635 | 323.279 |358.761 |107.912 |299.485 | 94.413 |
|
||||
|TOTAL admin | 959.099 | 133.562 |292.911 | 48.326 |249.877 | 43.316 |
|
||||
|TOTAL user |1.217.635 | 279.203 |358.761 | 97.885 |299.485 | 85.303 |
|
||||
|TOTAL user+sieve |1.217.635 | 320.602 |358.761 |108.279 |299.485 | 94.659 |
|
||||
|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
|
||||
|
||||
|
|
@ -169,6 +170,7 @@ For a user its around 70% smaller and faster than traditional RainLoop.
|
|||
* Themes work in mobile mode
|
||||
* Bugfix invalid/conflicting css rules
|
||||
* Use flexbox
|
||||
* Use border-box
|
||||
* Split app.css to have separate admin.css
|
||||
* Remove oldschool 'float'
|
||||
* 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 |
|
||||
|------------ |-------: |------: |------: |------: |--------: |
|
||||
|app.css | 340.331 | 82.870 | 46.946 | 17.215 | 14.727 |
|
||||
|app.min.css | 274.947 | 66.646 | 39.647 | 15.260 | 13.322 |
|
||||
|app.css | 340.331 | 84.390 | 46.946 | 17.605 | 15.084 |
|
||||
|app.min.css | 274.947 | 67.774 | 39.647 | 15.487 | 13.527 |
|
||||
|boot.css | | 1.326 | | 664 | 545 |
|
||||
|boot.min.css | | 1.071 | | 590 | 474 |
|
||||
|admin.css | | 30.506 | | 6.907 | 5.998 |
|
||||
|admin.min.css | | 24.677 | | 6.267 | 5.498 |
|
||||
|admin.css | | 30.482 | | 6.988 | 6.092 |
|
||||
|admin.min.css | | 24.607 | | 6.315 | 5.579 |
|
||||
|
||||
### PGP
|
||||
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.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 | |
|
||||
|
||||
|
||||
|
|
@ -216,7 +218,6 @@ The [Squire](https://github.com/neilj/Squire) implementation is not 100% compati
|
|||
Still TODO:
|
||||
|
||||
* support for tables (really needed?!?)
|
||||
* support BIDI (really needed?!?)
|
||||
|
||||
| | normal | min | gzip | min gzip |
|
||||
|-------- |-------: |-------: |------: |--------: |
|
||||
|
|
|
|||
15
_include.php
15
_include.php
|
|
@ -4,16 +4,6 @@
|
|||
|
||||
//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.
|
||||
*/
|
||||
|
|
@ -34,3 +24,8 @@
|
|||
* Additional configuration file name
|
||||
*/
|
||||
//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
|
||||
echo "\x1b[33;1m === Nextcloud === \x1b[0m\n";
|
||||
|
||||
$cert_dir = $_SERVER['HOME'].'/.nextcloud/certificates';
|
||||
|
||||
$nc_destination = "{$destPath}snappymail-{$package->version}-nextcloud.tar";
|
||||
|
||||
@unlink($nc_destination);
|
||||
@unlink("{$nc_destination}.gz");
|
||||
|
||||
$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/@");
|
||||
$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);
|
||||
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}");
|
||||
$newName = str_replace('/.htaccess', '/_htaccess', $newFile);
|
||||
$nc_tar->addFile($file, "snappymail/app/{$newName}");
|
||||
$hashes["app/{$newFile}"] = hash_file('sha512', $file);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
$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');
|
||||
$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);
|
||||
//$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);
|
||||
|
||||
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';
|
||||
|
||||
$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);
|
||||
|
||||
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
|
||||
define('ROOT_DIR', dirname(__DIR__));
|
||||
define('PLUGINS_DEST_DIR', __DIR__ . '/dist/releases/plugins');
|
||||
|
||||
is_dir(PLUGINS_DEST_DIR) || mkdir(PLUGINS_DEST_DIR, 0777, true);
|
||||
|
|
@ -11,6 +10,8 @@ foreach ($files as $fileinfo) {
|
|||
$fileinfo->isDir() || unlink($fileinfo->getRealPath());
|
||||
}
|
||||
|
||||
$terser = ROOT_DIR . '/node_modules/terser/bin/terser';
|
||||
|
||||
$manifest = [];
|
||||
require ROOT_DIR . '/snappymail/v/0.0.0/app/libraries/RainLoop/Plugins/AbstractPlugin.php';
|
||||
$keys = [
|
||||
|
|
@ -42,6 +43,21 @@ foreach (glob(ROOT_DIR . '/plugins/*', GLOB_NOSORT | GLOB_ONLYDIR) as $dir) {
|
|||
$version = $manifest_item['version'];
|
||||
if (0 < floatval($version)) {
|
||||
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['id'] = $name;
|
||||
$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);
|
||||
unlink($tar_destination);
|
||||
rename("{$tar_destination}.gz", $tgz_destination);
|
||||
/*
|
||||
if (Phar::canWrite()) {
|
||||
$phar_destination = PLUGINS_DEST_DIR . "/{$name}.phar";
|
||||
@unlink($phar_destination);
|
||||
|
|
@ -63,6 +80,7 @@ foreach (glob(ROOT_DIR . '/plugins/*', GLOB_NOSORT | GLOB_ONLYDIR) as $dir) {
|
|||
unlink($phar_destination);
|
||||
rename("{$phar_destination}.gz", $phar_destination);
|
||||
}
|
||||
*/
|
||||
if (isset($options['sign'])) {
|
||||
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')));
|
||||
|
|
@ -85,6 +103,6 @@ $manifest = str_replace('"}', "\"\n\t}", $manifest);
|
|||
$manifest = str_replace('}]', "}\n]", $manifest);
|
||||
$manifest = str_replace('","', "\",\n\t\t\"", $manifest);
|
||||
$manifest = str_replace('\/', '/', $manifest);
|
||||
file_put_contents(PLUGINS_DEST_DIR . "/packages.json", $manifest);
|
||||
file_put_contents(dirname(PLUGINS_DEST_DIR) . "/packages.json", $manifest);
|
||||
|
||||
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>
|
||||
# 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 )"
|
||||
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"
|
||||
|
||||
cd "${SCRIPT_DIR}"
|
||||
OWNERGROUP=`stat -c "%U:%G" snappymail`
|
||||
OLD_VERSION=`grep "define('APP_VERSION" index.php | awk -F\' '{print $4}'`
|
||||
|
||||
# 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 .
|
||||
# check if we are root
|
||||
if [[ $(id -u) -ne 0 ]] ; then echo -e "\033[1;31mPlease run as root\033[0m" ; exit 1 ; fi
|
||||
|
||||
# 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
|
||||
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}'`
|
||||
|
||||
echo -e "\033[1;33mInstalling $NEW_VERSION\033[0m"
|
||||
tar -xzf /tmp/snappymail_latest.tar.gz
|
||||
if [ $OLD_VERSION != $NEW_VERSION ] ;
|
||||
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
|
||||
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 *
|
||||
# installing
|
||||
echo -e "\033[1;33mInstalling $NEW_VERSION\033[0m";
|
||||
tar -xzf /tmp/snappymail_latest.tar.gz;
|
||||
|
||||
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 { LanguageStore } from 'Stores/Language';
|
||||
import { ThemeStore } from 'Stores/Theme';
|
||||
import { initThemes } from 'Stores/Theme';
|
||||
|
||||
import { SelectComponent } from 'Component/Select';
|
||||
import { CheckboxComponent } from 'Component/Checkbox';
|
||||
|
|
@ -36,9 +36,6 @@ export class AbstractApp {
|
|||
createViewModel: (params, componentInfo) => {
|
||||
params = params || {};
|
||||
i18nToNodes(componentInfo.element);
|
||||
if (params.inline) {
|
||||
componentInfo.element.style.display = 'inline-block';
|
||||
}
|
||||
return new ClassObject(params);
|
||||
}
|
||||
}
|
||||
|
|
@ -49,7 +46,7 @@ export class AbstractApp {
|
|||
initOnStartOrLangChange();
|
||||
|
||||
LanguageStore.populate();
|
||||
ThemeStore.populate();
|
||||
initThemes();
|
||||
|
||||
this.start();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import 'External/ko';
|
||||
|
||||
import { Settings, SettingsGet } from 'Common/Globals';
|
||||
import { ThemeStore } from 'Stores/Theme';
|
||||
import { initThemes } from 'Stores/Theme';
|
||||
|
||||
import Remote from 'Remote/Admin/Fetch';
|
||||
|
||||
|
|
@ -18,7 +18,7 @@ export class AdminApp extends AbstractApp {
|
|||
}
|
||||
|
||||
refresh() {
|
||||
ThemeStore.populate();
|
||||
initThemes();
|
||||
this.start();
|
||||
}
|
||||
|
||||
|
|
@ -27,7 +27,7 @@ export class AdminApp extends AbstractApp {
|
|||
rl.route.root();
|
||||
setTimeout(() => location.href = '/', 1);
|
||||
} else if (SettingsGet('Auth')) {
|
||||
this.weakPassword(SettingsGet('WeakPassword'));
|
||||
this.weakPassword(SettingsGet('weakPassword'));
|
||||
startScreens([SettingsAdminScreen]);
|
||||
} else {
|
||||
startScreens([LoginAdminScreen]);
|
||||
|
|
|
|||
107
dev/App/User.js
107
dev/App/User.js
|
|
@ -1,12 +1,11 @@
|
|||
import 'External/User/ko';
|
||||
|
||||
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 {
|
||||
FolderType,
|
||||
SetSystemFoldersNotification,
|
||||
ClientSideKeyNameFolderListSize
|
||||
} from 'Common/EnumsUser';
|
||||
|
||||
|
|
@ -38,7 +37,7 @@ import { IdentityUserStore } from 'Stores/User/Identity';
|
|||
import { FolderUserStore } from 'Stores/User/Folder';
|
||||
import { PgpUserStore } from 'Stores/User/Pgp';
|
||||
import { MessagelistUserStore } from 'Stores/User/Messagelist';
|
||||
import { ThemeStore } from 'Stores/Theme';
|
||||
import { ThemeStore, initThemes } from 'Stores/Theme';
|
||||
import { LanguageStore } from 'Stores/Language';
|
||||
import { MessageUserStore } from 'Stores/User/Message';
|
||||
|
||||
|
|
@ -62,7 +61,7 @@ import { AskPopupView } from 'View/Popup/Ask';
|
|||
import {
|
||||
folderInformation,
|
||||
folderInformationMultiply,
|
||||
refreshFoldersInterval,
|
||||
setRefreshFoldersInterval,
|
||||
messagesMoveHelper,
|
||||
messagesDeleteHelper
|
||||
} from 'Common/Folders';
|
||||
|
|
@ -80,7 +79,7 @@ export class AppUser extends AbstractApp {
|
|||
(currentTime > (lastTime + interval + 1000))
|
||||
&& Remote.request('Version',
|
||||
iError => (100 < iError) && location.reload(),
|
||||
{ Version: Settings.app('version') }
|
||||
{ version: Settings.app('version') }
|
||||
);
|
||||
lastTime = currentTime;
|
||||
}, interval);
|
||||
|
|
@ -91,6 +90,7 @@ export class AppUser extends AbstractApp {
|
|||
addEventListener('click', dropdownsDetectVisibility);
|
||||
|
||||
this.folderList = FolderUserStore.folderList;
|
||||
this.messageList = MessagelistUserStore;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -101,27 +101,27 @@ export class AppUser extends AbstractApp {
|
|||
*/
|
||||
moveMessagesToFolderType(iFolderType, sFromFolderFullName, oUids, bDelete) {
|
||||
let oMoveFolder = null,
|
||||
nSetSystemFoldersNotification = null;
|
||||
nSetSystemFoldersNotification = 0;
|
||||
|
||||
switch (iFolderType) {
|
||||
case FolderType.Spam:
|
||||
case FolderType.Junk:
|
||||
oMoveFolder = getFolderFromCacheList(FolderUserStore.spamFolder());
|
||||
nSetSystemFoldersNotification = SetSystemFoldersNotification.Spam;
|
||||
nSetSystemFoldersNotification = iFolderType;
|
||||
bDelete = bDelete || UNUSED_OPTION_VALUE === FolderUserStore.spamFolder();
|
||||
break;
|
||||
case FolderType.NotSpam:
|
||||
case FolderType.Inbox:
|
||||
oMoveFolder = getFolderFromCacheList(getFolderInboxName());
|
||||
break;
|
||||
case FolderType.Trash:
|
||||
oMoveFolder = getFolderFromCacheList(FolderUserStore.trashFolder());
|
||||
nSetSystemFoldersNotification = SetSystemFoldersNotification.Trash;
|
||||
nSetSystemFoldersNotification = iFolderType;
|
||||
bDelete = bDelete || UNUSED_OPTION_VALUE === FolderUserStore.trashFolder()
|
||||
|| sFromFolderFullName === FolderUserStore.spamFolder()
|
||||
|| sFromFolderFullName === FolderUserStore.trashFolder();
|
||||
break;
|
||||
case FolderType.Archive:
|
||||
oMoveFolder = getFolderFromCacheList(FolderUserStore.archiveFolder());
|
||||
nSetSystemFoldersNotification = SetSystemFoldersNotification.Archive;
|
||||
nSetSystemFoldersNotification = iFolderType;
|
||||
bDelete = bDelete || UNUSED_OPTION_VALUE === FolderUserStore.archiveFolder();
|
||||
break;
|
||||
// no default
|
||||
|
|
@ -152,41 +152,18 @@ export class AppUser extends AbstractApp {
|
|||
IdentityUserStore.loading(false);
|
||||
|
||||
if (!iError) {
|
||||
const
|
||||
// counts = {},
|
||||
accounts = oData.Result.Accounts,
|
||||
mainEmail = SettingsGet('MainEmail');
|
||||
let items = oData.Result.Accounts;
|
||||
AccountUserStore(isArray(items)
|
||||
? items.map(oValue => new AccountModel(oValue.email, oValue.name))
|
||||
: []
|
||||
);
|
||||
AccountUserStore.unshift(new AccountModel(SettingsGet('mainEmail'), '', false));
|
||||
|
||||
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.accounts.unshift(new AccountModel(mainEmail/*, counts[mainEmail]*/, false));
|
||||
}
|
||||
|
||||
if (isArray(oData.Result.Identities)) {
|
||||
IdentityUserStore(
|
||||
oData.Result.Identities.map(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;
|
||||
})
|
||||
);
|
||||
}
|
||||
items = oData.Result.Identities;
|
||||
IdentityUserStore(isArray(items)
|
||||
? items.map(identityData => IdentityModel.reviveFromJson(identityData))
|
||||
: []
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -200,31 +177,30 @@ export class AppUser extends AbstractApp {
|
|||
}
|
||||
|
||||
logout() {
|
||||
localStorage.removeItem('register_protocol_offered');
|
||||
Remote.request('Logout', () => rl.logoutReload(Settings.app('customLogoutLink')));
|
||||
}
|
||||
|
||||
bootstart() {
|
||||
super.bootstart();
|
||||
|
||||
addEventListener('resize', () => leftPanelDisabled(ThemeStore.isMobile() || 1000 > innerWidth));
|
||||
addEventListener('beforeunload', event => {
|
||||
if (arePopupsVisible() || (ThemeStore.isMobile() && MessageUserStore.message())) {
|
||||
if (arePopupsVisible() || (!SettingsUserStore.usePreviewPane() && MessageUserStore.message())) {
|
||||
event.preventDefault();
|
||||
return event.returnValue = "Are you sure you want to exit?";
|
||||
return event.returnValue = i18n('POPUPS_ASK/EXIT_ARE_YOU_SURE');
|
||||
}
|
||||
}, {capture: true});
|
||||
}
|
||||
|
||||
refresh() {
|
||||
ThemeStore.populate();
|
||||
LanguageStore.language(SettingsGet('Language'));
|
||||
changeTheme(SettingsGet('Theme'));
|
||||
initThemes();
|
||||
LanguageStore.language(SettingsGet('language'));
|
||||
this.start();
|
||||
}
|
||||
|
||||
start() {
|
||||
if (SettingsGet('Auth')) {
|
||||
rl.setWindowTitle(i18n('GLOBAL/LOADING'));
|
||||
rl.setTitle(i18n('GLOBAL/LOADING'));
|
||||
|
||||
SMAudio.notifications(!!SettingsGet('SoundNotification'));
|
||||
NotificationUserStore.enabled(!!SettingsGet('DesktopNotifications'));
|
||||
|
|
@ -242,13 +218,7 @@ export class AppUser extends AbstractApp {
|
|||
SettingsUserScreen
|
||||
]);
|
||||
|
||||
setInterval(() => {
|
||||
const cF = FolderUserStore.currentFolderFullName(),
|
||||
iF = getFolderInboxName();
|
||||
folderInformation(iF);
|
||||
iF === cF || folderInformation(cF);
|
||||
folderInformationMultiply();
|
||||
}, refreshFoldersInterval);
|
||||
setRefreshFoldersInterval(pInt(SettingsGet('CheckMailInterval')));
|
||||
|
||||
ContactUserStore.init();
|
||||
|
||||
|
|
@ -269,30 +239,31 @@ export class AppUser extends AbstractApp {
|
|||
// initLeftSideLayoutResizer
|
||||
setTimeout(() => {
|
||||
const left = elementById('rl-left'),
|
||||
right = elementById('rl-right'),
|
||||
fToggle = () =>
|
||||
setLayoutResizer(left, right, ClientSideKeyNameFolderListSize,
|
||||
setLayoutResizer(left, ClientSideKeyNameFolderListSize,
|
||||
(ThemeStore.isMobile() || leftPanelDisabled()) ? 0 : 'Width');
|
||||
if (left && right) {
|
||||
if (left) {
|
||||
fToggle();
|
||||
leftPanelDisabled.subscribe(fToggle);
|
||||
}
|
||||
}, 1);
|
||||
|
||||
setInterval(reloadTime(), 60000);
|
||||
setInterval(reloadTime, 60000);
|
||||
|
||||
PgpUserStore.init();
|
||||
|
||||
// When auto-login is active
|
||||
try {
|
||||
setTimeout(() => mailToHelper(SettingsGet('mailToEmail')), 500);
|
||||
|
||||
if (!localStorage.getItem('register_protocol_offered')) {
|
||||
// When auto-login is active
|
||||
navigator.registerProtocolHandler?.(
|
||||
'mailto',
|
||||
location.protocol + '//' + location.host + location.pathname + '?mailto&to=%s',
|
||||
(SettingsGet('Title') || 'SnappyMail')
|
||||
(SettingsGet('title') || 'SnappyMail')
|
||||
);
|
||||
} catch (e) {} // eslint-disable-line no-empty
|
||||
localStorage.setItem('register_protocol_offered', '1');
|
||||
}
|
||||
|
||||
setTimeout(() => mailToHelper(SettingsGet('MailToEmail')), 500);
|
||||
} else {
|
||||
this.logout();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,5 @@
|
|||
import { MessageSetAction } from 'Common/EnumsUser';
|
||||
import { isArray } from 'Common/Utils';
|
||||
|
||||
let FOLDERS_CACHE = {},
|
||||
FOLDERS_NAME_CACHE = {},
|
||||
MESSAGE_FLAGS_CACHE = {},
|
||||
let FOLDERS_CACHE = new Map,
|
||||
FOLDERS_HASH_MAP = new Map,
|
||||
inboxFolderName = 'INBOX';
|
||||
|
||||
export const
|
||||
|
|
@ -11,18 +7,10 @@ export const
|
|||
* @returns {void}
|
||||
*/
|
||||
clearCache = () => {
|
||||
FOLDERS_CACHE = {};
|
||||
FOLDERS_NAME_CACHE = {};
|
||||
MESSAGE_FLAGS_CACHE = {};
|
||||
FOLDERS_CACHE.clear();
|
||||
FOLDERS_HASH_MAP.clear();
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {string} folderFullName
|
||||
* @param {string} uid
|
||||
* @returns {string}
|
||||
*/
|
||||
getMessageKey = (folderFullName, uid) => folderFullName + '#' + uid,
|
||||
|
||||
/**
|
||||
* @returns {string}
|
||||
*/
|
||||
|
|
@ -34,143 +22,35 @@ export const
|
|||
setFolderInboxName = name => inboxFolderName = name,
|
||||
|
||||
/**
|
||||
* @param {string} folderHash
|
||||
* @param {string} fullNameHash
|
||||
* @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
|
||||
*/
|
||||
setFolder = folder => {
|
||||
folder.hash = '';
|
||||
FOLDERS_CACHE[folder.fullName] = folder;
|
||||
FOLDERS_NAME_CACHE[folder.fullNameHash] = folder.fullName;
|
||||
folder.etag = '';
|
||||
FOLDERS_CACHE.set(folder.fullName, folder);
|
||||
FOLDERS_HASH_MAP.set(folder.fullNameHash, folder.fullName);
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {string} folderFullName
|
||||
* @param {string} folderHash
|
||||
* @param {string} folderETag
|
||||
*/
|
||||
setFolderHash = (folderFullName, folderHash) =>
|
||||
FOLDERS_CACHE[folderFullName] && (FOLDERS_CACHE[folderFullName].hash = folderHash),
|
||||
setFolderETag = (folderFullName, folderETag) =>
|
||||
FOLDERS_CACHE.has(folderFullName) && (FOLDERS_CACHE.get(folderFullName).etag = folderETag),
|
||||
|
||||
/**
|
||||
* @param {string} folderFullName
|
||||
* @returns {?FolderModel}
|
||||
*/
|
||||
getFolderFromCacheList = folderFullName =>
|
||||
FOLDERS_CACHE[folderFullName] ? FOLDERS_CACHE[folderFullName] : null,
|
||||
FOLDERS_CACHE.get(folderFullName),
|
||||
|
||||
/**
|
||||
* @param {string} folderFullName
|
||||
*/
|
||||
removeFolderFromCacheList = folderFullName => delete FOLDERS_CACHE[folderFullName];
|
||||
|
||||
export class MessageFlagsCache
|
||||
{
|
||||
/**
|
||||
* @param {string} folderFullName
|
||||
* @param {string} uid
|
||||
* @param {string} flag
|
||||
* @returns {bool}
|
||||
*/
|
||||
static hasFlag(folderFullName, uid, flag) {
|
||||
return MESSAGE_FLAGS_CACHE[folderFullName]?.[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;
|
||||
}
|
||||
|
||||
}
|
||||
removeFolderFromCacheList = folderFullName => FOLDERS_CACHE.delete(folderFullName);
|
||||
|
|
|
|||
|
|
@ -5,12 +5,10 @@ export const
|
|||
/**
|
||||
* @enum {string}
|
||||
*/
|
||||
Scope = {
|
||||
MessageList: 'MessageList',
|
||||
FolderList: 'FolderList',
|
||||
MessageView: 'MessageView',
|
||||
Settings: 'Settings'
|
||||
},
|
||||
ScopeMessageList = 'MessageList',
|
||||
ScopeFolderList = 'FolderList',
|
||||
ScopeMessageView = 'MessageView',
|
||||
ScopeSettings = 'Settings',
|
||||
|
||||
/**
|
||||
* @enum {number}
|
||||
|
|
@ -39,7 +37,7 @@ SaveSettingStatus = {
|
|||
/**
|
||||
* @enum {number}
|
||||
*/
|
||||
Notification = {
|
||||
Notifications = {
|
||||
RequestError: 1,
|
||||
RequestAborted: 2,
|
||||
RequestTimeout: 3,
|
||||
|
|
|
|||
|
|
@ -4,20 +4,33 @@
|
|||
* @enum {number}
|
||||
*/
|
||||
export const FolderType = {
|
||||
User: 0,
|
||||
Inbox: 1,
|
||||
Sent: 2,
|
||||
Drafts: 3,
|
||||
Spam: 4, // JUNK
|
||||
Junk: 4, // Spam
|
||||
Trash: 5,
|
||||
Archive: 6,
|
||||
NotSpam: 80
|
||||
};
|
||||
Archive: 6
|
||||
/*
|
||||
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}
|
||||
*/
|
||||
export const FolderMetadataKeys = {
|
||||
FolderMetadataKeys = {
|
||||
// RFC 5464
|
||||
Comment: '/private/comment',
|
||||
CommentShared: '/shared/comment',
|
||||
|
|
@ -26,12 +39,12 @@ export const FolderMetadataKeys = {
|
|||
// Kolab
|
||||
KolabFolderType: '/private/vendor/kolab/folder-type',
|
||||
KolabFolderTypeShared: '/shared/vendor/kolab/folder-type'
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* @enum {string}
|
||||
*/
|
||||
export const ComposeType = {
|
||||
ComposeType = {
|
||||
Empty: 0,
|
||||
Reply: 1,
|
||||
ReplyAll: 2,
|
||||
|
|
@ -39,63 +52,32 @@ export const ComposeType = {
|
|||
ForwardAsAttachment: 4,
|
||||
Draft: 5,
|
||||
EditAsNew: 6
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* @enum {number}
|
||||
*/
|
||||
export const SetSystemFoldersNotification = {
|
||||
None: 0,
|
||||
Sent: 1,
|
||||
Draft: 2,
|
||||
Spam: 3,
|
||||
Trash: 4,
|
||||
Archive: 5
|
||||
};
|
||||
ClientSideKeyNameExpandedFolders = 3,
|
||||
ClientSideKeyNameFolderListSize = 4,
|
||||
ClientSideKeyNameMessageListSize = 5,
|
||||
ClientSideKeyNameLastSignMe = 7,
|
||||
ClientSideKeyNameMessageHeaderFullInfo = 9,
|
||||
ClientSideKeyNameMessageAttachmentControls = 10,
|
||||
|
||||
/**
|
||||
* @enum {number}
|
||||
*/
|
||||
export const
|
||||
ClientSideKeyNameExpandedFolders = 3,
|
||||
ClientSideKeyNameFolderListSize = 4,
|
||||
ClientSideKeyNameMessageListSize = 5,
|
||||
ClientSideKeyNameLastSignMe = 7,
|
||||
ClientSideKeyNameMessageHeaderFullInfo = 9,
|
||||
ClientSideKeyNameMessageAttachmentControls = 10;
|
||||
|
||||
/**
|
||||
* @enum {number}
|
||||
*/
|
||||
export const MessageSetAction = {
|
||||
MessageSetAction = {
|
||||
SetSeen: 0,
|
||||
UnsetSeen: 1,
|
||||
SetFlag: 2,
|
||||
UnsetFlag: 3
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* @enum {number}
|
||||
*/
|
||||
export const MessagePriority = {
|
||||
Low: 5,
|
||||
Normal: 3,
|
||||
High: 1
|
||||
};
|
||||
|
||||
/**
|
||||
* @enum {string}
|
||||
*/
|
||||
export const EditorDefaultType = {
|
||||
Html: 'Html',
|
||||
Plain: 'Plain'
|
||||
};
|
||||
|
||||
/**
|
||||
* @enum {number}
|
||||
*/
|
||||
export const Layout = {
|
||||
NoPreview: 0,
|
||||
SidePreview: 1,
|
||||
BottomPreview: 2
|
||||
};
|
||||
//LayoutNoView = 0,
|
||||
LayoutSideView = 1,
|
||||
LayoutBottomView = 2
|
||||
;
|
||||
|
|
|
|||
|
|
@ -22,7 +22,8 @@ const
|
|||
ics: 'text/calendar',
|
||||
xml: 'text/xml',
|
||||
json: app+'json',
|
||||
asc: app+'pgp-signature',
|
||||
// asc: app+'pgp-signature',
|
||||
// asc: app+'pgp-keys',
|
||||
p10: app+'pkcs10',
|
||||
p7c: app+'pkcs7-mime',
|
||||
p7m: app+'pkcs7-mime',
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import { isArray, arrayLength } from 'Common/Utils';
|
||||
import {
|
||||
MessageFlagsCache,
|
||||
setFolderHash,
|
||||
setFolderETag,
|
||||
getFolderInboxName,
|
||||
getFolderFromCacheList
|
||||
} from 'Common/Cache';
|
||||
|
|
@ -9,13 +8,27 @@ import { SettingsUserStore } from 'Stores/User/Settings';
|
|||
import { FolderUserStore } from 'Stores/User/Folder';
|
||||
import { MessagelistUserStore } from 'Stores/User/Messagelist';
|
||||
import { getNotification } from 'Common/Translator';
|
||||
import { Settings, } from 'Common/Globals';
|
||||
import { serverRequest } from 'Common/Links';
|
||||
|
||||
import Remote from 'Remote/User/Fetch';
|
||||
|
||||
let refreshInterval,
|
||||
// Default every 5 minutes
|
||||
refreshFoldersInterval = 300000;
|
||||
|
||||
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 => {
|
||||
try {
|
||||
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=} aHeaderLines
|
||||
|
|
@ -118,56 +100,59 @@ folderListOptionsBuilder = (
|
|||
return aResult;
|
||||
},
|
||||
|
||||
// Every 5 minutes
|
||||
refreshFoldersInterval = 300000,
|
||||
|
||||
/**
|
||||
* @param {string} folder
|
||||
* @param {Array=} list = []
|
||||
*/
|
||||
folderInformation = (folder, list) => {
|
||||
if (folder?.trim()) {
|
||||
fetchFolderInformation(
|
||||
(iError, data) => {
|
||||
let count = 1;
|
||||
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) {
|
||||
const result = data.Result,
|
||||
folderFromCache = getFolderFromCacheList(result.Folder);
|
||||
folderFromCache = getFolderFromCacheList(result.name);
|
||||
if (folderFromCache) {
|
||||
const oldHash = folderFromCache.hash,
|
||||
const oldHash = folderFromCache.etag,
|
||||
unreadCountChange = (folderFromCache.unreadEmails() !== result.unreadEmails);
|
||||
|
||||
// folderFromCache.revivePropertiesFromJson(result);
|
||||
// folderFromCache.revivePropertiesFromJson(result);
|
||||
folderFromCache.expires = Date.now();
|
||||
folderFromCache.uidNext = result.UidNext;
|
||||
folderFromCache.hash = result.Hash;
|
||||
folderFromCache.uidNext = result.uidNext;
|
||||
folderFromCache.etag = result.etag;
|
||||
folderFromCache.totalEmails(result.totalEmails);
|
||||
folderFromCache.unreadEmails(result.unreadEmails);
|
||||
|
||||
unreadCountChange && MessageFlagsCache.clearFolder(folderFromCache.fullName);
|
||||
MessagelistUserStore.notifyNewMessages(folderFromCache.fullName, result.newMessages);
|
||||
|
||||
if (result.MessagesFlags.length) {
|
||||
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 (!oldHash || unreadCountChange || result.etag !== oldHash) {
|
||||
if (folderFromCache.fullName === FolderUserStore.currentFolderFullName()) {
|
||||
MessagelistUserStore.reload();
|
||||
/*
|
||||
} 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,
|
||||
list
|
||||
);
|
||||
}, {
|
||||
folder: folder,
|
||||
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)) {
|
||||
const utc = Date.now();
|
||||
oData.Result.forEach(item => {
|
||||
const folder = getFolderFromCacheList(item.Folder);
|
||||
|
||||
const folder = getFolderFromCacheList(item.name);
|
||||
if (folder) {
|
||||
const oldHash = folder.hash,
|
||||
const oldHash = folder.etag,
|
||||
unreadCountChange = folder.unreadEmails() !== item.unreadEmails;
|
||||
|
||||
// folder.revivePropertiesFromJson(item);
|
||||
folder.expires = utc;
|
||||
folder.hash = item.Hash;
|
||||
folder.etag = item.etag;
|
||||
folder.totalEmails(item.totalEmails);
|
||||
folder.unreadEmails(item.unreadEmails);
|
||||
|
||||
unreadCountChange && MessageFlagsCache.clearFolder(folder.fullName);
|
||||
|
||||
if (!oldHash || item.Hash !== oldHash) {
|
||||
if (!oldHash || item.etag !== oldHash) {
|
||||
if (folder.fullName === FolderUserStore.currentFolderFullName()) {
|
||||
MessagelistUserStore.reload();
|
||||
}
|
||||
|
|
@ -210,20 +192,20 @@ folderInformationMultiply = (boot = false) => {
|
|||
boot && setTimeout(() => folderInformationMultiply(true), 2000);
|
||||
}
|
||||
}, {
|
||||
Folders: folders
|
||||
folders: folders
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
moveOrDeleteResponseHelper = (iError, oData) => {
|
||||
if (iError) {
|
||||
setFolderHash(FolderUserStore.currentFolderFullName(), '');
|
||||
setFolderETag(FolderUserStore.currentFolderFullName(), '');
|
||||
alert(getNotification(iError));
|
||||
} else if (FolderUserStore.currentFolder()) {
|
||||
if (2 === arrayLength(oData.Result)) {
|
||||
setFolderHash(oData.Result[0], oData.Result[1]);
|
||||
setFolderETag(oData.Result[0], oData.Result[1]);
|
||||
} else {
|
||||
setFolderHash(FolderUserStore.currentFolderFullName(), '');
|
||||
setFolderETag(FolderUserStore.currentFolderFullName(), '');
|
||||
}
|
||||
MessagelistUserStore.reload(!MessagelistUserStore.length);
|
||||
}
|
||||
|
|
@ -235,24 +217,24 @@ messagesMoveHelper = (fromFolderFullName, toFolderFullName, uidsForMove) => {
|
|||
isSpam = sSpamFolder === toFolderFullName,
|
||||
isHam = !isSpam && sSpamFolder === fromFolderFullName && getFolderInboxName() === toFolderFullName;
|
||||
|
||||
Remote.abort('MessageList').request('MessageMove',
|
||||
Remote.abort('MessageList', 'reload').request('MessageMove',
|
||||
moveOrDeleteResponseHelper,
|
||||
{
|
||||
FromFolder: fromFolderFullName,
|
||||
ToFolder: toFolderFullName,
|
||||
Uids: [...uidsForMove].join(','),
|
||||
MarkAsRead: (isSpam || FolderUserStore.trashFolder() === toFolderFullName) ? 1 : 0,
|
||||
Learning: isSpam ? 'SPAM' : isHam ? 'HAM' : ''
|
||||
fromFolder: fromFolderFullName,
|
||||
toFolder: toFolderFullName,
|
||||
uids: [...uidsForMove].join(','),
|
||||
markAsRead: (isSpam || FolderUserStore.trashFolder() === toFolderFullName) ? 1 : 0,
|
||||
learning: isSpam ? 'SPAM' : isHam ? 'HAM' : ''
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
messagesDeleteHelper = (sFromFolderFullName, aUidForRemove) => {
|
||||
Remote.abort('MessageList').request('MessageDelete',
|
||||
Remote.abort('MessageList', 'reload').request('MessageDelete',
|
||||
moveOrDeleteResponseHelper,
|
||||
{
|
||||
Folder: sFromFolderFullName,
|
||||
Uids: [...aUidForRemove].join(',')
|
||||
folder: sFromFolderFullName,
|
||||
uids: [...aUidForRemove].join(',')
|
||||
}
|
||||
);
|
||||
},
|
||||
|
|
@ -271,9 +253,9 @@ moveMessagesToFolder = (sFromFolderFullName, oUids, sToFolderFullName, bCopy) =>
|
|||
if (oFromFolder && oToFolder) {
|
||||
bCopy
|
||||
? Remote.request('MessageCopy', null, {
|
||||
FromFolder: oFromFolder.fullName,
|
||||
ToFolder: oToFolder.fullName,
|
||||
Uids: [...oUids].join(',')
|
||||
fromFolder: oFromFolder.fullName,
|
||||
toFolder: oToFolder.fullName,
|
||||
uids: [...oUids].join(',')
|
||||
})
|
||||
: messagesMoveHelper(oFromFolder.fullName, oToFolder.fullName, oUids);
|
||||
|
||||
|
|
@ -286,28 +268,20 @@ moveMessagesToFolder = (sFromFolderFullName, oUids, sToFolderFullName, bCopy) =>
|
|||
},
|
||||
|
||||
dropFilesInFolder = (sFolderFullName, files) => {
|
||||
let count = 0,
|
||||
fn = () => 0 == --count
|
||||
&& FolderUserStore.currentFolderFullName() == sFolderFullName
|
||||
&& MessagelistUserStore.reload(true, true);
|
||||
let count = files.length;
|
||||
for (const file of files) {
|
||||
if ('message/rfc822' === file.type) {
|
||||
++count;
|
||||
let data = new FormData;
|
||||
data.append('Folder', sFolderFullName);
|
||||
data.append('AppendFile', file);
|
||||
data.XToken = Settings.app('token');
|
||||
fetch(serverRequest('Append'), {
|
||||
method: 'POST',
|
||||
mode: 'same-origin',
|
||||
cache: 'no-cache',
|
||||
redirect: 'error',
|
||||
referrerPolicy: 'no-referrer',
|
||||
credentials: 'same-origin',
|
||||
body: data
|
||||
})
|
||||
.then(fn)
|
||||
.catch(fn);
|
||||
data.append('folder', sFolderFullName);
|
||||
data.append('appendFile', file);
|
||||
Remote.request('FolderAppend', (iError, data)=>{
|
||||
iError && console.error(data.ErrorMessage);
|
||||
0 == --count
|
||||
&& FolderUserStore.currentFolderFullName() == sFolderFullName
|
||||
&& MessagelistUserStore.reload(true, true);
|
||||
}, data);
|
||||
} else {
|
||||
--count;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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
|
||||
export const
|
||||
app = elementById('rl-app'),
|
||||
appFullscreen = () => (doc.fullscreenElement || doc.webkitFullscreenElement) === app,
|
||||
appFullscreen = () => (doc.fullscreenElement || doc.webkitFullscreenElement) === appEl,
|
||||
exitFullscreen = () => appFullscreen() && (doc.exitFullscreen || doc.webkitExitFullscreen).call(doc),
|
||||
isFullscreen = ko.observable(false),
|
||||
toggleFullscreen = () => isFullscreen() ? exitFullscreen() : app.requestFullscreen();
|
||||
toggleFullscreen = () => isFullscreen() ? exitFullscreen() : appEl.requestFullscreen();
|
||||
|
||||
if (app) {
|
||||
if (appEl) {
|
||||
let event = 'fullscreenchange';
|
||||
if (!app.requestFullscreen && app.webkitRequestFullscreen) {
|
||||
app.requestFullscreen = app.webkitRequestFullscreen;
|
||||
if (!appEl.requestFullscreen && appEl.webkitRequestFullscreen) {
|
||||
appEl.requestFullscreen = appEl.webkitRequestFullscreen;
|
||||
event = 'webkit'+event;
|
||||
}
|
||||
if (app.requestFullscreen) {
|
||||
if (appEl.requestFullscreen) {
|
||||
doc.addEventListener(event, () => {
|
||||
isFullscreen(appFullscreen());
|
||||
$htmlCL.toggle('rl-fullscreen', appFullscreen());
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ export const
|
|||
|
||||
elementById = id => doc.getElementById(id),
|
||||
|
||||
appEl = elementById('rl-app'),
|
||||
|
||||
Settings = rl.settings,
|
||||
SettingsGet = Settings.get,
|
||||
SettingsCapa = name => name && !!(SettingsGet('Capa') || {})[name],
|
||||
|
|
@ -18,8 +20,8 @@ export const
|
|||
dropdowns = [],
|
||||
dropdownVisibility = ko.observable(false).extend({ rateLimit: 0 }),
|
||||
|
||||
moveAction = ko.observable(false),
|
||||
leftPanelDisabled = ko.observable(false),
|
||||
toggleLeftPanel = () => leftPanelDisabled(!leftPanelDisabled()),
|
||||
|
||||
createElement = (name, attr) => {
|
||||
let el = doc.createElement(name);
|
||||
|
|
@ -31,6 +33,11 @@ export const
|
|||
new CustomEvent(name, {detail:detail, cancelable: !!cancelable})
|
||||
),
|
||||
|
||||
stopEvent = event => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
},
|
||||
|
||||
formFieldFocused = () => doc.activeElement?.matches('input,textarea'),
|
||||
|
||||
addShortcut = (...args) => shortcuts.add(...args),
|
||||
|
|
@ -68,10 +75,4 @@ dropdownVisibility.subscribe(value => {
|
|||
}
|
||||
});
|
||||
|
||||
leftPanelDisabled.toggle = () => leftPanelDisabled(!leftPanelDisabled());
|
||||
leftPanelDisabled.subscribe(value => {
|
||||
value && moveAction() && moveAction(false);
|
||||
$htmlCL.toggle('rl-left-panel-disabled', value);
|
||||
});
|
||||
|
||||
moveAction.subscribe(value => value && leftPanelDisabled() && leftPanelDisabled(false));
|
||||
leftPanelDisabled.subscribe(value => $htmlCL.toggle('rl-left-panel-disabled', value));
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { forEachObjectEntry, pInt } from 'Common/Utils';
|
|||
import { SettingsUserStore } from 'Stores/User/Settings';
|
||||
|
||||
const
|
||||
tpl = createElement('template'),
|
||||
tmpl = createElement('template'),
|
||||
htmlre = /[&<>"']/g,
|
||||
httpre = /^(https?:)?\/\//i,
|
||||
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]),
|
||||
|
||||
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
|
||||
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
|
||||
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
|
||||
/** 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
|
||||
*/
|
||||
// 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,
|
||||
base64Url = data => atob(data.replace(/_/g,'/').replace(/-/g,'+')),
|
||||
decode = decodeURIComponent,
|
||||
stripTracking = url => {
|
||||
try {
|
||||
let nurl = url
|
||||
// Copernica
|
||||
.replace(/^.+\/(https%253A[^/?&]+).*$/i, (...m) => decode(decode(m[1])))
|
||||
.replace(/tracking\.(printabout\.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,
|
||||
() => urlGetParam(url, 'url'))
|
||||
.replace(/^.+click\.godaddy\.com.+$/i, () => urlGetParam(url, 'redir'))
|
||||
.replace(/^.+delivery-status\.com.+$/i, () => urlGetParam(url, 'fb'))
|
||||
.replace(/^.+go\.dhlparcel\.nl.+\/([A-Za-z0-9_-]+)$/i, (...m) => base64Url(m[1]))
|
||||
.replace(/^(.+mopinion\.com.+)\?.*$/i, (...m) => m[1])
|
||||
|
|
@ -73,6 +96,93 @@ const
|
|||
});
|
||||
}
|
||||
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
|
||||
|
|
@ -88,28 +198,27 @@ export const
|
|||
* @param {string} text
|
||||
* @returns {string}
|
||||
*/
|
||||
cleanHtml = (html, oAttachments) => {
|
||||
cleanHtml = (html, oAttachments, msgId) => {
|
||||
let aColor;
|
||||
const
|
||||
debug = false, // Config()->Get('debug', 'enable', false);
|
||||
detectHiddenImages = true, // !!SettingsGet('try_to_detect_hidden_images'),
|
||||
|
||||
bqLevel = parseInt(SettingsUserStore.maxBlockquotesLevel()),
|
||||
|
||||
result = {
|
||||
hasExternals: false
|
||||
},
|
||||
|
||||
findAttachmentByCid = cid => oAttachments.findByCid(cid),
|
||||
findLocationByCid = cid => {
|
||||
const attachment = findAttachmentByCid(cid);
|
||||
findAttachmentByCid = cId => oAttachments.findByCid(cId),
|
||||
findLocationByCid = cId => {
|
||||
const attachment = findAttachmentByCid(cId);
|
||||
return attachment?.contentLocation ? attachment : 0;
|
||||
},
|
||||
|
||||
// convert body attributes to CSS
|
||||
tasks = {
|
||||
link: value => {
|
||||
if (/^#[a-fA-Z0-9]{3,6}$/.test(value)) {
|
||||
tpl.content.querySelectorAll('a').forEach(node => node.style.color || (node.style.color = value))
|
||||
}
|
||||
},
|
||||
link: value => aColor = value,
|
||||
text: (value, node) => node.style.color = value,
|
||||
topmargin: (value, node) => node.style.marginTop = pInt(value) + 'px',
|
||||
leftmargin: (value, node) => node.style.marginLeft = pInt(value) + 'px',
|
||||
|
|
@ -145,41 +254,65 @@ export const
|
|||
// td
|
||||
'colspan', 'rowspan', 'headers'
|
||||
],
|
||||
disallowedTags = [
|
||||
'HEAD','STYLE','SVG','SCRIPT','TITLE','LINK','BASE','META',
|
||||
'INPUT','OUTPUT','SELECT','BUTTON','TEXTAREA',
|
||||
'BGSOUND','KEYGEN','SOURCE','OBJECT','EMBED','APPLET','IFRAME','FRAME','FRAMESET','VIDEO','AUDIO','AREA','MAP'
|
||||
],
|
||||
nonEmptyTags = [
|
||||
'A','B','EM','I','SPAN','STRONG'
|
||||
];
|
||||
|
||||
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(/<!doctype[^>]*>/gi, '')
|
||||
.replace(/<\?xml[^>]*\?>/gi, '')
|
||||
// Not supported by <template> element
|
||||
// .replace(/<!doctype[^>]*>/gi, '')
|
||||
// .replace(/<\?xml[^>]*\?>/gi, '')
|
||||
.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
|
||||
.replace(/<span class="preview-text"[\s\S]+?<\/span>/, '')
|
||||
// https://github.com/the-djmaze/snappymail/issues/900
|
||||
.replace(/\u2028/g,' ')
|
||||
.trim();
|
||||
html = '';
|
||||
|
||||
// \MailSo\Base\HtmlUtils::ClearComments()
|
||||
// https://github.com/the-djmaze/snappymail/issues/187
|
||||
const nodeIterator = document.createNodeIterator(tpl.content, NodeFilter.SHOW_COMMENT);
|
||||
// Strip all comments
|
||||
const nodeIterator = document.createNodeIterator(tmpl.content, NodeFilter.SHOW_COMMENT);
|
||||
while (nodeIterator.nextNode()) {
|
||||
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,
|
||||
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()
|
||||
if (disallowedTags.includes(name)
|
||||
|| 'none' == oStyle.display
|
||||
if ('none' == oStyle.display
|
||||
|| 'hidden' == oStyle.visibility
|
||||
// || (oStyle.lineHeight && 1 > parseFloat(oStyle.lineHeight)
|
||||
// || (oStyle.maxHeight && 1 > parseFloat(oStyle.maxHeight)
|
||||
|
|
@ -189,29 +322,24 @@ export const
|
|||
oElement.remove();
|
||||
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 = [],
|
||||
className = oElement.className,
|
||||
hasAttribute = name => oElement.hasAttribute(name),
|
||||
getAttribute = name => hasAttribute(name) ? oElement.getAttribute(name).trim() : '',
|
||||
setAttribute = (name, value) => oElement.setAttribute(name, value),
|
||||
delAttribute = name => oElement.removeAttribute(name);
|
||||
delAttribute = name => {
|
||||
let value = getAttribute(name);
|
||||
oElement.removeAttribute(name);
|
||||
return value;
|
||||
};
|
||||
|
||||
if ('mail-body' === oElement.className) {
|
||||
forEachObjectEntry(tasks, (name, cb) => {
|
||||
if (hasAttribute(name)) {
|
||||
cb(getAttribute(name), oElement);
|
||||
delAttribute(name);
|
||||
}
|
||||
});
|
||||
if ('mail-body' === className) {
|
||||
forEachObjectEntry(tasks, (name, cb) =>
|
||||
hasAttribute(name) && cb(delAttribute(name), oElement)
|
||||
);
|
||||
} else if (msgId && className) {
|
||||
oElement.className = className.replace(/(^|\s+)/g, '$1msg-');
|
||||
}
|
||||
|
||||
if (oElement.hasAttributes()) {
|
||||
|
|
@ -229,32 +357,24 @@ export const
|
|||
|
||||
// if ('TABLE' === name || 'TD' === name || 'TH' === name) {
|
||||
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';
|
||||
if ('TD' !== name && 'TH' !== name) {
|
||||
['width','height'].forEach(key => {
|
||||
if (hasAttribute(key)) {
|
||||
value = delAttribute(key);
|
||||
oStyle[key] || (oStyle[key] = value.includes('%') ? value : value + 'px');
|
||||
}
|
||||
}
|
||||
delAttribute('width');
|
||||
}
|
||||
if (value && !value.includes('%')) {
|
||||
oStyle.maxWidth = value;
|
||||
if ('TD' !== name && 'TH' !== name) {
|
||||
});
|
||||
// Make width responsive
|
||||
value = oStyle.width;
|
||||
if (100 < parseInt(value,10) && !oStyle.maxWidth) {
|
||||
oStyle.maxWidth = value;
|
||||
oStyle.width = '100%';
|
||||
}
|
||||
}
|
||||
if (hasAttribute('height')) {
|
||||
value = getAttribute('height');
|
||||
if (pInt(value)) {
|
||||
oStyle.height = value.includes('%') ? value : value + 'px';
|
||||
// Make height responsive
|
||||
value = oStyle.removeProperty('height');
|
||||
if (value && !oStyle.maxHeight) {
|
||||
oStyle.maxHeight = value;
|
||||
}
|
||||
delAttribute('height');
|
||||
}
|
||||
value = oStyle.removeProperty('height');
|
||||
if (value && !value.includes('%')) {
|
||||
oStyle.maxHeight = value;
|
||||
}
|
||||
}
|
||||
// } else
|
||||
|
|
@ -269,10 +389,11 @@ export const
|
|||
// setAttribute('rel', 'external nofollow noopener noreferrer');
|
||||
}
|
||||
setAttribute('tabindex', '-1');
|
||||
aColor && !oElement.style.color && (oElement.style.color = aColor);
|
||||
}
|
||||
|
||||
// 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);
|
||||
return;
|
||||
}
|
||||
|
|
@ -286,8 +407,7 @@ export const
|
|||
|
||||
let skipStyle = false;
|
||||
if (hasAttribute('src')) {
|
||||
value = stripTracking(getAttribute('src'));
|
||||
delAttribute('src');
|
||||
value = stripTracking(delAttribute('src'));
|
||||
|
||||
if ('IMG' === name) {
|
||||
oElement.loading = 'lazy';
|
||||
|
|
@ -348,18 +468,15 @@ export const
|
|||
}
|
||||
|
||||
if (hasAttribute('background')) {
|
||||
oStyle.backgroundImage = 'url("' + getAttribute('background') + '")';
|
||||
delAttribute('background');
|
||||
oStyle.backgroundImage = 'url("' + delAttribute('background') + '")';
|
||||
}
|
||||
|
||||
if (hasAttribute('bgcolor')) {
|
||||
oStyle.backgroundColor = getAttribute('bgcolor');
|
||||
delAttribute('bgcolor');
|
||||
oStyle.backgroundColor = delAttribute('bgcolor');
|
||||
}
|
||||
|
||||
if (hasAttribute('color')) {
|
||||
oStyle.color = getAttribute('color');
|
||||
delAttribute('color');
|
||||
oStyle.color = delAttribute('color');
|
||||
}
|
||||
|
||||
if (!skipStyle) {
|
||||
|
|
@ -410,11 +527,12 @@ export const
|
|||
if (urls_broken.length) {
|
||||
setAttribute('data-x-style-broken-urls', JSON.stringify(urls_broken));
|
||||
}
|
||||
|
||||
/*
|
||||
// https://github.com/the-djmaze/snappymail/issues/1082
|
||||
if (11 > pInt(oStyle.fontSize)) {
|
||||
oStyle.removeProperty('font-size');
|
||||
}
|
||||
|
||||
*/
|
||||
// Removes background and color
|
||||
// Many e-mails incorrectly only define one, not both
|
||||
// And in dark theme mode this kills the readability
|
||||
|
|
@ -424,8 +542,7 @@ export const
|
|||
oStyle.removeProperty('color');
|
||||
}
|
||||
|
||||
// Drop Microsoft Office style properties
|
||||
// oStyle.cssText = oStyle.cssText.replace(/mso-[^:;]+:[^;]+/gi, '');
|
||||
oStyle.cssText = cleanCSS(oStyle.cssText);
|
||||
}
|
||||
|
||||
if (debug && aAttrsForRemove.length) {
|
||||
|
|
@ -433,8 +550,10 @@ export const
|
|||
}
|
||||
});
|
||||
|
||||
// return tpl.content.firstChild;
|
||||
result.html = tpl.innerHTML.trim();
|
||||
blockquoteSwitcher();
|
||||
|
||||
// return tmpl.content.firstChild;
|
||||
result.html = tmpl.innerHTML.trim();
|
||||
return result;
|
||||
},
|
||||
|
||||
|
|
@ -445,7 +564,7 @@ export const
|
|||
htmlToPlain = html => {
|
||||
const
|
||||
hr = '⎯'.repeat(64),
|
||||
forEach = (selector, fn) => tpl.content.querySelectorAll(selector).forEach(fn),
|
||||
forEach = (selector, fn) => tmpl.content.querySelectorAll(selector).forEach(fn),
|
||||
blockquotes = node => {
|
||||
let bq;
|
||||
while ((bq = node.querySelector('blockquote'))) {
|
||||
|
|
@ -473,7 +592,7 @@ export const
|
|||
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(/<\/tr(\s[\s\S]*?)?>/gi, '\n');
|
||||
|
||||
|
|
@ -523,16 +642,16 @@ export const
|
|||
forEach('i,em', i => i.replaceWith(`*${i.textContent}*`));
|
||||
|
||||
// Convert line-breaks
|
||||
tpl.innerHTML = tpl.innerHTML
|
||||
tmpl.innerHTML = tmpl.innerHTML
|
||||
.replace(/\n{3,}/gm, '\n\n')
|
||||
.replace(/\n<br[^>]*>/g, '\n')
|
||||
.replace(/<br[^>]*>\n/g, '\n');
|
||||
forEach('br', br => br.replaceWith('\n'));
|
||||
|
||||
// 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 => {
|
||||
plain = plain.toString()
|
||||
.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,
|
||||
bDo = true,
|
||||
|
|
@ -584,21 +705,40 @@ export const
|
|||
aText = aNextText;
|
||||
} while (bDo);
|
||||
|
||||
return aText.join('\n')
|
||||
tmpl.innerHTML = aText.join('\n')
|
||||
// .replace(/~~~\/blockquote~~~\n~~~blockquote~~~/g, '\n')
|
||||
.replace(/&/g, '&')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/</g, '<')
|
||||
.replace(url, (...m) => {
|
||||
m[2] = stripTracking(m[2]);
|
||||
return `${m[1]}<a href="${m[2]}" target="_blank">${m[2]}</a>`;
|
||||
.replace(urlRegExp, (...m) => {
|
||||
m[0] = stripTracking(m[0]);
|
||||
return `<a href="${m[0]}" target="_blank">${m[0]}</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(/\s*~~~\/blockquote~~~/g, '</blockquote>')
|
||||
.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 {
|
||||
/**
|
||||
|
|
@ -614,29 +754,22 @@ export class HtmlEditor {
|
|||
this.onModeChange = onModeChange;
|
||||
|
||||
if (element) {
|
||||
let editor;
|
||||
|
||||
onReady = onReady ? [onReady] : [];
|
||||
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;
|
||||
editor.on('blur', () => this.blurTrigger());
|
||||
editor.on('focus', () => clearTimeout(this.blurTimer));
|
||||
editor.on('mode', () => {
|
||||
this.blurTrigger();
|
||||
this.onModeChange?.(!this.isPlain());
|
||||
});
|
||||
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('focus', () => this.blurTimer && clearTimeout(this.blurTimer));
|
||||
editor.on('mode', () => {
|
||||
this.blurTrigger();
|
||||
this.onModeChange?.(!this.isPlain());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
import { pInt } from 'Common/Utils';
|
||||
import { Settings } from 'Common/Globals';
|
||||
import { doc, Settings } from 'Common/Globals';
|
||||
|
||||
const
|
||||
BASE = doc.location.pathname.replace(/\/+$/,'') + '/',
|
||||
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
|
||||
SUB_QUERY_PREFIX = '&q[]=',
|
||||
|
|
@ -21,7 +21,7 @@ export const
|
|||
/**
|
||||
* @returns {string}
|
||||
*/
|
||||
logoutLink = () => adminPath() ? prefix() : './',
|
||||
logoutLink = () => adminPath() ? prefix() : BASE,
|
||||
|
||||
/**
|
||||
* @param {string} type
|
||||
|
|
@ -30,8 +30,8 @@ export const
|
|||
* @returns {string}
|
||||
*/
|
||||
serverRequestRaw = (type, hash) =>
|
||||
SERVER_PREFIX + '/Raw/' + SUB_QUERY_PREFIX + '/'
|
||||
+ '0/' // Settings.get('AccountHash') ?
|
||||
BASE + '?/Raw/' + SUB_QUERY_PREFIX + '/'
|
||||
+ '0/' // Settings.get('accountHash') ?
|
||||
+ (type
|
||||
? type + '/' + (hash ? SUB_QUERY_PREFIX + '/' + hash : '')
|
||||
: ''),
|
||||
|
|
@ -45,8 +45,9 @@ export const
|
|||
serverRequestRaw('Download', download, customSpecSuffix),
|
||||
|
||||
proxy = url =>
|
||||
SERVER_PREFIX + '/ProxyExternal/'
|
||||
BASE + '?/ProxyExternal/'
|
||||
+ btoa(url.replace(/ /g, '%20')).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''),
|
||||
// + b64EncodeJSONSafe(url.replace(/ /g, '%20')),
|
||||
|
||||
/**
|
||||
* @param {string} type
|
||||
|
|
@ -54,13 +55,16 @@ export const
|
|||
*/
|
||||
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 {boolean} isAdmin
|
||||
* @returns {string}
|
||||
*/
|
||||
langLink = (lang, isAdmin) =>
|
||||
SERVER_PREFIX + '/Lang/0/' + (isAdmin ? 'Admin' : 'App')
|
||||
BASE + '?/Lang/0/' + (isAdmin ? 'Admin' : 'App')
|
||||
+ '/' + encodeURI(lang)
|
||||
+ '/' + Settings.app('version') + '/',
|
||||
|
||||
|
|
@ -75,6 +79,10 @@ export const
|
|||
* @returns {string}
|
||||
*/
|
||||
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';
|
||||
if (theme.endsWith('@custom')) {
|
||||
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.
|
||||
*/
|
||||
|
||||
let aCache = [],
|
||||
aCheckedCache = [],
|
||||
let aCheckedCache = [],
|
||||
mFocused = null,
|
||||
mSelected = null;
|
||||
|
||||
|
|
@ -99,7 +98,6 @@ export class Selector {
|
|||
items.forEach(item => {
|
||||
const uid = this.getItemUid(item);
|
||||
if (uid) {
|
||||
aCache.push(uid);
|
||||
item.checked() && aCheckedCache.push(uid);
|
||||
if (!mFocused && item.focused()) {
|
||||
mFocused = uid;
|
||||
|
|
@ -118,8 +116,7 @@ export class Selector {
|
|||
koList.subscribe(aItems => {
|
||||
selectedItemUseCallback = false;
|
||||
|
||||
koFocusedItem(null);
|
||||
koSelectedItem(null);
|
||||
this.unselect();
|
||||
|
||||
if (isArray(aItems)) {
|
||||
let temp,
|
||||
|
|
@ -182,7 +179,6 @@ export class Selector {
|
|||
}
|
||||
}
|
||||
|
||||
aCache = [];
|
||||
aCheckedCache = [];
|
||||
mFocused = null;
|
||||
mSelected = null;
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
import ko from 'ko';
|
||||
import { Notification, UploadErrorCode } from 'Common/Enums';
|
||||
import { Notifications, UploadErrorCode } from 'Common/Enums';
|
||||
import { langLink } from 'Common/Links';
|
||||
import { doc, createElement } from 'Common/Globals';
|
||||
import { getKeyByValue, forEachObjectEntry } from 'Common/Utils';
|
||||
import { pInt } from 'Common/Utils';
|
||||
import { LanguageStore } from 'Stores/Language';
|
||||
|
||||
let I18N_DATA = {};
|
||||
|
||||
|
|
@ -11,8 +12,8 @@ const
|
|||
init = () => {
|
||||
if (rl.I18N) {
|
||||
I18N_DATA = rl.I18N;
|
||||
Date.defineRelativeTimeFormat(rl.relativeTime || {});
|
||||
rl.I18N = null;
|
||||
doc.documentElement.dir = I18N_DATA.LANG_DIR;
|
||||
return 1;
|
||||
}
|
||||
},
|
||||
|
|
@ -20,13 +21,40 @@ const
|
|||
i18nKey = key => key.replace(/([a-z])([A-Z])/g, '$1_$2').toUpperCase(),
|
||||
|
||||
getNotificationMessage = code => {
|
||||
let key = getKeyByValue(Notification, code);
|
||||
let key = getKeyByValue(Notifications, code);
|
||||
return key ? I18N_DATA.NOTIFICATIONS[i18nKey(key).replace('_NOTIFICATION', '_ERROR')] : '';
|
||||
};
|
||||
},
|
||||
|
||||
fromNow = date => relativeTime(Math.round((date.getTime() - Date.now()) / 1000));
|
||||
|
||||
export const
|
||||
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 {Object=} valueList
|
||||
|
|
@ -77,27 +105,31 @@ export const
|
|||
time = 0 < timeStampInUTC ? Math.min(now, timeStampInUTC * 1000) : (0 === timeStampInUTC ? now : 0);
|
||||
|
||||
if (31536000000 < time) {
|
||||
const m = new Date(time);
|
||||
const m = new Date(time), h = LanguageStore.hourCycle();
|
||||
switch (formatStr) {
|
||||
case 'FROMNOW':
|
||||
return m.fromNow();
|
||||
case 'SHORT': {
|
||||
if (4 >= (now - time) / 3600000)
|
||||
return m.fromNow();
|
||||
const mt = m.getTime(), date = new Date,
|
||||
return fromNow(m);
|
||||
case 'AUTO': {
|
||||
// 4 hours
|
||||
if (14400000 >= now - time)
|
||||
return fromNow(m);
|
||||
const date = new Date,
|
||||
dt = date.setHours(0,0,0,0);
|
||||
if (mt > dt)
|
||||
return i18n('MESSAGE_LIST/TODAY_AT', {TIME: m.format('LT')});
|
||||
if (mt > dt - 86400000)
|
||||
return i18n('MESSAGE_LIST/YESTERDAY_AT', {TIME: m.format('LT')});
|
||||
if (date.getFullYear() === m.getFullYear())
|
||||
return m.format('d M');
|
||||
return m.format('LL');
|
||||
return (time > dt - 86400000)
|
||||
? i18n(
|
||||
time > dt ? 'MESSAGE_LIST/TODAY_AT' : 'MESSAGE_LIST/YESTERDAY_AT',
|
||||
{TIME: m.format('LT',0,h)}
|
||||
)
|
||||
: m.format(
|
||||
date.getFullYear() === m.getFullYear()
|
||||
? {day: '2-digit', month: 'short', hour: 'numeric', minute: 'numeric'}
|
||||
: {dateStyle: 'medium', timeStyle: 'short'}
|
||||
, 0, h);
|
||||
}
|
||||
case 'FULL':
|
||||
return m.format('LLL');
|
||||
return m.format('LLL',0,h);
|
||||
default:
|
||||
return m.format(formatStr);
|
||||
return m.format(formatStr,0,h);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -107,7 +139,7 @@ export const
|
|||
timeToNode = (element, time) => {
|
||||
try {
|
||||
if (time) {
|
||||
element.dateTime = (new Date(time * 1000)).format('Y-m-d\\TH:i:s');
|
||||
element.dateTime = new Date(time * 1000).toISOString();
|
||||
} else {
|
||||
time = Date.parse(element.dateTime) / 1000;
|
||||
}
|
||||
|
|
@ -115,10 +147,9 @@ export const
|
|||
let key = element.dataset.momentFormat;
|
||||
if (key) {
|
||||
element.textContent = timestampToString(time, key);
|
||||
}
|
||||
|
||||
if ((key = element.dataset.momentFormatTitle)) {
|
||||
element.title = timestampToString(time, key);
|
||||
if ('FULL' !== key && 'FROMNOW' !== key) {
|
||||
element.title = timestampToString(time, 'FULL');
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// prevent knockout crashes
|
||||
|
|
@ -126,9 +157,7 @@ export const
|
|||
}
|
||||
},
|
||||
|
||||
reloadTime = () => setTimeout(() =>
|
||||
doc.querySelectorAll('time').forEach(element => timeToNode(element))
|
||||
, 1),
|
||||
reloadTime = () => doc.querySelectorAll('time').forEach(element => timeToNode(element)),
|
||||
|
||||
/**
|
||||
* @param {Function} startCallback
|
||||
|
|
@ -148,7 +177,7 @@ export const
|
|||
*/
|
||||
getNotification = (code, message = '', defCode = 0) => {
|
||||
code = pInt(code);
|
||||
if (Notification.ClientViewError === code && message) {
|
||||
if (Notifications.ClientViewError === code && message) {
|
||||
return message;
|
||||
}
|
||||
|
||||
|
|
@ -170,15 +199,15 @@ export const
|
|||
* @param {boolean} admin
|
||||
* @param {string} language
|
||||
*/
|
||||
translatorReload = (admin, language) =>
|
||||
translatorReload = (language, admin) =>
|
||||
new Promise((resolve, reject) => {
|
||||
const script = createElement('script');
|
||||
script.onload = () => {
|
||||
// reload the data
|
||||
if (init()) {
|
||||
i18nToNodes(doc);
|
||||
admin || reloadTime();
|
||||
translateTrigger(!translateTrigger());
|
||||
// admin || reloadTime();
|
||||
}
|
||||
script.remove();
|
||||
resolve();
|
||||
|
|
|
|||
|
|
@ -1,9 +1,3 @@
|
|||
import { SaveSettingStatus } from 'Common/Enums';
|
||||
import { elementById } from 'Common/Globals';
|
||||
|
||||
let __themeTimer = 0,
|
||||
__themeJson = null;
|
||||
|
||||
export const
|
||||
isArray = Array.isArray,
|
||||
arrayLength = array => isArray(array) && array.length,
|
||||
|
|
@ -19,52 +13,19 @@ export const
|
|||
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) =>
|
||||
item && undefined !== item.disabled && domItem?.classList.toggle('disabled', domItem.disabled = item.disabled),
|
||||
|
||||
// unescape(encodeURIComponent()) makes the UTF-16 DOMString to an UTF-8 string
|
||||
b64EncodeJSON = data => btoa(unescape(encodeURIComponent(JSON.stringify(data)))),
|
||||
b64Encode = data => btoa(unescape(encodeURIComponent(data))),
|
||||
/* // 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)
|
||||
)),
|
||||
*/
|
||||
|
||||
b64EncodeJSON = data => b64Encode(JSON.stringify(data)),
|
||||
|
||||
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);
|
||||
|
|
|
|||
|
|
@ -1,20 +1,21 @@
|
|||
import { MessageFlagsCache } from 'Common/Cache';
|
||||
import { Notification } from 'Common/Enums';
|
||||
import { Notifications } from 'Common/Enums';
|
||||
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 { getNotification } from 'Common/Translator';
|
||||
import { EmailModel } from 'Model/Email';
|
||||
import { MessageModel } from 'Model/Message';
|
||||
import { EmailCollectionModel } from 'Model/EmailCollection';
|
||||
import { MessageUserStore } from 'Stores/User/Message';
|
||||
import { MessagelistUserStore } from 'Stores/User/Messagelist';
|
||||
import { SettingsUserStore } from 'Stores/User/Settings';
|
||||
import * as Local from 'Storage/Client';
|
||||
import { ThemeStore } from 'Stores/Theme';
|
||||
import Remote from 'Remote/User/Fetch';
|
||||
import { attachmentDownload } from 'Common/Links';
|
||||
|
||||
export const
|
||||
|
||||
moveAction = ko.observable(false),
|
||||
|
||||
dropdownsDetectVisibility = (() =>
|
||||
dropdownVisibility(!!dropdowns.find(item => item.classList.contains('show')))
|
||||
).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}
|
||||
*/
|
||||
|
|
@ -48,14 +72,15 @@ computedPaginatorHelper = (koCurrentPage, koPageCount) => {
|
|||
const currentPage = koCurrentPage(),
|
||||
pageCount = koPageCount(),
|
||||
result = [],
|
||||
lang = doc.documentElement.lang,
|
||||
fAdd = (index, push = true, customName = '') => {
|
||||
const data = {
|
||||
current: index === currentPage,
|
||||
name: customName ? customName.toString() : index.toString(),
|
||||
custom: !!customName,
|
||||
title: customName ? index.toString() : '',
|
||||
value: index.toString()
|
||||
};
|
||||
const name = index.toLocaleString(lang),
|
||||
data = {
|
||||
current: index === currentPage,
|
||||
name: customName || name,
|
||||
title: customName ? name : '',
|
||||
value: index
|
||||
};
|
||||
|
||||
push ? result.push(data) : result.unshift(data);
|
||||
};
|
||||
|
|
@ -131,29 +156,15 @@ mailToHelper = mailToUrl => {
|
|||
mailToUrl = mailToUrl.slice(7).split('?');
|
||||
|
||||
const
|
||||
email = mailToUrl[0],
|
||||
email = decodeURIComponent(mailToUrl[0]),
|
||||
params = new URLSearchParams(mailToUrl[1]),
|
||||
toEmailModel = value => null != value ? EmailModel.parseEmailLine(value) : null;
|
||||
to = params.get('to'),
|
||||
toEmailModel = value => EmailCollectionModel.fromString(value);
|
||||
|
||||
showMessageComposer([
|
||||
ComposeType.Empty,
|
||||
null,
|
||||
params.get('to')
|
||||
? Object.values(
|
||||
toEmailModel(email + ',' + params.get('to')).reduce((result, value) => {
|
||||
if (value) {
|
||||
if (result[value.email]) {
|
||||
if (!result[value.email].name) {
|
||||
result[value.email] = value;
|
||||
}
|
||||
} else {
|
||||
result[value.email] = value;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}, {})
|
||||
)
|
||||
: EmailModel.parseEmailLine(email),
|
||||
toEmailModel(to ? email + ',' + to : email),
|
||||
toEmailModel(params.get('cc')),
|
||||
toEmailModel(params.get('bcc')),
|
||||
params.get('subject'),
|
||||
|
|
@ -171,37 +182,24 @@ showMessageComposer = (params = []) =>
|
|||
rl.app.showMessageComposer(params);
|
||||
},
|
||||
|
||||
setLayoutResizer = (source, target, sClientSideKeyName, mode) =>
|
||||
setLayoutResizer = (source, sClientSideKeyName, mode) =>
|
||||
{
|
||||
if (source.layoutResizer && source.layoutResizer.mode != mode) {
|
||||
target.removeAttribute('style');
|
||||
source.removeAttribute('style');
|
||||
}
|
||||
source.observer?.disconnect();
|
||||
// source.classList.toggle('resizable', mode);
|
||||
if (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;
|
||||
};
|
||||
const length = Local.get(sClientSideKeyName + mode) || SettingsGet('Resizer' + sClientSideKeyName + mode);
|
||||
if (length) {
|
||||
source.style[mode.toLowerCase()] = length + 'px';
|
||||
setTargetPos(mode);
|
||||
}
|
||||
if (!source.layoutResizer) {
|
||||
const resizer = createElement('div', {'class':'resizer'}),
|
||||
save = (data => Remote.saveSettings(0, data)).debounce(500),
|
||||
size = {},
|
||||
store = () => {
|
||||
const value = setTargetPos(resizer.mode),
|
||||
const value = ('Width' == resizer.mode) ? source.offsetWidth : source.offsetHeight,
|
||||
prop = resizer.key + resizer.mode;
|
||||
(value == Local.get(prop)) || Local.set(prop, value);
|
||||
(value == SettingsGet('Resizer' + prop)) || save({['Resizer' + prop]: value});
|
||||
|
|
@ -248,111 +246,80 @@ setLayoutResizer = (source, target, sClientSideKeyName, mode) =>
|
|||
}
|
||||
},
|
||||
|
||||
viewMessage = (oMessage, popup) => {
|
||||
if (popup) {
|
||||
oMessage.viewPopupMessage();
|
||||
} else {
|
||||
MessageUserStore.error('');
|
||||
let id = 'rl-msg-' + oMessage.hash,
|
||||
body = oMessage.body || elementById(id);
|
||||
if (!body) {
|
||||
body = createElement('div',{
|
||||
id:id,
|
||||
hidden:'',
|
||||
class:'b-text-part'
|
||||
+ (oMessage.pgpSigned() ? ' openpgp-signed' : '')
|
||||
+ (oMessage.pgpEncrypted() ? ' openpgp-encrypted' : '')
|
||||
});
|
||||
MessageUserStore.purgeCache();
|
||||
}
|
||||
|
||||
body.message = oMessage;
|
||||
oMessage.body = body;
|
||||
|
||||
if (!SettingsUserStore.viewHTML() || !oMessage.viewHtml()) {
|
||||
oMessage.viewPlain();
|
||||
}
|
||||
|
||||
MessageUserStore.bodiesDom().append(body);
|
||||
|
||||
MessageUserStore.loading(false);
|
||||
oMessage.body.hidden = false;
|
||||
|
||||
if (oMessage.isUnseen()) {
|
||||
MessageUserStore.MessageSeenTimer = setTimeout(
|
||||
() => MessagelistUserStore.setAction(oMessage.folder, MessageSetAction.SetSeen, [oMessage]),
|
||||
SettingsUserStore.messageReadDelay() * 1000 // seconds
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
populateMessageBody = (oMessage, popup) => {
|
||||
if (oMessage) {
|
||||
popup || MessageUserStore.message(oMessage);
|
||||
popup || MessageUserStore.loading(true);
|
||||
Remote.message((iError, oData/*, bCached*/) => {
|
||||
if (iError) {
|
||||
if (Notification.RequestAborted !== iError && !popup) {
|
||||
MessageUserStore.message(null);
|
||||
MessageUserStore.error(getNotification(iError));
|
||||
}
|
||||
} else {
|
||||
let json = oData?.Result;
|
||||
|
||||
if (
|
||||
json &&
|
||||
MessageModel.validJson(json) &&
|
||||
oMessage.folder === json.Folder
|
||||
) {
|
||||
const threads = oMessage.threads(),
|
||||
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.body) {
|
||||
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));
|
||||
}
|
||||
|
||||
if (oMessage && oMessage.uid == json.Uid) {
|
||||
popup || MessageUserStore.error('');
|
||||
} else {
|
||||
let json = oData?.Result;
|
||||
if (json
|
||||
&& oMessage.hash === json.hash
|
||||
// && oMessage.folder === json.folder
|
||||
// && oMessage.uid == json.uid
|
||||
&& oMessage.revivePropertiesFromJson(json)
|
||||
) {
|
||||
/*
|
||||
if (bCached) {
|
||||
delete json.Flags;
|
||||
delete json.flags;
|
||||
}
|
||||
oMessage.body.remove();
|
||||
*/
|
||||
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.pgpEncrypted() ? ' openpgp-encrypted' : '')
|
||||
+ '">'
|
||||
+ '</div>');
|
||||
oMessage.body = body;
|
||||
if (!SettingsUserStore.viewHTML() || !oMessage.viewHtml()) {
|
||||
oMessage.viewPlain();
|
||||
}
|
||||
|
||||
MessageUserStore.purgeMessageBodyCache();
|
||||
}
|
||||
|
||||
messagesDom.append(body);
|
||||
|
||||
popup || (oMessage.body.hidden = false);
|
||||
popup && oMessage.viewPopupMessage();
|
||||
}
|
||||
|
||||
MessageFlagsCache.initMessage(oMessage);
|
||||
if (oMessage.isUnseen()) {
|
||||
MessageUserStore.MessageSeenTimer = setTimeout(
|
||||
() => MessagelistUserStore.setAction(oMessage.folder, MessageSetAction.SetSeen, [oMessage]),
|
||||
SettingsUserStore.messageReadDelay() * 1000 // seconds
|
||||
);
|
||||
}
|
||||
|
||||
if (isNew) {
|
||||
let selectedMessage = MessagelistUserStore.selectedMessage();
|
||||
if (
|
||||
selectedMessage &&
|
||||
(oMessage.folder !== selectedMessage.folder || oMessage.uid != selectedMessage.uid)
|
||||
) {
|
||||
MessagelistUserStore.selectedMessage(null);
|
||||
if (1 === MessagelistUserStore.length) {
|
||||
MessagelistUserStore.focusedMessage(null);
|
||||
}
|
||||
} else if (!selectedMessage) {
|
||||
selectedMessage = MessagelistUserStore.find(
|
||||
subMessage =>
|
||||
subMessage &&
|
||||
subMessage.folder === oMessage.folder &&
|
||||
subMessage.uid == oMessage.uid
|
||||
);
|
||||
|
||||
if (selectedMessage) {
|
||||
MessagelistUserStore.selectedMessage(selectedMessage);
|
||||
MessagelistUserStore.focusedMessage(selectedMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
viewMessage(oMessage, popup);
|
||||
}
|
||||
}
|
||||
}
|
||||
popup || MessageUserStore.loading(false);
|
||||
}, oMessage.folder, oMessage.uid);
|
||||
popup || MessageUserStore.loading(false);
|
||||
}, oMessage.folder, oMessage.uid);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
leftPanelDisabled.subscribe(value => value && moveAction(false));
|
||||
moveAction.subscribe(value => value && leftPanelDisabled(false));
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
export class CheckboxComponent {
|
||||
constructor(params = {}) {
|
||||
this.name = params.name;
|
||||
|
||||
this.value = ko.isObservable(params.value) ? params.value
|
||||
: ko.observable(!!params.value);
|
||||
|
||||
|
|
@ -7,7 +9,6 @@ export class CheckboxComponent {
|
|||
: ko.observable(undefined === params.enable || !!params.enable);
|
||||
|
||||
this.label = params.label;
|
||||
this.inline = params.inline;
|
||||
}
|
||||
|
||||
click() {
|
||||
|
|
|
|||
|
|
@ -1,8 +1,29 @@
|
|||
import { doc, createElement, addEventsListeners } from 'Common/Globals';
|
||||
import { EmailModel } from 'Model/Email';
|
||||
import { EmailModel, addressparser } from 'Model/Email';
|
||||
|
||||
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;
|
||||
|
||||
|
|
@ -154,8 +175,8 @@ export class EmailAddressesComponent {
|
|||
if (val) {
|
||||
const self = this,
|
||||
v = val.trim(),
|
||||
hook = (v && [',', ';', '\n'].includes(v.slice(-1))) ? EmailModel.splitEmailLine(val) : null,
|
||||
values = (hook || [val]).map(value => EmailModel.parseEmailLine(value))
|
||||
hook = (v && [',', ';', '\n'].includes(v.slice(-1))) ? splitEmailLine(val) : null,
|
||||
values = (hook || [val]).map(value => parseEmailLine(value))
|
||||
.flat(Infinity)
|
||||
.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),
|
||||
|
||||
tpl = createElement('template'),
|
||||
clr = createElement('input'),
|
||||
|
||||
trimLines = html => html.trim().replace(/^(<div>\s*<br\s*\/?>\s*<\/div>)+/, '').trim(),
|
||||
htmlToPlain = html => rl.Utils.htmlToPlain(html).trim(),
|
||||
plainToHtml = text => rl.Utils.plainToHtml(text),
|
||||
|
||||
forEachObjectValue = (obj, fn) => Object.values(obj).forEach(fn),
|
||||
|
||||
getFragmentOfChildren = parent => {
|
||||
let frag = doc.createDocumentFragment();
|
||||
frag.append(...parent.childNodes);
|
||||
|
|
@ -28,11 +29,6 @@ const
|
|||
|
||||
SquireDefaultConfig = {
|
||||
/*
|
||||
blockTag: 'P',
|
||||
undo: {
|
||||
documentSizeThreshold: -1, // -1 means no threshold
|
||||
undoLimit: -1 // -1 means no limit
|
||||
},
|
||||
addLinks: true // allow_smart_html_links
|
||||
*/
|
||||
sanitizeToDOMFragment: (html, isPaste/*, squire*/) => {
|
||||
|
|
@ -61,18 +57,19 @@ const
|
|||
}
|
||||
};
|
||||
|
||||
clr.type = "color";
|
||||
clr.style.display = 'none';
|
||||
doc.body.append(clr);
|
||||
|
||||
class SquireUI
|
||||
{
|
||||
constructor(container) {
|
||||
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.onchange = () => squire.setStyle({[name]:clr.value});
|
||||
clr.click();
|
||||
setTimeout(()=>clr.click(),1);
|
||||
},
|
||||
|
||||
actions = {
|
||||
|
|
@ -84,8 +81,7 @@ class SquireUI
|
|||
[i18n('SETTINGS_GENERAL/EDITOR_HTML'),'wysiwyg'],
|
||||
[i18n('SETTINGS_GENERAL/EDITOR_PLAIN'),'plain']
|
||||
],
|
||||
cmd: s => this.setMode('plain' == s.value ? 'plain' : 'wysiwyg'),
|
||||
hint: i18n('EDITOR/TEXT_SWITCHER_PLAIN_TEXT', 'Plain')
|
||||
cmd: s => this.setMode('plain' == s.value ? 'plain' : 'wysiwyg')
|
||||
}
|
||||
},
|
||||
font: {
|
||||
|
|
@ -112,19 +108,44 @@ class SquireUI
|
|||
},
|
||||
fontSize: {
|
||||
select: ['11px','13px','16px','20px','24px','30px'],
|
||||
defaultValueIndex: 2,
|
||||
cmd: s => squire.setStyle({ fontSize: s.value })
|
||||
// TODO: maybe consider using https://developer.mozilla.org/en-US/docs/Web/CSS/font-size#values
|
||||
// example:
|
||||
// select: ['xx-small', 'x-small',' small',' medium', 'large', 'x-large', 'xx-large', 'xxx-large'],
|
||||
// defaultValueIndex: 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: {
|
||||
textColor: {
|
||||
html: 'A<sub>▾</sub>',
|
||||
cmd: doClr('color'),
|
||||
hint: 'Text color'
|
||||
cmd: doClr('color')
|
||||
},
|
||||
backgroundColor: {
|
||||
html: '🎨', /* ▧ */
|
||||
cmd: doClr('backgroundColor'),
|
||||
hint: 'Background color'
|
||||
cmd: doClr('backgroundColor')
|
||||
},
|
||||
},
|
||||
inline: {
|
||||
|
|
@ -132,37 +153,37 @@ class SquireUI
|
|||
html: 'B',
|
||||
cmd: () => this.doAction('bold'),
|
||||
key: 'B',
|
||||
hint: 'Bold'
|
||||
matches: 'B,STRONT'
|
||||
},
|
||||
italic: {
|
||||
html: 'I',
|
||||
cmd: () => this.doAction('italic'),
|
||||
key: 'I',
|
||||
hint: 'Italic'
|
||||
matches: 'I'
|
||||
},
|
||||
underline: {
|
||||
html: '<u>U</u>',
|
||||
cmd: () => this.doAction('underline'),
|
||||
key: 'U',
|
||||
hint: 'Underline'
|
||||
matches: 'U'
|
||||
},
|
||||
strike: {
|
||||
html: '<s>S</s>',
|
||||
cmd: () => this.doAction('strikethrough'),
|
||||
key: 'Shift + 7',
|
||||
hint: 'Strikethrough'
|
||||
matches: 'S'
|
||||
},
|
||||
sub: {
|
||||
html: 'Xₙ',
|
||||
cmd: () => this.doAction('subscript'),
|
||||
key: 'Shift + 5',
|
||||
hint: 'Subscript'
|
||||
matches: 'SUB'
|
||||
},
|
||||
sup: {
|
||||
html: 'Xⁿ',
|
||||
cmd: () => this.doAction('superscript'),
|
||||
key: 'Shift + 6',
|
||||
hint: 'Superscript'
|
||||
matches: 'SUP'
|
||||
}
|
||||
},
|
||||
block: {
|
||||
|
|
@ -170,13 +191,13 @@ class SquireUI
|
|||
html: '#',
|
||||
cmd: () => this.doList('OL'),
|
||||
key: 'Shift + 8',
|
||||
hint: 'Ordered list'
|
||||
matches: 'OL'
|
||||
},
|
||||
ul: {
|
||||
html: '⋮',
|
||||
cmd: () => this.doList('UL'),
|
||||
key: 'Shift + 9',
|
||||
hint: 'Unordered list'
|
||||
matches: 'UL'
|
||||
},
|
||||
quote: {
|
||||
html: '"',
|
||||
|
|
@ -184,19 +205,17 @@ class SquireUI
|
|||
let parent = squire.getSelectionClosest('UL,OL,BLOCKQUOTE')?.nodeName;
|
||||
('BLOCKQUOTE' == parent) ? squire.decreaseQuoteLevel() : squire.increaseQuoteLevel();
|
||||
},
|
||||
hint: 'Blockquote'
|
||||
matches: 'BLOCKQUOTE'
|
||||
},
|
||||
indentDecrease: {
|
||||
html: '⇤',
|
||||
cmd: () => squire.changeIndentationLevel('decrease'),
|
||||
key: ']',
|
||||
hint: 'Decrease indent'
|
||||
key: ']'
|
||||
},
|
||||
indentIncrease: {
|
||||
html: '⇥',
|
||||
cmd: () => squire.changeIndentationLevel('increase'),
|
||||
key: '[',
|
||||
hint: 'Increase indent'
|
||||
key: '['
|
||||
}
|
||||
},
|
||||
targets: {
|
||||
|
|
@ -209,21 +228,21 @@ class SquireUI
|
|||
url.length ? squire.makeLink(url) : (node && squire.removeLink());
|
||||
}
|
||||
},
|
||||
hint: 'Link'
|
||||
matches: 'A'
|
||||
},
|
||||
imageUrl: {
|
||||
html: '🖼️',
|
||||
cmd: () => {
|
||||
let node = squire.getSelectionClosest('IMG'),
|
||||
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: {
|
||||
html: '📂️',
|
||||
cmd: () => browseImage.click(),
|
||||
hint: 'Image select',
|
||||
matches: 'IMG'
|
||||
}
|
||||
},
|
||||
/*
|
||||
|
|
@ -235,19 +254,19 @@ class SquireUI
|
|||
undo: {
|
||||
html: '↶',
|
||||
cmd: () => squire.undo(),
|
||||
key: 'Z',
|
||||
hint: 'Undo'
|
||||
key: 'Z'
|
||||
},
|
||||
redo: {
|
||||
html: '↷',
|
||||
cmd: () => squire.redo(),
|
||||
key: 'Y',
|
||||
hint: 'Redo'
|
||||
key: 'Y'
|
||||
},
|
||||
source: {
|
||||
html: '👁',
|
||||
cmd: () => this.setMode('source' == this.mode ? 'wysiwyg' : 'source'),
|
||||
hint: i18n('EDITOR/TEXT_SWITCHER_SOURCE', 'Source')
|
||||
cmd: btn => {
|
||||
this.setMode('source' == this.mode ? 'wysiwyg' : 'source');
|
||||
btn.classList.toggle('active', 'source' == this.mode);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
@ -258,6 +277,9 @@ class SquireUI
|
|||
browseImage = createElement('input'),
|
||||
squire = new Squire(wysiwyg, SquireDefaultConfig);
|
||||
|
||||
clr.type = 'color';
|
||||
toolbar.append(clr);
|
||||
|
||||
browseImage.type = 'file';
|
||||
browseImage.accept = 'image/*';
|
||||
browseImage.style.display = 'none';
|
||||
|
|
@ -271,6 +293,7 @@ class SquireUI
|
|||
|
||||
plain.className = 'squire-plain';
|
||||
wysiwyg.className = 'squire-wysiwyg';
|
||||
wysiwyg.dir = 'auto';
|
||||
this.mode = ''; // 'plain' | 'wysiwyg'
|
||||
this.__plain = {
|
||||
getRawData: () => this.plain.value,
|
||||
|
|
@ -282,14 +305,11 @@ class SquireUI
|
|||
this.plain = plain;
|
||||
this.wysiwyg = wysiwyg;
|
||||
|
||||
dispatchEvent(new CustomEvent('squire-toolbar', {detail:{squire:this,actions:actions}}));
|
||||
|
||||
toolbar.className = 'squire-toolbar btn-toolbar';
|
||||
let group, action/*, touchTap*/;
|
||||
for (group in actions) {
|
||||
/*
|
||||
if ('bidi' == group && !rl.settings.app('allowHtmlEditorBitiButtons')) {
|
||||
continue;
|
||||
}
|
||||
*/
|
||||
let toolgroup = createElement('div');
|
||||
toolgroup.className = 'btn-group';
|
||||
toolgroup.id = 'squire-toolgroup-'+group;
|
||||
|
|
@ -342,6 +362,7 @@ class SquireUI
|
|||
*/
|
||||
}
|
||||
input.addEventListener(ev, () => cfg.cmd(input));
|
||||
cfg.hint = i18n('EDITOR/' + action.toUpperCase());
|
||||
if (cfg.hint) {
|
||||
input.title = cfg.key ? cfg.hint + ' (' + ctrlKey + cfg.key + ')' : cfg.hint;
|
||||
} else if (cfg.key) {
|
||||
|
|
@ -364,21 +385,127 @@ class SquireUI
|
|||
changes.redo.input.disabled = !state.canRedo;
|
||||
});
|
||||
|
||||
actions.font.fontSize.input.selectedIndex = actions.font.fontSize.defaultValueIndex;
|
||||
|
||||
// squire.addEventListener('focus', () => shortcuts.off());
|
||||
// squire.addEventListener('blur', () => shortcuts.on());
|
||||
|
||||
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('drop', );
|
||||
squire.addEventListener('pathChange', );
|
||||
squire.addEventListener('cursor', );
|
||||
squire.addEventListener('select', );
|
||||
squire.addEventListener('input', );
|
||||
squire.addEventListener('willPaste', );
|
||||
squire.addEventListener( 'keydown keyup', monitorShiftKey )
|
||||
squire.addEventListener( 'keydown', onKey )
|
||||
squire.addEventListener('cursor', e => {
|
||||
console.dir({cursor:e.range});
|
||||
});
|
||||
squire.addEventListener('select', e => {
|
||||
console.dir({select:e.range});
|
||||
});
|
||||
*/
|
||||
|
||||
// CKEditor gimmicks used by HtmlEditor
|
||||
|
|
@ -401,11 +528,11 @@ class SquireUI
|
|||
fn = {UL:'makeUnorderedList',OL:'makeOrderedList'};
|
||||
(parent == type) ? this.squire.removeList() : this.squire[fn[type]]();
|
||||
}
|
||||
|
||||
/*
|
||||
testPresenceinSelection(format, validation) {
|
||||
return validation.test(this.squire.getPath()) || this.squire.hasFormat(format);
|
||||
}
|
||||
|
||||
*/
|
||||
setMode(mode) {
|
||||
if (this.mode != mode) {
|
||||
let cl = this.container.classList, source = 'source' == this.mode;
|
||||
|
|
@ -493,7 +620,12 @@ class SquireUI
|
|||
}
|
||||
|
||||
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 { HtmlEditor } from 'Common/Html';
|
||||
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 { EmailAddressesComponent } from 'Component/EmailAddresses';
|
||||
import { ThemeStore } from 'Stores/Theme';
|
||||
|
|
@ -29,37 +29,39 @@ const rlContentType = 'snappymail/action',
|
|||
},
|
||||
|
||||
dragTimer = {
|
||||
id: 0,
|
||||
stop: () => clearTimeout(dragTimer.id),
|
||||
start: fn => dragTimer.id = setTimeout(fn, 500)
|
||||
id: 0
|
||||
},
|
||||
|
||||
fnStop = (e, element) => {
|
||||
dragStop = (e, element) => {
|
||||
e.preventDefault();
|
||||
element.classList.remove('droppableHover');
|
||||
dragTimer.stop();
|
||||
element?.classList.remove('droppableHover');
|
||||
if (dragTimer.node == element) {
|
||||
dragTimer.node = null;
|
||||
clearTimeout(dragTimer.id);
|
||||
}
|
||||
},
|
||||
fnHover = (e, element, folder) => {
|
||||
dragEnter = (e, element, folder) => {
|
||||
let files = false;
|
||||
// if (e.dataTransfer.types.includes('Files'))
|
||||
for (const item of e.dataTransfer.items) {
|
||||
files |= 'file' === item.kind && 'message/rfc822' === item.type;
|
||||
}
|
||||
if (files || dragMessages()) {
|
||||
fnStop(e, element);
|
||||
files && e.stopPropagation();
|
||||
e.stopPropagation();
|
||||
dragStop(e, dragTimer.node);
|
||||
e.dataTransfer.dropEffect = files ? 'copy' : (e.ctrlKey ? 'copy' : 'move');
|
||||
element.classList.add('droppableHover');
|
||||
if (folder.collapsed()) {
|
||||
dragTimer.start(() => {
|
||||
dragTimer.node = element;
|
||||
dragTimer.id = setTimeout(() => {
|
||||
folder.collapsed(false);
|
||||
setExpandedFolder(folder.fullName, true);
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
},
|
||||
fnDrop = (e, element, folder, dragData) => {
|
||||
fnStop(e, element);
|
||||
dragDrop = (e, element, folder, dragData) => {
|
||||
dragStop(e, element);
|
||||
if (dragMessages() && 'copyMove' == e.dataTransfer.effectAllowed) {
|
||||
moveMessagesToFolder(FolderUserStore.currentFolderFullName(), dragData.data, folder.fullName, e.ctrlKey);
|
||||
} else if (e.dataTransfer.types.includes('Files')) {
|
||||
|
|
@ -79,8 +81,8 @@ Object.assign(ko.bindingHandlers, {
|
|||
let editor = null;
|
||||
|
||||
const fValue = fValueAccessor(),
|
||||
fUpdateEditorValue = () => fValue?.__editor?.setHtmlOrPlain(fValue()),
|
||||
fUpdateKoValue = () => fValue?.__editor && fValue(fValue.__editor.getDataWithHtmlMark()),
|
||||
fUpdateEditorValue = () => fValue.__editor?.setHtmlOrPlain(fValue()),
|
||||
fUpdateKoValue = () => fValue.__editor && fValue(fValue.__editor.getDataWithHtmlMark()),
|
||||
fOnReady = () => {
|
||||
fValue.__editor = editor;
|
||||
fUpdateEditorValue();
|
||||
|
|
@ -144,6 +146,8 @@ Object.assign(ko.bindingHandlers, {
|
|||
|
||||
// Remove the Chrome visibility
|
||||
dragImage.style.cssText = '';
|
||||
|
||||
leftPanelDisabled(false);
|
||||
} else {
|
||||
e.preventDefault();
|
||||
}
|
||||
|
|
@ -158,10 +162,10 @@ Object.assign(ko.bindingHandlers, {
|
|||
init: (element, fValueAccessor) => {
|
||||
const folder = fValueAccessor(); // ko.dataFor(element)
|
||||
folder && addEventsListeners(element, {
|
||||
dragenter: e => fnHover(e, element, folder),
|
||||
dragover: e => fnHover(e, element, folder),
|
||||
dragleave: e => fnStop(e, element),
|
||||
drop: e => fnDrop(e, element, folder, dragData)
|
||||
dragenter: e => dragEnter(e, element, folder),
|
||||
dragover: e => e.preventDefault(),
|
||||
dragleave: e => dragStop(e, element),
|
||||
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(),
|
||||
|
||||
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
|
||||
koArrayWithDestroy = data => {
|
||||
data = ko.observableArray(data);
|
||||
|
|
@ -56,41 +68,18 @@ Object.assign(ko.bindingHandlers, {
|
|||
},
|
||||
|
||||
onEnter: {
|
||||
init: (element, fValueAccessor, fAllBindings, viewModel) => {
|
||||
let fn = event => {
|
||||
if ('Enter' == event.key) {
|
||||
element.dispatchEvent(new Event('change'));
|
||||
fValueAccessor().call(viewModel);
|
||||
}
|
||||
};
|
||||
element.addEventListener('keydown', fn);
|
||||
ko.utils.domNodeDisposal.addDisposeCallback(element, () => element.removeEventListener('keydown', fn));
|
||||
}
|
||||
init: (element, fValueAccessor, fAllBindings, model) =>
|
||||
onKey('Enter', element, fValueAccessor, fAllBindings, model)
|
||||
},
|
||||
|
||||
onEsc: {
|
||||
init: (element, fValueAccessor, fAllBindings, viewModel) => {
|
||||
let fn = event => {
|
||||
if ('Escape' == event.key) {
|
||||
element.dispatchEvent(new Event('change'));
|
||||
fValueAccessor().call(viewModel);
|
||||
}
|
||||
};
|
||||
element.addEventListener('keyup', fn);
|
||||
ko.utils.domNodeDisposal.addDisposeCallback(element, () => element.removeEventListener('keyup', fn));
|
||||
}
|
||||
init: (element, fValueAccessor, fAllBindings, model) =>
|
||||
onKey('Escape', element, fValueAccessor, fAllBindings, model)
|
||||
},
|
||||
|
||||
onSpace: {
|
||||
init: (element, fValueAccessor, fAllBindings, viewModel) => {
|
||||
let fn = event => {
|
||||
if (' ' == event.key) {
|
||||
fValueAccessor().call(viewModel, event);
|
||||
}
|
||||
};
|
||||
element.addEventListener('keyup', fn);
|
||||
ko.utils.domNodeDisposal.addDisposeCallback(element, () => element.removeEventListener('keyup', fn));
|
||||
}
|
||||
init: (element, fValueAccessor, fAllBindings, model) =>
|
||||
onKey(' ', element, fValueAccessor, fAllBindings, model)
|
||||
},
|
||||
|
||||
i18nUpdate: {
|
||||
|
|
|
|||
|
|
@ -40,6 +40,12 @@ pre {
|
|||
body > * {
|
||||
padding: 0.5em 1em;
|
||||
}
|
||||
|
||||
#attachments > * {
|
||||
border: 1px solid rgba(125,128,128,0.5);
|
||||
padding: 0.25em;
|
||||
margin-right: 1em;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body></body>
|
||||
|
|
|
|||
|
|
@ -80,13 +80,11 @@ export class AbstractModel {
|
|||
}
|
||||
|
||||
revivePropertiesFromJson(json) {
|
||||
let model = this.constructor;
|
||||
if (!model.validJson(json)) {
|
||||
return false;
|
||||
}
|
||||
forEachObjectEntry(json, (key, value) => {
|
||||
const model = this.constructor,
|
||||
valid = model.validJson(json);
|
||||
valid && forEachObjectEntry(json, (key, value) => {
|
||||
if ('@' !== key[0]) try {
|
||||
key = key[0].toLowerCase() + key.slice(1);
|
||||
// key = key[0].toLowerCase() + key.slice(1);
|
||||
switch (typeof this[key])
|
||||
{
|
||||
case 'function':
|
||||
|
|
@ -102,9 +100,12 @@ export class AbstractModel {
|
|||
case 'string':
|
||||
this[key] = typeCast(this[key], value);
|
||||
break;
|
||||
// fall through
|
||||
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');
|
||||
}
|
||||
} catch (e) {
|
||||
|
|
@ -112,7 +113,7 @@ export class AbstractModel {
|
|||
console.error(e);
|
||||
}
|
||||
});
|
||||
return true;
|
||||
return valid;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import ko from '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 { SaveSettingStatus } from 'Common/Enums';
|
||||
|
|
@ -59,7 +59,8 @@ export class AbstractViewPopup extends AbstractView
|
|||
super('Popups' + name, ViewTypePopup);
|
||||
this.keyScope.scope = name;
|
||||
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()) {
|
||||
this.close();
|
||||
}
|
||||
|
|
@ -78,8 +79,6 @@ export class AbstractViewPopup extends AbstractView
|
|||
afterShow() {} // Happens after showModal() animation transitionend
|
||||
onHide() {} // Happens before animation transitionend
|
||||
afterHide() {} // Happens after animation transitionend
|
||||
|
||||
close() {}
|
||||
*/
|
||||
}
|
||||
|
||||
|
|
@ -97,6 +96,7 @@ export class AbstractViewLeft extends AbstractView
|
|||
{
|
||||
super(templateID, 'left');
|
||||
this.leftPanelDisabled = leftPanelDisabled;
|
||||
this.toggleLeftPanel = toggleLeftPanel;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,15 +1,19 @@
|
|||
import ko from 'ko';
|
||||
import { koComputable } from 'External/ko';
|
||||
import { doc, $htmlCL, elementById, fireEvent } from 'Common/Globals';
|
||||
import { forEachObjectValue, forEachObjectEntry } from 'Common/Utils';
|
||||
import { doc, $htmlCL, elementById, createElement, fireEvent } from 'Common/Globals';
|
||||
import { forEachObjectEntry } from 'Common/Utils';
|
||||
import { i18nToNodes } from 'Common/Translator';
|
||||
|
||||
import { leftPanelDisabled } from 'Common/Globals';
|
||||
import { ThemeStore } from 'Stores/Theme';
|
||||
|
||||
let
|
||||
SCREENS = {},
|
||||
currentScreen = null,
|
||||
defaultScreenName = '';
|
||||
|
||||
const
|
||||
SCREENS = new Map,
|
||||
|
||||
autofocus = dom => dom.querySelector('[autofocus]')?.focus(),
|
||||
|
||||
visiblePopups = new Set,
|
||||
|
|
@ -18,7 +22,7 @@ const
|
|||
* @param {string} screenName
|
||||
* @returns {?Object}
|
||||
*/
|
||||
screen = screenName => (screenName && SCREENS[screenName]) || null,
|
||||
screen = screenName => (screenName && SCREENS.get(screenName)) || null,
|
||||
|
||||
/**
|
||||
* @param {Function} ViewModelClass
|
||||
|
|
@ -35,27 +39,24 @@ const
|
|||
dialog = ViewTypePopup === vm.viewType,
|
||||
vmPlace = doc.getElementById(position);
|
||||
|
||||
fireEvent('rl-view-model.create', vm);
|
||||
|
||||
ViewModelClass.__builded = true;
|
||||
ViewModelClass.__vm = vm;
|
||||
|
||||
if (vmPlace) {
|
||||
vmDom = Element.fromHTML(dialog
|
||||
? '<dialog id="V-'+ id + '"></dialog>'
|
||||
: '<div id="V-'+ id + '" hidden=""></div>');
|
||||
vmDom = dialog
|
||||
? createElement('dialog',{id:'V-'+id})
|
||||
: createElement('div',{id:'V-'+id,hidden:''})
|
||||
vmPlace.append(vmDom);
|
||||
|
||||
vm.viewModelDom = ViewModelClass.__dom = vmDom;
|
||||
|
||||
if (dialog) {
|
||||
vm.close = () => hideScreenPopup(ViewModelClass);
|
||||
|
||||
// Firefox < 98 / Safari < 15.4 HTMLDialogElement not defined
|
||||
if (!vmDom.showModal) {
|
||||
vmDom.className = 'polyfill';
|
||||
vmDom.showModal = () => {
|
||||
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.open = true;
|
||||
vmDom.returnValue = null;
|
||||
|
|
@ -79,7 +80,6 @@ const
|
|||
const endShowHide = e => {
|
||||
if (e.target === vmDom) {
|
||||
if (vmDom.classList.contains('animate')) {
|
||||
autofocus(vmDom);
|
||||
vm.afterShow?.();
|
||||
} else {
|
||||
vmDom.close();
|
||||
|
|
@ -98,6 +98,7 @@ const
|
|||
vmDom.backdrop.style.zIndex = 3000 + (visiblePopups.size * 2);
|
||||
}
|
||||
vm.keyScope.set();
|
||||
setTimeout(()=>autofocus(vmDom),1);
|
||||
requestAnimationFrame(() => { // wait just before the next paint
|
||||
vmDom.offsetHeight; // force a reflow
|
||||
vmDom.classList.add('animate'); // trigger the transitions
|
||||
|
|
@ -113,6 +114,8 @@ const
|
|||
vmDom.addEventListener('transitionend', endShowHide);
|
||||
}
|
||||
|
||||
fireEvent('rl-view-model.create', vm);
|
||||
|
||||
ko.applyBindingAccessorsToNode(
|
||||
vmDom,
|
||||
{
|
||||
|
|
@ -129,7 +132,7 @@ const
|
|||
}
|
||||
}
|
||||
|
||||
return ViewModelClass && ViewModelClass.__vm;
|
||||
return ViewModelClass?.__vm;
|
||||
},
|
||||
|
||||
forEachViewModel = (screen, fn) => {
|
||||
|
|
@ -151,16 +154,7 @@ const
|
|||
vm.onHide?.();
|
||||
destroy && vm.viewModelDom.remove();
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {Function} ViewModelClassToHide
|
||||
* @returns {void}
|
||||
*/
|
||||
hideScreenPopup = ViewModelClassToHide => {
|
||||
if (ViewModelClassToHide?.__vm && ViewModelClassToHide?.__dom) {
|
||||
ViewModelClassToHide.__vm.modalVisible(false);
|
||||
}
|
||||
ThemeStore.isMobile() && leftPanelDisabled(true);
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
@ -169,69 +163,58 @@ const
|
|||
* @returns {void}
|
||||
*/
|
||||
screenOnRoute = (screenName, subPart) => {
|
||||
let vmScreen = null,
|
||||
isSameScreen = false;
|
||||
|
||||
if (null == screenName || '' == screenName) {
|
||||
screenName = defaultScreenName;
|
||||
}
|
||||
|
||||
if (fireEvent('sm-show-screen', screenName, 1)) {
|
||||
|
||||
screenName = screenName || defaultScreenName;
|
||||
if (screenName && fireEvent('sm-show-screen', screenName, 1)) {
|
||||
// Close all popups
|
||||
for (let vm of visiblePopups) {
|
||||
(false === vm.onClose()) || vm.close();
|
||||
}
|
||||
|
||||
if (screenName) {
|
||||
vmScreen = screen(screenName);
|
||||
if (!vmScreen) {
|
||||
vmScreen = screen(defaultScreenName);
|
||||
if (vmScreen) {
|
||||
subPart = screenName + '/' + subPart;
|
||||
screenName = defaultScreenName;
|
||||
}
|
||||
let vmScreen = screen(screenName);
|
||||
if (!vmScreen) {
|
||||
vmScreen = screen(defaultScreenName);
|
||||
if (vmScreen) {
|
||||
subPart = screenName + '/' + subPart;
|
||||
screenName = defaultScreenName;
|
||||
}
|
||||
}
|
||||
|
||||
if (vmScreen?.__started) {
|
||||
let isSameScreen = currentScreen && vmScreen === currentScreen;
|
||||
|
||||
if (!vmScreen.__builded) {
|
||||
vmScreen.__builded = true;
|
||||
|
||||
vmScreen.viewModels.forEach(ViewModelClass =>
|
||||
buildViewModel(ViewModelClass, vmScreen)
|
||||
);
|
||||
|
||||
vmScreen.onBuild?.();
|
||||
}
|
||||
|
||||
if (vmScreen?.__started) {
|
||||
isSameScreen = currentScreen && vmScreen === currentScreen;
|
||||
setTimeout(() => {
|
||||
// hide screen
|
||||
currentScreen && !isSameScreen && hideScreen(currentScreen);
|
||||
// --
|
||||
|
||||
if (!vmScreen.__builded) {
|
||||
vmScreen.__builded = true;
|
||||
currentScreen = vmScreen;
|
||||
|
||||
vmScreen.viewModels.forEach(ViewModelClass =>
|
||||
buildViewModel(ViewModelClass, vmScreen)
|
||||
);
|
||||
// show screen
|
||||
if (!isSameScreen) {
|
||||
vmScreen.onShow?.();
|
||||
|
||||
vmScreen.onBuild?.();
|
||||
forEachViewModel(vmScreen, (vm, dom) => {
|
||||
vm.beforeShow?.();
|
||||
i18nToNodes(dom);
|
||||
dom.hidden = false;
|
||||
vm.onShow?.();
|
||||
autofocus(dom);
|
||||
});
|
||||
}
|
||||
// --
|
||||
|
||||
setTimeout(() => {
|
||||
// hide screen
|
||||
if (currentScreen && !isSameScreen) {
|
||||
hideScreen(currentScreen);
|
||||
}
|
||||
// --
|
||||
|
||||
currentScreen = vmScreen;
|
||||
|
||||
// show screen
|
||||
if (!isSameScreen) {
|
||||
vmScreen.onShow?.();
|
||||
|
||||
forEachViewModel(vmScreen, (vm, dom) => {
|
||||
vm.beforeShow?.();
|
||||
i18nToNodes(dom);
|
||||
dom.hidden = false;
|
||||
vm.onShow?.();
|
||||
autofocus(dom);
|
||||
});
|
||||
}
|
||||
// --
|
||||
|
||||
vmScreen.__cross?.parse(subPart);
|
||||
}, 1);
|
||||
}
|
||||
vmScreen.__cross?.parse(subPart);
|
||||
}, 1);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
@ -267,21 +250,19 @@ export const
|
|||
*/
|
||||
startScreens = screensClasses => {
|
||||
hasher.clear();
|
||||
forEachObjectValue(SCREENS, screen => hideScreen(screen, 1));
|
||||
SCREENS = {};
|
||||
SCREENS.forEach(screen => hideScreen(screen, 1));
|
||||
SCREENS.clear();
|
||||
currentScreen = null,
|
||||
defaultScreenName = '';
|
||||
|
||||
screensClasses.forEach(CScreen => {
|
||||
if (CScreen) {
|
||||
const vmScreen = new CScreen(),
|
||||
screenName = vmScreen.screenName;
|
||||
defaultScreenName || (defaultScreenName = screenName);
|
||||
SCREENS[screenName] = vmScreen;
|
||||
}
|
||||
const vmScreen = new CScreen(),
|
||||
screenName = vmScreen.screenName;
|
||||
defaultScreenName || (defaultScreenName = screenName);
|
||||
SCREENS.set(screenName, vmScreen);
|
||||
});
|
||||
|
||||
forEachObjectValue(SCREENS, vmScreen => {
|
||||
SCREENS.forEach(vmScreen => {
|
||||
if (!vmScreen.__started) {
|
||||
vmScreen.onStart();
|
||||
vmScreen.__started = true;
|
||||
|
|
@ -289,7 +270,7 @@ export const
|
|||
});
|
||||
|
||||
const cross = new Crossroads();
|
||||
cross.addRoute(/^([a-zA-Z0-9-]*)\/?(.*)$/, screenOnRoute);
|
||||
cross.addRoute(/^([^/]*)\/?(.*)$/, screenOnRoute);
|
||||
|
||||
hasher.add(cross.parse.bind(cross));
|
||||
hasher.init();
|
||||
|
|
|
|||
|
|
@ -1,8 +1,17 @@
|
|||
//import { b64Encode } from 'Common/Utils';
|
||||
|
||||
const
|
||||
// RFC2045
|
||||
QPDecodeIn = /=([0-9A-F]{2})/g,
|
||||
QPDecodeOut = (...args) => String.fromCharCode(parseInt(args[1], 16));
|
||||
QPDecodeParams = [/=([0-9A-F]{2})/g, (...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)
|
||||
{
|
||||
|
|
@ -40,22 +49,14 @@ export function ParseMime(text)
|
|||
|
||||
get body() {
|
||||
let body = this.bodyRaw,
|
||||
// charset = this.header('content-type')?.params.charset,
|
||||
charset = this.header('content-type')?.params.charset,
|
||||
encoding = this.headerValue('content-transfer-encoding');
|
||||
if ('quoted-printable' == encoding) {
|
||||
body = body.replace(/=\r?\n/g, '').replace(QPDecodeIn, QPDecodeOut);
|
||||
body = QPDecode(body);
|
||||
} else if ('base64' == encoding) {
|
||||
body = atob(body.replace(/\r?\n/g, ''));
|
||||
}
|
||||
/*
|
||||
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;
|
||||
return decodeText(charset, body);
|
||||
}
|
||||
|
||||
get dataUrl() {
|
||||
|
|
@ -65,9 +66,10 @@ export function ParseMime(text)
|
|||
body = body.replace(/\r?\n/g, '');
|
||||
} else {
|
||||
if ('quoted-printable' == encoding) {
|
||||
body = body.replace(/=\r?\n/g, '').replace(QPDecodeIn, QPDecodeOut);
|
||||
body = QPDecode(body);
|
||||
}
|
||||
body = btoa(body);
|
||||
// body = b64Encode(body);
|
||||
}
|
||||
return 'data:' + this.headerValue('content-type') + ';base64,' + body;
|
||||
}
|
||||
|
|
@ -111,8 +113,12 @@ export function ParseMime(text)
|
|||
[...header.matchAll(/;\s*([^;=]+)=\s*"?([^;"]+)"?/g)].forEach(param =>
|
||||
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()] = {
|
||||
value: match[2].trim(),
|
||||
value: match[2],
|
||||
params: params
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
|
||||
import { ParseMime } from 'Mime/Parser';
|
||||
import { AttachmentModel } from 'Model/Attachment';
|
||||
import { EmailModel } from 'Model/Email';
|
||||
import { FileInfo } from 'Common/File';
|
||||
import { BEGIN_PGP_MESSAGE } from 'Stores/User/Pgp';
|
||||
|
||||
|
|
@ -14,25 +13,20 @@ export function MimeToMessage(data, message)
|
|||
let signed;
|
||||
const struct = ParseMime(data);
|
||||
if (struct.headers) {
|
||||
let html = struct.getByContentType('text/html');
|
||||
let html = struct.getByContentType('text/html'),
|
||||
subject = struct.headerValue('subject');
|
||||
html = html ? html.body : '';
|
||||
|
||||
if (struct.headers.subject) {
|
||||
message.subject(struct.headers.subject.value);
|
||||
}
|
||||
['from','to'].forEach(name => {
|
||||
if (struct.headers[name] && !message[name].length) {
|
||||
let mail = new EmailModel;
|
||||
mail.parse(struct.headers[name].value);
|
||||
message[name].push(mail);
|
||||
}
|
||||
});
|
||||
subject && message.subject(subject);
|
||||
|
||||
// EmailCollectionModel
|
||||
['from','to'].forEach(name => message[name].fromString(struct.headerValue(name)));
|
||||
|
||||
struct.forEach(part => {
|
||||
let cd = part.header('content-disposition'),
|
||||
cid = part.header('content-id'),
|
||||
cId = part.header('content-id'),
|
||||
type = part.header('content-type');
|
||||
if (cid || cd) {
|
||||
if (cId || cd) {
|
||||
// if (cd && 'attachment' === cd.value) {
|
||||
let attachment = new AttachmentModel;
|
||||
attachment.mimeType = type.value;
|
||||
|
|
@ -40,17 +34,15 @@ export function MimeToMessage(data, message)
|
|||
attachment.fileNameExt = attachment.fileName.replace(/^.+(\.[a-z]+)$/, '$1');
|
||||
attachment.fileType = FileInfo.getType('', type.value);
|
||||
attachment.url = part.dataUrl;
|
||||
attachment.friendlySize = FileInfo.friendlySize(part.body.length);
|
||||
attachment.estimatedSize = part.body.length;
|
||||
/*
|
||||
attachment.isThumbnail = false;
|
||||
attachment.contentLocation = '';
|
||||
attachment.download = '';
|
||||
attachment.folder = '';
|
||||
attachment.uid = '';
|
||||
attachment.mimeIndex = part.id;
|
||||
*/
|
||||
attachment.cid = cid ? cid.value : '';
|
||||
if (cid && html) {
|
||||
attachment.cId = cId ? cId.value : '';
|
||||
if (cId && html) {
|
||||
let cid = 'cid:' + attachment.contentId(),
|
||||
found = html.includes(cid);
|
||||
attachment.isInline(found);
|
||||
|
|
@ -64,9 +56,9 @@ export function MimeToMessage(data, message)
|
|||
}
|
||||
} else if ('multipart/signed' === type.value && 'application/pgp-signature' === type.params.protocol) {
|
||||
signed = {
|
||||
MicAlg: type.micalg,
|
||||
BodyPart: part.parts[0],
|
||||
SigPart: part.parts[1]
|
||||
micAlg: type.micalg,
|
||||
bodyPart: part.parts[0],
|
||||
sigPart: part.parts[1]
|
||||
};
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
import { AbstractModel } from 'Knoin/AbstractModel';
|
||||
import { addObservablesTo } from 'External/ko';
|
||||
import Remote from 'Remote/User/Fetch';
|
||||
import { SettingsUserStore } from 'Stores/User/Settings';
|
||||
|
||||
export class AccountModel extends AbstractModel {
|
||||
/**
|
||||
|
|
@ -7,16 +9,52 @@ export class AccountModel extends AbstractModel {
|
|||
* @param {boolean=} canBeDelete = true
|
||||
* @param {number=} count = 0
|
||||
*/
|
||||
constructor(email/*, count = 0*/, isAdditional = true) {
|
||||
constructor(email, name, isAdditional = true) {
|
||||
super();
|
||||
|
||||
this.name = name;
|
||||
this.email = email;
|
||||
|
||||
this.displayName = name ? name + ' <' + email + '>' : email;
|
||||
|
||||
addObservablesTo(this, {
|
||||
// count: count || 0,
|
||||
unreadEmails: null,
|
||||
askDelete: false,
|
||||
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 { FileInfo, FileType } from 'Common/File';
|
||||
import { stopEvent, SettingsGet, SettingsCapa } from 'Common/Globals';
|
||||
import { b64EncodeJSONSafe } from 'Common/Utils';
|
||||
import {
|
||||
attachmentDownload,
|
||||
serverRequestRaw
|
||||
|
|
@ -18,13 +20,12 @@ export class AttachmentModel extends AbstractModel {
|
|||
this.checked = ko.observable(true);
|
||||
|
||||
this.mimeType = '';
|
||||
// this.mimeTypeParams = '';
|
||||
this.fileName = '';
|
||||
this.fileNameExt = '';
|
||||
this.fileType = FileType.Unknown;
|
||||
this.isThumbnail = false;
|
||||
this.cid = '';
|
||||
this.cId = '';
|
||||
this.contentLocation = '';
|
||||
this.download = '';
|
||||
this.folder = '';
|
||||
this.uid = '';
|
||||
this.url = '';
|
||||
|
|
@ -51,12 +52,17 @@ export class AttachmentModel extends AbstractModel {
|
|||
return attachment;
|
||||
}
|
||||
|
||||
toggleChecked(self, event) {
|
||||
stopEvent(event);
|
||||
self.checked(!self.checked());
|
||||
}
|
||||
|
||||
friendlySize() {
|
||||
return FileInfo.friendlySize(this.estimatedSize) + (this.isLinked() ? ' 🔗' : '');
|
||||
}
|
||||
|
||||
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}
|
||||
*/
|
||||
|
|
@ -137,7 +154,7 @@ export class AttachmentModel extends AbstractModel {
|
|||
* @returns {boolean}
|
||||
*/
|
||||
hasThumbnail() {
|
||||
return this.isThumbnail && !this.isLinked();
|
||||
return SettingsCapa('AttachmentThumbnails') && this.isImage() && !this.isLinked();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -146,7 +163,7 @@ export class AttachmentModel extends AbstractModel {
|
|||
thumbnailStyle() {
|
||||
return this.hasThumbnail()
|
||||
? 'background:url(' + serverRequestRaw('ViewThumbnail', this.download) + ')'
|
||||
: '';
|
||||
: null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -21,11 +21,11 @@ export class AttachmentCollectionModel extends AbstractCollectionModel
|
|||
}
|
||||
|
||||
/**
|
||||
* @param {string} cid
|
||||
* @param {string} cId
|
||||
* @returns {*}
|
||||
*/
|
||||
findByCid(cid) {
|
||||
cid = cid.replace(/^<+|>+$/g, '');
|
||||
return this.find(item => cid === item.contentId());
|
||||
findByCid(cId) {
|
||||
cId = cId.replace(/^<+|>+$/g, '');
|
||||
return this.find(item => cId === item.contentId());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,16 +10,16 @@ export class ComposeAttachmentModel extends AbstractModel {
|
|||
* @param {?number=} size = null
|
||||
* @param {boolean=} isInline = false
|
||||
* @param {boolean=} isLinked = false
|
||||
* @param {string=} CID = ''
|
||||
* @param {string=} cId = ''
|
||||
* @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();
|
||||
|
||||
this.id = id;
|
||||
this.isInline = !!isInline;
|
||||
this.isLinked = !!isLinked;
|
||||
this.CID = CID;
|
||||
this.cId = cId;
|
||||
this.contentLocation = contentLocation;
|
||||
this.fromMessage = false;
|
||||
|
||||
|
|
@ -27,6 +27,7 @@ export class ComposeAttachmentModel extends AbstractModel {
|
|||
fileName: fileName,
|
||||
size: size,
|
||||
tempName: '',
|
||||
type: '', // application/octet-stream
|
||||
|
||||
progress: 0,
|
||||
error: '',
|
||||
|
|
@ -54,7 +55,7 @@ export class ComposeAttachmentModel extends AbstractModel {
|
|||
return null === localSize ? '' : FileInfo.friendlySize(localSize);
|
||||
},
|
||||
|
||||
mimeType: () => FileInfo.getContentType(this.fileName()),
|
||||
mimeType: () => this.type() || FileInfo.getContentType(this.fileName()),
|
||||
fileExt: () => FileInfo.getExtension(this.fileName()),
|
||||
|
||||
iconClass: () => FileInfo.getIconClass(this.fileExt(), this.mimeType())
|
||||
|
|
|
|||
|
|
@ -119,7 +119,7 @@ export class ContactModel extends AbstractModel {
|
|||
|
||||
display: () => {
|
||||
let a = this.fullName(),
|
||||
b = this.email()?.[0]?.value,
|
||||
b = this.email()[0]?.value(),
|
||||
c = this.nickname();
|
||||
return a || b || c;
|
||||
}
|
||||
|
|
@ -139,7 +139,7 @@ export class ContactModel extends AbstractModel {
|
|||
*/
|
||||
getNameAndEmailHelper() {
|
||||
let name = (this.givenName() + ' ' + this.surName()).trim(),
|
||||
email = this.email()[0];
|
||||
email = this.email()[0]?.value();
|
||||
/*
|
||||
// this.jCard.getOne('fn')?.notEmpty() ||
|
||||
this.jCard.parseFullName({set:true});
|
||||
|
|
@ -304,7 +304,7 @@ export class ContactModel extends AbstractModel {
|
|||
// jCard.set('rev', '2022-05-21T10:59:52Z')
|
||||
|
||||
return {
|
||||
Uid: this.id,
|
||||
uid: this.id,
|
||||
jCard: JSON.stringify(jCard)
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,32 +18,83 @@ import { AbstractModel } from 'Knoin/AbstractModel';
|
|||
* @param {String} str Address field
|
||||
* @return {Array} An array of address objects
|
||||
*/
|
||||
function addressparser(str) {
|
||||
var tokenizer = new Tokenizer(str);
|
||||
var tokens = tokenizer.tokenize();
|
||||
var addresses = [];
|
||||
var address = [];
|
||||
var parsedAddresses = [];
|
||||
export function addressparser(str) {
|
||||
str = (str || '').toString();
|
||||
|
||||
tokens.forEach(token => {
|
||||
if (token.type === 'operator' && (token.value === ',' || token.value === ';')) {
|
||||
address.length && addresses.push(address);
|
||||
let
|
||||
endOperator = '',
|
||||
node = {
|
||||
type: 'text',
|
||||
value: ''
|
||||
},
|
||||
escaped = false,
|
||||
address = [],
|
||||
addresses = [];
|
||||
|
||||
const
|
||||
/*
|
||||
* Operator tokens and which tokens are expected to end the sequence
|
||||
*/
|
||||
OPERATORS = {
|
||||
'"': '"',
|
||||
'(': ')',
|
||||
'<': '>',
|
||||
',': '',
|
||||
// Groups are ended by semicolons
|
||||
':': ';',
|
||||
// Semicolons are not a legal delimiter per the RFC2822 grammar other
|
||||
// than for terminating a group, but they are also not valid for any
|
||||
// other use in this context. Given that some mail clients have
|
||||
// historically allowed the semicolon as a delimiter equivalent to the
|
||||
// comma in their UI, it makes sense to treat them the same as a comma
|
||||
// when used outside of a group.
|
||||
';': ''
|
||||
},
|
||||
pushToken = token => {
|
||||
token.value = (token.value || '').toString().trim();
|
||||
token.value.length && address.push(token);
|
||||
node = {
|
||||
type: 'text',
|
||||
value: ''
|
||||
},
|
||||
escaped = false;
|
||||
},
|
||||
pushAddress = () => {
|
||||
if (address.length) {
|
||||
address = _handleAddress(address);
|
||||
if (address.length) {
|
||||
addresses = addresses.concat(address);
|
||||
}
|
||||
}
|
||||
address = [];
|
||||
};
|
||||
|
||||
[...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 {
|
||||
address.push(token);
|
||||
node.value += chr;
|
||||
escaped = !escaped && '\\' === chr;
|
||||
}
|
||||
});
|
||||
pushToken(node);
|
||||
|
||||
address.length && addresses.push(address);
|
||||
pushAddress();
|
||||
|
||||
addresses.forEach(address => {
|
||||
address = _handleAddress(address);
|
||||
if (address.length) {
|
||||
parsedAddresses = parsedAddresses.concat(address);
|
||||
}
|
||||
});
|
||||
|
||||
return parsedAddresses;
|
||||
return addresses;
|
||||
// return addresses.map(item => (item.name || item.email) ? new EmailModel(item.email, item.name) : null).filter(v => v);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -53,37 +104,20 @@ function addressparser(str) {
|
|||
* @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: []
|
||||
};
|
||||
let
|
||||
isGroup = false,
|
||||
address = {},
|
||||
addresses = [],
|
||||
data = {
|
||||
email: [],
|
||||
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);
|
||||
}
|
||||
isGroup = isGroup || 'group' === token.type;
|
||||
data[token.type].push(token.value);
|
||||
});
|
||||
|
||||
// If there is no text but a comment, replace the two
|
||||
|
|
@ -94,181 +128,90 @@ function _handleAddress(tokens) {
|
|||
|
||||
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(',')) : []
|
||||
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.address.length && data.text.length) {
|
||||
if (!data.email.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);
|
||||
data.email = data.text.splice(i, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// still no address
|
||||
if (!data.address.length) {
|
||||
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.address.length) {
|
||||
data.address = [address.trim()];
|
||||
if (!data.email.length) {
|
||||
data.email = [address.trim()];
|
||||
return '';
|
||||
}
|
||||
return address.trim();
|
||||
});
|
||||
if (data.address.length) {
|
||||
if (data.email.length) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If there's still is no text but a comment exixts, replace the two
|
||||
// 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.address.length > 1) {
|
||||
data.text = data.text.concat(data.address.splice(1));
|
||||
if (data.email.length > 1) {
|
||||
data.text = data.text.concat(data.email.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 || ''
|
||||
// Join values with spaces
|
||||
email: data.email.join(' ').trim(),
|
||||
name: data.text.join(' ').trim()
|
||||
// ,comment: data.comment.join(' ').trim()
|
||||
};
|
||||
|
||||
if (address.address === address.name) {
|
||||
if ((address.address || '').match(/@/)) {
|
||||
if (address.email === address.name) {
|
||||
if (address.email.includes('@')) {
|
||||
address.name = '';
|
||||
} else {
|
||||
address.address = '';
|
||||
address.email = '';
|
||||
}
|
||||
}
|
||||
|
||||
// address.email = address.email.replace(/^[<]+(.*)[>]+$/g, '$1');
|
||||
|
||||
addresses.push(address);
|
||||
}
|
||||
|
||||
return addresses;
|
||||
}
|
||||
|
||||
/*
|
||||
* Operator tokens and which tokens are expected to end the sequence
|
||||
*/
|
||||
var OPERATORS = {
|
||||
'"': '"',
|
||||
'(': ')',
|
||||
'<': '>',
|
||||
',': '',
|
||||
// Groups are ended by semicolons
|
||||
':': ';',
|
||||
// Semicolons are not a legal delimiter per the RFC2822 grammar other
|
||||
// than for terminating a group, but they are also not valid for any
|
||||
// other use in this context. Given that some mail clients have
|
||||
// historically allowed the semicolon as a delimiter equivalent to the
|
||||
// comma in their UI, it makes sense to treat them the same as a comma
|
||||
// when used outside of a group.
|
||||
';': ''
|
||||
};
|
||||
|
||||
class Tokenizer
|
||||
{
|
||||
constructor(str) {
|
||||
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',
|
||||
value: ''
|
||||
};
|
||||
this.list.push(this.node);
|
||||
}
|
||||
|
||||
if (this.escaped && chr !== '\\') {
|
||||
this.node.value += '\\';
|
||||
}
|
||||
|
||||
this.node.value += chr;
|
||||
this.escaped = false;
|
||||
}
|
||||
}
|
||||
|
||||
export class EmailModel extends AbstractModel {
|
||||
/**
|
||||
* @param {string=} email = ''
|
||||
* @param {string=} name = ''
|
||||
* @param {string=} dkimStatus = 'none'
|
||||
* @param {string=} dkimValue = ''
|
||||
*/
|
||||
constructor(email = '', name = '', dkimStatus = 'none', dkimValue = '') {
|
||||
constructor(email, name, dkimStatus = 'none') {
|
||||
super();
|
||||
this.email = email;
|
||||
this.name = name;
|
||||
this.email = email || '';
|
||||
this.name = name || '';
|
||||
this.dkimStatus = dkimStatus;
|
||||
this.dkimValue = dkimValue;
|
||||
|
||||
this.clearDuplicateName();
|
||||
this.cleanup();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -278,61 +221,34 @@ export class EmailModel extends AbstractModel {
|
|||
*/
|
||||
static reviveFromJson(json) {
|
||||
const email = super.reviveFromJson(json);
|
||||
email?.clearDuplicateName();
|
||||
return email;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {void}
|
||||
*/
|
||||
clear() {
|
||||
this.email = '';
|
||||
this.name = '';
|
||||
|
||||
this.dkimStatus = 'none';
|
||||
this.dkimValue = '';
|
||||
email?.cleanup();
|
||||
return email?.valid() ? email : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {boolean}
|
||||
*/
|
||||
validate() {
|
||||
valid() {
|
||||
return this.name || this.email;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {boolean} withoutName = false
|
||||
* @returns {string}
|
||||
*/
|
||||
hash(withoutName = false) {
|
||||
return '#' + (withoutName ? '' : this.name) + '#' + this.email + '#';
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {void}
|
||||
*/
|
||||
clearDuplicateName() {
|
||||
cleanup() {
|
||||
if (this.name === this.email) {
|
||||
this.name = '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} query
|
||||
* @returns {boolean}
|
||||
*/
|
||||
search(query) {
|
||||
return (this.name + ' ' + this.email).toLowerCase().includes(query.toLowerCase());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {boolean} friendlyView = false
|
||||
* @param {boolean} wrapWithLink = false
|
||||
* @returns {string}
|
||||
*/
|
||||
toLine(friendlyView, wrapWithLink) {
|
||||
let result = this.email,
|
||||
name = this.name,
|
||||
let name = this.name,
|
||||
result = this.email,
|
||||
toLink = text =>
|
||||
'<a href="mailto:'
|
||||
+ encodeHtml(result) + (name ? '?to=' + encodeURIComponent('"' + name + '" <' + result + '>') : '')
|
||||
|
|
@ -351,47 +267,6 @@ export class EmailModel extends AbstractModel {
|
|||
result = toLink();
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
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;
|
||||
return result || name;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { AbstractCollectionModel } from 'Model/AbstractCollection';
|
||||
import { EmailModel } from 'Model/Email';
|
||||
import { EmailModel, addressparser } from 'Model/Email';
|
||||
import { forEachObjectValue } from 'Common/Utils';
|
||||
|
||||
'use strict';
|
||||
|
||||
|
|
@ -13,6 +14,16 @@ export class EmailCollectionModel extends AbstractCollectionModel
|
|||
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=} wrapWithLink = false
|
||||
|
|
@ -21,4 +32,23 @@ export class EmailCollectionModel extends AbstractCollectionModel
|
|||
toString(friendlyView, wrapWithLink) {
|
||||
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 { isArray, getKeyByValue, forEachObjectEntry, b64EncodeJSONSafe } from 'Common/Utils';
|
||||
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 { Notifications } from 'Common/Enums';
|
||||
|
||||
import * as Local from 'Storage/Client';
|
||||
|
||||
|
|
@ -14,18 +15,18 @@ import { MessagelistUserStore } from 'Stores/User/Messagelist';
|
|||
import { SettingsUserStore } from 'Stores/User/Settings';
|
||||
|
||||
import { sortFolders } from 'Common/Folders';
|
||||
import { i18n, translateTrigger } from 'Common/Translator';
|
||||
import { i18n, translateTrigger, getNotification } from 'Common/Translator';
|
||||
|
||||
import { AbstractModel } from 'Knoin/AbstractModel';
|
||||
|
||||
import { koComputable, addObservablesTo } from 'External/ko';
|
||||
import { /*koComputable,*/ addObservablesTo } from 'External/ko';
|
||||
|
||||
//import { mailBox } from 'Common/Links';
|
||||
|
||||
import Remote from 'Remote/User/Fetch';
|
||||
|
||||
const
|
||||
isPosNumeric = value => null != value && /^[0-9]*$/.test(value.toString()),
|
||||
// isPosNumeric = value => null != value && /^[0-9]*$/.test(value.toString()),
|
||||
|
||||
normalizeFolder = sFolderFullName => ('' === sFolderFullName
|
||||
|| UNUSED_OPTION_VALUE === sFolderFullName
|
||||
|
|
@ -37,7 +38,7 @@ const
|
|||
Inbox: 0,
|
||||
Sent: 0,
|
||||
Drafts: 0,
|
||||
Spam: 0,
|
||||
Junk: 0, // Spam
|
||||
Trash: 0,
|
||||
Archive: 0
|
||||
},
|
||||
|
|
@ -62,7 +63,7 @@ const
|
|||
case FolderType.Trash:
|
||||
case FolderType.Archive:
|
||||
return i18n('FOLDER_LIST/' + getKeyByValue(FolderType, type).toUpperCase() + '_NAME');
|
||||
case FolderType.Spam:
|
||||
case FolderType.Junk:
|
||||
return i18n('GLOBAL/SPAM');
|
||||
// no default
|
||||
}
|
||||
|
|
@ -91,6 +92,7 @@ export const
|
|||
Remote.abort('Folders')
|
||||
.post('Folders', FolderUserStore.foldersLoading)
|
||||
.then(data => {
|
||||
clearCache();
|
||||
FolderCollectionModel.reviveFromJson(data.Result)?.storeIt();
|
||||
fCallback?.(true);
|
||||
// Repeat every 15 minutes?
|
||||
|
|
@ -104,12 +106,11 @@ export class FolderCollectionModel extends AbstractCollectionModel
|
|||
/*
|
||||
constructor() {
|
||||
super();
|
||||
this.CountRec
|
||||
this.IsThreadsSupported
|
||||
this.Namespace;
|
||||
this.Optimized
|
||||
this.SystemFolders
|
||||
this.Capabilities
|
||||
this.quotaUsage;
|
||||
this.quotaLimit;
|
||||
this.namespace;
|
||||
this.optimized
|
||||
this.capabilities
|
||||
}
|
||||
*/
|
||||
|
||||
|
|
@ -119,20 +120,17 @@ export class FolderCollectionModel extends AbstractCollectionModel
|
|||
*/
|
||||
static reviveFromJson(object) {
|
||||
const expandedFolders = Local.get(ClientSideKeyNameExpandedFolders);
|
||||
if (object?.SystemFolders) {
|
||||
forEachObjectEntry(SystemFolders, key =>
|
||||
SystemFolders[key] = SettingsGet(key+'Folder') || object.SystemFolders[FolderType[key]]
|
||||
);
|
||||
}
|
||||
|
||||
forEachObjectEntry(SystemFolders, (key, value) =>
|
||||
value || (SystemFolders[key] = SettingsGet(key+'Folder'))
|
||||
);
|
||||
|
||||
const result = super.reviveFromJson(object, oFolder => {
|
||||
let oCacheFolder = getFolderFromCacheList(oFolder.FullName),
|
||||
type = FolderType[getKeyByValue(SystemFolders, oFolder.FullName)];
|
||||
|
||||
let oCacheFolder = getFolderFromCacheList(oFolder.fullName);
|
||||
if (oCacheFolder) {
|
||||
// oCacheFolder.revivePropertiesFromJson(oFolder);
|
||||
if (oFolder.Hash) {
|
||||
oCacheFolder.hash = oFolder.Hash;
|
||||
if (oFolder.etag) {
|
||||
oCacheFolder.etag = oFolder.etag;
|
||||
}
|
||||
if (null != oFolder.totalEmails) {
|
||||
oCacheFolder.totalEmails(oFolder.totalEmails);
|
||||
|
|
@ -144,17 +142,59 @@ export class FolderCollectionModel extends AbstractCollectionModel
|
|||
oCacheFolder = FolderModel.reviveFromJson(oFolder);
|
||||
if (!oCacheFolder)
|
||||
return null;
|
||||
|
||||
if (1 == type) {
|
||||
oCacheFolder.type(type);
|
||||
setFolderInboxName(oFolder.FullName);
|
||||
}
|
||||
setFolder(oCacheFolder);
|
||||
}
|
||||
|
||||
if (1 < type) {
|
||||
oCacheFolder.type(type);
|
||||
// JMAP RFC 8621
|
||||
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
|
||||
|| !isArray(expandedFolders)
|
||||
|
|
@ -163,12 +203,42 @@ export class FolderCollectionModel extends AbstractCollectionModel
|
|||
return oCacheFolder;
|
||||
});
|
||||
|
||||
result.CountRec = result.length;
|
||||
setFolderInboxName(SystemFolders.Inbox);
|
||||
|
||||
let i = result.length;
|
||||
if (i) {
|
||||
sortFolders(result);
|
||||
try {
|
||||
while (i--) {
|
||||
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) {
|
||||
parent.subFolders.unshift(folder);
|
||||
result.splice(i,1);
|
||||
|
|
@ -188,7 +258,7 @@ export class FolderCollectionModel extends AbstractCollectionModel
|
|||
if (!(
|
||||
SettingsGet('SentFolder') +
|
||||
SettingsGet('DraftsFolder') +
|
||||
SettingsGet('SpamFolder') +
|
||||
SettingsGet('JunkFolder') +
|
||||
SettingsGet('TrashFolder') +
|
||||
SettingsGet('ArchiveFolder')
|
||||
)
|
||||
|
|
@ -198,18 +268,19 @@ export class FolderCollectionModel extends AbstractCollectionModel
|
|||
|
||||
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.quotaLimit(this.quotaLimit);
|
||||
FolderUserStore.capabilities(this.Capabilities);
|
||||
FolderUserStore.capabilities(this.capabilities);
|
||||
|
||||
FolderUserStore.sentFolder(normalizeFolder(SystemFolders.Sent));
|
||||
FolderUserStore.draftsFolder(normalizeFolder(SystemFolders.Drafts));
|
||||
FolderUserStore.spamFolder(normalizeFolder(SystemFolders.Spam));
|
||||
FolderUserStore.spamFolder(normalizeFolder(SystemFolders.Junk));
|
||||
FolderUserStore.trashFolder(normalizeFolder(SystemFolders.Trash));
|
||||
FolderUserStore.archiveFolder(normalizeFolder(SystemFolders.Archive));
|
||||
|
||||
|
|
@ -230,13 +301,14 @@ export class FolderModel extends AbstractModel {
|
|||
|
||||
this.exists = true;
|
||||
|
||||
this.hash = '';
|
||||
// this.id = null;
|
||||
this.uidNext = null;
|
||||
this.etag = '';
|
||||
this.id = 0;
|
||||
this.uidNext = 0;
|
||||
|
||||
addObservablesTo(this, {
|
||||
name: '',
|
||||
type: FolderType.User,
|
||||
type: 0,
|
||||
role: null,
|
||||
selectable: false,
|
||||
|
||||
focused: false,
|
||||
|
|
@ -249,8 +321,8 @@ export class FolderModel extends AbstractModel {
|
|||
nameForEdit: '',
|
||||
errorMsg: '',
|
||||
|
||||
totalEmailsValue: 0,
|
||||
unreadEmailsValue: 0,
|
||||
totalEmails: 0,
|
||||
unreadEmails: 0,
|
||||
|
||||
kolabType: null,
|
||||
|
||||
|
|
@ -259,17 +331,20 @@ export class FolderModel extends AbstractModel {
|
|||
tagsAllowed: false
|
||||
});
|
||||
|
||||
this.flags = ko.observableArray();
|
||||
this.attributes = ko.observableArray();
|
||||
// For messages
|
||||
this.permanentFlags = ko.observableArray();
|
||||
|
||||
this.addSubscribables({
|
||||
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.actionBlink = ko.observable(false).extend({ falseTimeout: 1000 });
|
||||
|
||||
/*
|
||||
this.totalEmails = koComputable({
|
||||
read: this.totalEmailsValue,
|
||||
write: iValue =>
|
||||
|
|
@ -283,6 +358,7 @@ export class FolderModel extends AbstractModel {
|
|||
isPosNumeric(value) ? this.unreadEmailsValue(value) : this.unreadEmailsValue.valueHasMutated()
|
||||
})
|
||||
.extend({ notify: 'always' });
|
||||
*/
|
||||
/*
|
||||
https://www.rfc-editor.org/rfc/rfc8621.html#section-2
|
||||
"myRights": {
|
||||
|
|
@ -297,6 +373,145 @@ export class FolderModel extends AbstractModel {
|
|||
"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);
|
||||
if (folder) {
|
||||
const path = folder.fullName.split(folder.delimiter),
|
||||
attr = name => folder.attributes.includes(name),
|
||||
type = (folder.metadata[FolderMetadataKeys.KolabFolderType]
|
||||
|| folder.metadata[FolderMetadataKeys.KolabFolderTypeShared]
|
||||
|| ''
|
||||
|
|
@ -325,105 +541,11 @@ export class FolderModel extends AbstractModel {
|
|||
path.pop();
|
||||
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);
|
||||
|
||||
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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,12 +6,12 @@ export class IdentityModel extends AbstractModel {
|
|||
* @param {string} id
|
||||
* @param {string} email
|
||||
*/
|
||||
constructor(id, email) {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
addObservablesTo(this, {
|
||||
id: id || '',
|
||||
email: email,
|
||||
id: '',
|
||||
email: '',
|
||||
name: '',
|
||||
|
||||
replyTo: '',
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
import ko from 'ko';
|
||||
|
||||
import { MessagePriority } from 'Common/EnumsUser';
|
||||
import { i18n } from 'Common/Translator';
|
||||
|
||||
import { doc, SettingsGet } from 'Common/Globals';
|
||||
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 { addObservablesTo, addComputablesTo } from 'External/ko';
|
||||
|
||||
|
|
@ -19,17 +18,12 @@ import { AbstractModel } from 'Knoin/AbstractModel';
|
|||
|
||||
import PreviewHTML from 'Html/PreviewMessage.html';
|
||||
|
||||
//import { MessageFlagsCache } from 'Common/Cache';
|
||||
import { LanguageStore } from 'Stores/Language';
|
||||
|
||||
import Remote from 'Remote/User/Fetch';
|
||||
|
||||
const
|
||||
hcont = Element.fromHTML('<div area="hidden" style="position:absolute;left:-5000px"></div>'),
|
||||
getRealHeight = el => {
|
||||
hcont.innerHTML = el.outerHTML;
|
||||
const result = hcont.clientHeight;
|
||||
hcont.innerHTML = '';
|
||||
return result;
|
||||
},
|
||||
msgHtml = msg => cleanHtml(msg.html(), msg.attachments(), '#rl-msg-' + msg.hash),
|
||||
|
||||
toggleTag = (message, keyword) => {
|
||||
const lower = keyword.toLowerCase(),
|
||||
|
|
@ -38,13 +32,12 @@ const
|
|||
Remote.request('MessageSetKeyword', iError => {
|
||||
if (!iError) {
|
||||
isSet ? flags.remove(lower) : flags.push(lower);
|
||||
// MessageFlagsCache.setFor(message.folder, message.uid, flags());
|
||||
}
|
||||
}, {
|
||||
Folder: message.folder,
|
||||
Uids: message.uid,
|
||||
Keyword: keyword,
|
||||
SetAction: isSet ? 0 : 1
|
||||
folder: message.folder,
|
||||
uids: message.uid,
|
||||
keyword: keyword,
|
||||
setAction: isSet ? 0 : 1
|
||||
})
|
||||
},
|
||||
|
||||
|
|
@ -58,13 +51,29 @@ const
|
|||
unic[email.email] || localEmails.has(email.email) || localEmails.set(email.email, email)
|
||||
);
|
||||
|
||||
doc.body.append(hcont);
|
||||
|
||||
export class MessageModel extends AbstractModel {
|
||||
constructor() {
|
||||
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, {
|
||||
subject: '',
|
||||
|
|
@ -75,8 +84,9 @@ export class MessageModel extends AbstractModel {
|
|||
spamResult: '',
|
||||
isSpam: false,
|
||||
hasVirus: null, // or boolean when scanned
|
||||
dateTimeStampInUTC: 0,
|
||||
priority: MessagePriority.Normal,
|
||||
dateTimestamp: 0,
|
||||
internalTimestamp: 0,
|
||||
priority: 3, // Normal
|
||||
|
||||
senderEmailsString: '',
|
||||
senderClearEmailsString: '',
|
||||
|
|
@ -101,35 +111,28 @@ export class MessageModel extends AbstractModel {
|
|||
|
||||
readReceipt: '',
|
||||
|
||||
hasUnseenSubMessage: false,
|
||||
hasFlaggedSubMessage: false
|
||||
// rfc8621
|
||||
id: '',
|
||||
// threadId: ''
|
||||
});
|
||||
|
||||
this.attachments = ko.observableArray(new AttachmentCollectionModel);
|
||||
this.threads = ko.observableArray();
|
||||
this.threadUnseen = ko.observableArray();
|
||||
this.unsubsribeLinks = ko.observableArray();
|
||||
this.flags = ko.observableArray();
|
||||
|
||||
addComputablesTo(this, {
|
||||
attachmentIconClass: () =>
|
||||
this.encrypted() ? 'icon-lock' : FileInfo.getAttachmentsIconClass(this.attachments()),
|
||||
threadsLen: () => this.threads().length,
|
||||
listAttachments: () => this.attachments()
|
||||
.filter(item => SettingsUserStore.listInlineAttachments() || !item.isLinked()),
|
||||
hasAttachments: () => this.listAttachments().length,
|
||||
threadsLen: () => rl.app.messageList.threadUid() ? 0 : this.threads().length,
|
||||
threadUnseenLen: () => rl.app.messageList.threadUid() ? 0 : this.threadUnseen().length,
|
||||
|
||||
isUnseen: () => !this.flags().includes('\\seen'),
|
||||
isFlagged: () => this.flags().includes('\\flagged'),
|
||||
isReadReceipt: () => this.flags().includes('$mdnsent'),
|
||||
// isJunk: () => this.flags().includes('$junk') && !this.flags().includes('$nonjunk'),
|
||||
// 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: () => {
|
||||
const tagOptions = [];
|
||||
FolderUserStore.currentFolder().permanentFlags.forEach(value => {
|
||||
|
|
@ -145,8 +148,41 @@ export class MessageModel extends AbstractModel {
|
|||
}
|
||||
});
|
||||
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);
|
||||
}
|
||||
|
||||
_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() {
|
||||
let spam = this.spamResult();
|
||||
return spam ? i18n(this.isSpam() ? 'GLOBAL/SPAM' : 'GLOBAL/NOT_SPAM') + ': ' + spam : '';
|
||||
|
|
@ -240,84 +215,15 @@ export class MessageModel extends AbstractModel {
|
|||
* @returns {boolean}
|
||||
*/
|
||||
revivePropertiesFromJson(json) {
|
||||
if ('Priority' in json && ![MessagePriority.High, MessagePriority.Low].includes(json.Priority)) {
|
||||
json.Priority = MessagePriority.Normal;
|
||||
}
|
||||
if (super.revivePropertiesFromJson(json)) {
|
||||
// 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();
|
||||
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
|
||||
*/
|
||||
|
|
@ -329,20 +235,22 @@ export class MessageModel extends AbstractModel {
|
|||
checked: this.checked(),
|
||||
unseen: this.isUnseen(),
|
||||
focused: this.focused(),
|
||||
priorityHigh: this.priority() === MessagePriority.High,
|
||||
priorityHigh: this.priority() === 1,
|
||||
withAttachments: !!this.attachments().length,
|
||||
// hasChildrenMessage: 1 < this.threadsLen(),
|
||||
hasUnseenSubMessage: this.hasUnseenSubMessage(),
|
||||
hasFlaggedSubMessage: this.hasFlaggedSubMessage()
|
||||
// hasChildrenMessage: 1 < this.threadsLen()
|
||||
}, (key, value) => value && classes.push(key));
|
||||
flags && this.flags().forEach(value => classes.push('msgflag-'+value));
|
||||
return classes.join(' ');
|
||||
}
|
||||
|
||||
indent() {
|
||||
return this.level ? 'margin-left:'+this.level+'em' : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {string}
|
||||
*/
|
||||
viewLink() {
|
||||
viewRaw() {
|
||||
return serverRequestRaw('ViewAsPlain', this.requestHash);
|
||||
}
|
||||
|
||||
|
|
@ -386,83 +294,77 @@ export class MessageModel extends AbstractModel {
|
|||
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();
|
||||
if (body) {
|
||||
if (html) {
|
||||
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(
|
||||
(this.plain()
|
||||
? this.plain()
|
||||
.replace(/-----BEGIN PGP (SIGNED MESSAGE-----(\r?\n[^\r\n]+)+|SIGNATURE-----[\s\S]*)/sg, '')
|
||||
.trim()
|
||||
: htmlToPlain(body.innerHTML)
|
||||
)
|
||||
);
|
||||
this.hasImages(false);
|
||||
}
|
||||
|
||||
this.isHtml(true);
|
||||
this.initView();
|
||||
body.classList.toggle('html', html);
|
||||
body.classList.toggle('plain', !html);
|
||||
this.isHtml(html);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
viewHtml() {
|
||||
return this.html() && this.viewBody(true);
|
||||
}
|
||||
|
||||
viewPlain() {
|
||||
const body = this.body;
|
||||
if (body) {
|
||||
body.classList.toggle('html', 0);
|
||||
body.classList.toggle('plain', 1);
|
||||
body.innerHTML = plainToHtml(
|
||||
(this.plain()
|
||||
? this.plain()
|
||||
.replace(/-----BEGIN PGP (SIGNED MESSAGE-----(\r?\n[a-z][^\r\n]+)+|SIGNATURE-----[\s\S]*)/, '')
|
||||
.trim()
|
||||
: htmlToPlain(body.innerHTML)
|
||||
)
|
||||
);
|
||||
this.isHtml(false);
|
||||
this.hasImages(false);
|
||||
this.initView();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
initView() {
|
||||
// init BlockquoteSwitcher
|
||||
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);
|
||||
}
|
||||
}
|
||||
});
|
||||
return this.viewBody(false);
|
||||
}
|
||||
|
||||
viewPopupMessage(print) {
|
||||
const timeStampInUTC = this.dateTimeStampInUTC() || 0,
|
||||
ccLine = this.ccToLine(),
|
||||
const
|
||||
timeStampInUTC = this.dateTimestamp() || 0,
|
||||
ccLine = this.cc.toString(),
|
||||
bccLine = this.bcc.toString(),
|
||||
m = 0 < timeStampInUTC ? new Date(timeStampInUTC * 1000) : null,
|
||||
win = open(''),
|
||||
sdoc = win.document;
|
||||
let subject = encodeHtml(this.subject()),
|
||||
win = open('', 'sm-msg-'+this.requestHash
|
||||
/*,newWindow ? 'innerWidth=' + elementById('V-MailMessageView').clientWidth : ''*/
|
||||
),
|
||||
sdoc = win.document,
|
||||
subject = encodeHtml(this.subject()),
|
||||
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')),
|
||||
prop = property => style.getPropertyValue(property);
|
||||
let attachments = '';
|
||||
this.attachments.forEach(attachment => {
|
||||
attachments += `<a href="${attachment.linkDownload()}">${attachment.fileName}</a>`;
|
||||
});
|
||||
sdoc.write(PreviewHTML
|
||||
.replace('<title>', '<title>'+subject)
|
||||
// eslint-disable-next-line max-len
|
||||
.replace('<body>', `<body style="background-color:${prop('background-color')};color:${prop('color')}"><header><h1>${subject}</h1><time>${encodeHtml(m ? m.format('LLL') : '')}</time><div>${encodeHtml(this.fromToLine())}</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();
|
||||
|
||||
if (print) {
|
||||
setTimeout(() => win.print(), 100);
|
||||
}
|
||||
print && setTimeout(() => win.print(), 100);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -484,75 +386,72 @@ export class MessageModel extends AbstractModel {
|
|||
}
|
||||
|
||||
/**
|
||||
* @param {MessageModel} message
|
||||
* @returns {MessageModel}
|
||||
*/
|
||||
static fromMessageListItem(message) {
|
||||
*//*
|
||||
clone() {
|
||||
let self = new MessageModel();
|
||||
|
||||
if (message) {
|
||||
self.folder = message.folder;
|
||||
self.uid = message.uid;
|
||||
self.hash = message.hash;
|
||||
self.requestHash = message.requestHash;
|
||||
self.subject(message.subject());
|
||||
self.plain(message.plain());
|
||||
self.html(message.html());
|
||||
|
||||
self.size(message.size());
|
||||
self.spamScore(message.spamScore());
|
||||
self.spamResult(message.spamResult());
|
||||
self.isSpam(message.isSpam());
|
||||
self.hasVirus(message.hasVirus());
|
||||
self.dateTimeStampInUTC(message.dateTimeStampInUTC());
|
||||
self.priority(message.priority());
|
||||
|
||||
self.hasExternals(message.hasExternals());
|
||||
|
||||
self.emails = message.emails;
|
||||
|
||||
self.from = message.from;
|
||||
self.to = message.to;
|
||||
self.cc = message.cc;
|
||||
self.bcc = message.bcc;
|
||||
self.replyTo = message.replyTo;
|
||||
self.deliveredTo = message.deliveredTo;
|
||||
self.unsubsribeLinks(message.unsubsribeLinks);
|
||||
|
||||
self.flags(message.flags());
|
||||
|
||||
self.priority(message.priority());
|
||||
|
||||
self.selected(message.selected());
|
||||
self.checked(message.checked());
|
||||
self.attachments(message.attachments());
|
||||
|
||||
self.threads(message.threads());
|
||||
}
|
||||
|
||||
// Clone message values
|
||||
forEachObjectEntry(this, (key, value) => {
|
||||
if (ko.isObservable(value)) {
|
||||
ko.isComputed(value) || self[key](value());
|
||||
} else if (!isFunction(value)) {
|
||||
self[key] = value;
|
||||
}
|
||||
});
|
||||
self.computeSenderEmail();
|
||||
|
||||
return self;
|
||||
}
|
||||
}*/
|
||||
|
||||
showExternalImages() {
|
||||
showExternalImages(regex) {
|
||||
const body = this.body;
|
||||
if (body && this.hasImages()) {
|
||||
this.hasImages(false);
|
||||
body.rlHasImages = false;
|
||||
|
||||
let attr = 'data-x-src',
|
||||
src, useProxy = !!SettingsGet('UseLocalProxyForExternalImages');
|
||||
if (regex) {
|
||||
regex = [];
|
||||
SettingsUserStore.viewImagesWhitelist().trim().split(/[\s\r\n,;]+/g).forEach(rule => {
|
||||
rule = rule.split('+');
|
||||
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 => {
|
||||
src = node.getAttribute(attr);
|
||||
node.src = useProxy ? proxy(src) : src;
|
||||
if (isValid(src)) {
|
||||
node.src = useProxy ? proxy(src) : src;
|
||||
}
|
||||
});
|
||||
|
||||
body.querySelectorAll('[data-x-style-url]').forEach(node => {
|
||||
JSON.parse(node.dataset.xStyleUrl).forEach(data =>
|
||||
node.style[data[0]] = "url('" + (useProxy ? proxy(data[1]) : data[1]) + "')"
|
||||
);
|
||||
JSON.parse(node.dataset.xStyleUrl).forEach(data => {
|
||||
if (isValid(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;
|
||||
}
|
||||
let result = cleanHtml(this.html(), this.attachments())
|
||||
let result = msgHtml(this);
|
||||
return result.html || plainToHtml(this.plain());
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,6 @@
|
|||
import { AbstractCollectionModel } from 'Model/AbstractCollection';
|
||||
import { MessageModel } from 'Model/Message';
|
||||
|
||||
import {
|
||||
MessageFlagsCache
|
||||
} from 'Common/Cache';
|
||||
import { MessageUserStore } from 'Stores/User/Message';
|
||||
|
||||
'use strict';
|
||||
|
||||
|
|
@ -12,19 +9,16 @@ export class MessageCollectionModel extends AbstractCollectionModel
|
|||
/*
|
||||
constructor() {
|
||||
super();
|
||||
this.Filtered
|
||||
this.Folder
|
||||
this.FolderHash
|
||||
this.FolderInfo
|
||||
this.filtered
|
||||
this.folder
|
||||
this.totalEmails
|
||||
this.unreadEmails
|
||||
this.MessageResultCount
|
||||
this.UidNext
|
||||
this.ThreadUid
|
||||
this.NewMessages
|
||||
this.Offset
|
||||
this.Limit
|
||||
this.Search
|
||||
this.totalThreads
|
||||
this.threadUid
|
||||
this.newMessages
|
||||
this.offset
|
||||
this.limit
|
||||
this.search
|
||||
this.limited
|
||||
}
|
||||
*/
|
||||
|
||||
|
|
@ -32,12 +26,14 @@ export class MessageCollectionModel extends AbstractCollectionModel
|
|||
* @param {?Object} json
|
||||
* @returns {MessageCollectionModel}
|
||||
*/
|
||||
static reviveFromJson(object, cached) {
|
||||
static reviveFromJson(object/*, cached*/) {
|
||||
let msg = MessageUserStore.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) {
|
||||
message.deleted(false);
|
||||
cached ? MessageFlagsCache.initMessage(message) : MessageFlagsCache.store(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 { serverRequest } from 'Common/Links';
|
||||
import { getNotification } from 'Common/Translator';
|
||||
|
|
@ -9,17 +9,18 @@ const getURL = (add = '') => serverRequest('Json') + pString(add),
|
|||
|
||||
checkResponseError = data => {
|
||||
const err = data ? data.ErrorCode : null;
|
||||
if (Notification.InvalidToken === err) {
|
||||
alert(getNotification(err));
|
||||
if (Notifications.InvalidToken === err) {
|
||||
console.error(getNotification(err));
|
||||
// alert(getNotification(err));
|
||||
rl.logoutReload();
|
||||
} else if ([
|
||||
Notification.AuthError,
|
||||
Notification.ConnectionError,
|
||||
Notification.DomainNotAllowed,
|
||||
Notification.AccountNotAllowed,
|
||||
Notification.MailServerError,
|
||||
Notification.UnknownNotification,
|
||||
Notification.UnknownError
|
||||
Notifications.AuthError,
|
||||
Notifications.ConnectionError,
|
||||
Notifications.DomainNotAllowed,
|
||||
Notifications.AccountNotAllowed,
|
||||
Notifications.MailServerError,
|
||||
Notifications.UnknownNotification,
|
||||
Notifications.UnknownError
|
||||
].includes(err)
|
||||
) {
|
||||
if (7 < ++iJsonErrorCount) {
|
||||
|
|
@ -31,32 +32,34 @@ checkResponseError = data => {
|
|||
oRequests = {},
|
||||
|
||||
abort = (sAction, sReason, bClearOnly) => {
|
||||
if (oRequests[sAction]) {
|
||||
if (!bClearOnly && oRequests[sAction].abort) {
|
||||
// oRequests[sAction].__aborted = true;
|
||||
oRequests[sAction].abort(sReason || 'AbortError');
|
||||
}
|
||||
|
||||
oRequests[sAction] = null;
|
||||
delete oRequests[sAction];
|
||||
let controller = oRequests[sAction];
|
||||
oRequests[sAction] = null;
|
||||
if (controller) {
|
||||
clearTimeout(controller.timeoutId);
|
||||
bClearOnly || controller.abort(sReason || 'AbortError');
|
||||
}
|
||||
},
|
||||
|
||||
fetchJSON = (action, sGetAdd, params, timeout, jsonCallback) => {
|
||||
params = params || {};
|
||||
if (params instanceof FormData) {
|
||||
params.set('Action', action);
|
||||
} else {
|
||||
params.Action = action;
|
||||
fetchJSON = (action, sUrl, params, timeout, jsonCallback) => {
|
||||
if (params) {
|
||||
if (params instanceof FormData) {
|
||||
params.set('Action', action);
|
||||
} else {
|
||||
params.Action = action;
|
||||
}
|
||||
}
|
||||
// Don't abort, read https://github.com/the-djmaze/snappymail/issues/487
|
||||
// abort(action);
|
||||
// abort(action, 0, 1);
|
||||
const controller = new AbortController(),
|
||||
signal = controller.signal;
|
||||
oRequests[action] = controller;
|
||||
// Currently there is no way to combine multiple signals, so AbortSignal.timeout() not possible
|
||||
timeout && setTimeout(() => abort(action, 'TimeoutError'), timeout);
|
||||
return rl.fetchJSON(getURL(sGetAdd), {signal: signal}, sGetAdd ? null : params).then(jsonCallback).catch(err => {
|
||||
controller.timeoutId = timeout && setTimeout(() => abort(action, 'TimeoutError'), timeout);
|
||||
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.reason = signal.reason;
|
||||
return Promise.reject(err);
|
||||
|
|
@ -67,14 +70,14 @@ class FetchError extends Error
|
|||
{
|
||||
constructor(code, message) {
|
||||
super(message);
|
||||
this.code = code || Notification.JsonFalse;
|
||||
this.code = code || Notifications.JsonFalse;
|
||||
}
|
||||
}
|
||||
|
||||
export class AbstractFetchRemote
|
||||
{
|
||||
abort(sAction) {
|
||||
abort(sAction);
|
||||
abort(sAction, sReason) {
|
||||
abort(sAction, sReason);
|
||||
return this;
|
||||
}
|
||||
|
||||
|
|
@ -123,31 +126,17 @@ export class AbstractFetchRemote
|
|||
* @param {?number=} iTimeout
|
||||
* @param {string=} sGetAdd = ''
|
||||
*/
|
||||
request(sAction, fCallback, params, iTimeout, sGetAdd, abortActions) {
|
||||
request(sAction, fCallback, params, iTimeout, sGetAdd) {
|
||||
params = params || {};
|
||||
|
||||
const start = Date.now();
|
||||
|
||||
abortActions && console.error('abortActions is obsolete');
|
||||
|
||||
fetchJSON(sAction, sGetAdd,
|
||||
params,
|
||||
fetchJSON(sAction, getURL(sGetAdd),
|
||||
sGetAdd ? null : (params || {}),
|
||||
undefined === iTimeout ? 30000 : pInt(iTimeout),
|
||||
data => {
|
||||
let cached = false;
|
||||
if (data?.Time) {
|
||||
cached = pInt(data.Time) > Date.now() - start;
|
||||
}
|
||||
|
||||
let iError = 0;
|
||||
if (sAction && oRequests[sAction]) {
|
||||
if (oRequests[sAction].__aborted) {
|
||||
iError = 2;
|
||||
}
|
||||
abort(sAction, 0, 1);
|
||||
}
|
||||
|
||||
if (!iError && data) {
|
||||
if (data) {
|
||||
/*
|
||||
if (sAction !== data.Action) {
|
||||
console.log(sAction + ' !== ' + data.Action);
|
||||
|
|
@ -157,16 +146,19 @@ export class AbstractFetchRemote
|
|||
iJsonErrorCount = 0;
|
||||
} else {
|
||||
checkResponseError(data);
|
||||
iError = data.ErrorCode || Notification.UnknownError
|
||||
iError = data.ErrorCode || Notifications.UnknownError
|
||||
}
|
||||
}
|
||||
|
||||
fCallback && fCallback(
|
||||
iError,
|
||||
data,
|
||||
cached,
|
||||
sAction,
|
||||
params
|
||||
/**
|
||||
* Responses like "304 Not Modified" are returned as "200 OK"
|
||||
* 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) {
|
||||
this.setTrigger(fTrigger, true);
|
||||
return fetchJSON(action, '', params, pInt(timeOut, 30000),
|
||||
return fetchJSON(action, getURL(), params || {}, pInt(timeOut, 30000),
|
||||
data => {
|
||||
abort(action, 0, 1);
|
||||
|
||||
if (!data) {
|
||||
return Promise.reject(new FetchError(Notification.JsonParse));
|
||||
return Promise.reject(new FetchError(Notifications.JsonParse));
|
||||
}
|
||||
/*
|
||||
let isCached = false, type = '';
|
||||
if (data?.Time) {
|
||||
isCached = pInt(data.Time) > microtime() - start;
|
||||
if (data?.epoch) {
|
||||
isCached = data.epoch > microtime() - start;
|
||||
}
|
||||
// backward capability
|
||||
switch (true) {
|
||||
|
|
|
|||
|
|
@ -9,57 +9,11 @@ import { SUB_QUERY_PREFIX } from 'Common/Links';
|
|||
|
||||
import { AppUserStore } from 'Stores/User/App';
|
||||
import { SettingsUserStore } from 'Stores/User/Settings';
|
||||
import { FolderUserStore } from 'Stores/User/Folder';
|
||||
|
||||
import { AbstractFetchRemote } from 'Remote/AbstractFetch';
|
||||
|
||||
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 {string} sFolderFullName
|
||||
|
|
@ -82,7 +36,7 @@ class RemoteUserFetch extends AbstractFetchRemote {
|
|||
sFolderFullName,
|
||||
iUid,
|
||||
AppUserStore.threadsAllowed() && SettingsUserStore.useThreads() ? 1 : 0,
|
||||
SettingsGet('AccountHash')
|
||||
SettingsGet('accountHash')
|
||||
])
|
||||
);
|
||||
|
||||
|
|
@ -114,9 +68,9 @@ class RemoteUserFetch extends AbstractFetchRemote {
|
|||
/*
|
||||
folderMove(sPrevFolderFullName, sNewFolderFullName, bSubscribe) {
|
||||
return this.post('FolderMove', FolderUserStore.foldersRenaming, {
|
||||
Folder: sPrevFolderFullName,
|
||||
NewFolder: sNewFolderFullName,
|
||||
Subscribe: bSubscribe ? 1 : 0
|
||||
folder: sPrevFolderFullName,
|
||||
newFolder: sNewFolderFullName,
|
||||
subscribe: bSubscribe ? 1 : 0
|
||||
});
|
||||
}
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import ko from 'ko';
|
|||
|
||||
import { pString } from 'Common/Utils';
|
||||
import { settings } from 'Common/Links';
|
||||
import { createElement, elementById } from 'Common/Globals';
|
||||
import { createElement/*, elementById*/, fireEvent } from 'Common/Globals';
|
||||
|
||||
import { AbstractScreen } from 'Knoin/AbstractScreen';
|
||||
import { i18nToNodes } from 'Common/Translator';
|
||||
|
|
@ -25,35 +25,43 @@ export class AbstractSettingsScreen extends AbstractScreen {
|
|||
let settingsScreen = null,
|
||||
viewModelDom = null,
|
||||
RoutedSettingsViewModel = VIEW_MODELS.find(
|
||||
SettingsViewModel => subName === SettingsViewModel.__rlSettingsData.route
|
||||
SettingsViewModel => subName === SettingsViewModel.route
|
||||
);
|
||||
|
||||
if (RoutedSettingsViewModel) {
|
||||
const vmPlace = elementById('rl-settings-subscreen');
|
||||
if (RoutedSettingsViewModel.__vm) {
|
||||
settingsScreen = RoutedSettingsViewModel.__vm;
|
||||
// const vmPlace = elementById('V-SettingsPane') || elementById('V-AdminPane);
|
||||
const vmPlace = this.viewModels[1].__dom,
|
||||
SettingsViewModelClass = RoutedSettingsViewModel.vmc;
|
||||
if (SettingsViewModelClass.__vm) {
|
||||
settingsScreen = SettingsViewModelClass.__vm;
|
||||
viewModelDom = settingsScreen.viewModelDom;
|
||||
} else if (vmPlace) {
|
||||
viewModelDom = createElement('div',{
|
||||
id: 'V-Settings-' + RoutedSettingsViewModel.name.replace(/(User|Admin)Settings/,''),
|
||||
id: 'V-Settings-' + SettingsViewModelClass.name.replace(/(User|Admin)Settings/,''),
|
||||
hidden: ''
|
||||
})
|
||||
vmPlace.append(viewModelDom);
|
||||
|
||||
settingsScreen = new RoutedSettingsViewModel();
|
||||
settingsScreen = new SettingsViewModelClass();
|
||||
settingsScreen.viewModelDom = viewModelDom;
|
||||
settingsScreen.viewModelTemplateID = RoutedSettingsViewModel.template;
|
||||
|
||||
RoutedSettingsViewModel.__dom = viewModelDom;
|
||||
RoutedSettingsViewModel.__vm = settingsScreen;
|
||||
SettingsViewModelClass.__dom = viewModelDom;
|
||||
SettingsViewModelClass.__vm = settingsScreen;
|
||||
|
||||
fireEvent('rl-view-model.create', settingsScreen);
|
||||
|
||||
ko.applyBindingAccessorsToNode(
|
||||
viewModelDom,
|
||||
{
|
||||
template: () => ({ name: RoutedSettingsViewModel.__rlSettingsData.template })
|
||||
template: () => ({ name: RoutedSettingsViewModel.template })
|
||||
},
|
||||
settingsScreen
|
||||
);
|
||||
|
||||
settingsScreen.onBuild?.(viewModelDom);
|
||||
|
||||
fireEvent('rl-view-model', settingsScreen);
|
||||
} else {
|
||||
console.log('Cannot find sub settings view model position: SettingsSubScreen');
|
||||
}
|
||||
|
|
@ -68,13 +76,13 @@ export class AbstractSettingsScreen extends AbstractScreen {
|
|||
|
||||
// show
|
||||
settingsScreen.beforeShow?.();
|
||||
i18nToNodes(settingsScreen.viewModelDom);
|
||||
settingsScreen.viewModelDom.hidden = false;
|
||||
i18nToNodes(viewModelDom);
|
||||
viewModelDom.hidden = false;
|
||||
settingsScreen.onShow?.();
|
||||
|
||||
this.menu.forEach(item => {
|
||||
item.selected(
|
||||
item.route === RoutedSettingsViewModel.__rlSettingsData.route
|
||||
item.route === RoutedSettingsViewModel.route
|
||||
);
|
||||
});
|
||||
|
||||
|
|
@ -96,15 +104,17 @@ export class AbstractSettingsScreen extends AbstractScreen {
|
|||
}
|
||||
|
||||
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() {
|
||||
const DefaultViewModel = VIEW_MODELS.find(
|
||||
SettingsViewModel => SettingsViewModel.__rlSettingsData.isDefault
|
||||
SettingsViewModel => SettingsViewModel.isDefault
|
||||
),
|
||||
defaultRoute =
|
||||
DefaultViewModel ? DefaultViewModel.__rlSettingsData.route : 'general',
|
||||
defaultRoute = DefaultViewModel?.route || 'general',
|
||||
rules = {
|
||||
subname: /^(.*)$/,
|
||||
normalize_: (rquest, vals) => {
|
||||
|
|
@ -131,13 +141,12 @@ export class AbstractSettingsScreen extends AbstractScreen {
|
|||
*/
|
||||
export function settingsAddViewModel(SettingsViewModelClass, template, labelName, route, isDefault = false) {
|
||||
let name = SettingsViewModelClass.name.replace(/(User|Admin)Settings/, '');
|
||||
SettingsViewModelClass.__rlSettingsData = {
|
||||
label: labelName || 'SETTINGS_LABELS/LABEL_' + name.toUpperCase() + '_NAME',
|
||||
VIEW_MODELS.push({
|
||||
vmc: SettingsViewModelClass,
|
||||
label: labelName || 'SETTINGS_LABELS/' + name.toUpperCase(),
|
||||
route: route || name.toLowerCase(),
|
||||
selected: ko.observable(false),
|
||||
template: template || SettingsViewModelClass.name,
|
||||
isDefault: !!isDefault
|
||||
};
|
||||
|
||||
VIEW_MODELS.push(SettingsViewModelClass);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,6 @@ export class LoginAdminScreen extends AbstractScreen {
|
|||
}
|
||||
|
||||
onShow() {
|
||||
rl.setWindowTitle();
|
||||
rl.setTitle();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,6 +37,6 @@ export class SettingsAdminScreen extends AbstractSettingsScreen {
|
|||
}
|
||||
|
||||
onShow() {
|
||||
rl.setWindowTitle();
|
||||
rl.setTitle();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,6 @@ export class LoginUserScreen extends AbstractScreen {
|
|||
}
|
||||
|
||||
onShow() {
|
||||
rl.setWindowTitle();
|
||||
rl.setTitle();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,17 +1,14 @@
|
|||
import { Scope } from 'Common/Enums';
|
||||
import { Layout, ClientSideKeyNameMessageListSize } from 'Common/EnumsUser';
|
||||
import { doc, createElement, leftPanelDisabled, moveAction, Settings, elementById } from 'Common/Globals';
|
||||
import { ScopeMessageList } from 'Common/Enums';
|
||||
import { doc, createElement, Settings } from 'Common/Globals';
|
||||
import { pString, pInt } from 'Common/Utils';
|
||||
import { setLayoutResizer } from 'Common/UtilsUser';
|
||||
import { getFolderFromCacheList, getFolderFullName, getFolderInboxName } from 'Common/Cache';
|
||||
import { moveAction } from 'Common/UtilsUser';
|
||||
import { getFolderFromHashMap, getFolderInboxName } from 'Common/Cache';
|
||||
import { i18n, initOnStartOrLangChange } from 'Common/Translator';
|
||||
import { SettingsUserStore } from 'Stores/User/Settings';
|
||||
|
||||
import { AppUserStore } from 'Stores/User/App';
|
||||
import { AccountUserStore } from 'Stores/User/Account';
|
||||
import { FolderUserStore } from 'Stores/User/Folder';
|
||||
import { MessagelistUserStore } from 'Stores/User/Messagelist';
|
||||
import { ThemeStore } from 'Stores/Theme';
|
||||
|
||||
import { SystemDropDownUserView } from 'View/User/SystemDropDown';
|
||||
import { MailFolderList } from 'View/User/MailBox/FolderList';
|
||||
|
|
@ -46,7 +43,7 @@ export class MailBoxUserScreen extends AbstractScreen {
|
|||
const count = Settings.app('listPermanentFiltered') ? 0 : FolderUserStore.foldersInboxUnreadCount(),
|
||||
email = AccountUserStore.email();
|
||||
|
||||
rl.setWindowTitle(
|
||||
rl.setTitle(
|
||||
(email
|
||||
? '' + (0 < count ? '(' + count + ') ' : ' ') + email + ' - '
|
||||
: ''
|
||||
|
|
@ -59,11 +56,8 @@ export class MailBoxUserScreen extends AbstractScreen {
|
|||
*/
|
||||
onShow() {
|
||||
this.updateWindowTitle();
|
||||
|
||||
AppUserStore.focusedState('none');
|
||||
AppUserStore.focusedState(Scope.MessageList);
|
||||
|
||||
ThemeStore.isMobile() && leftPanelDisabled(true);
|
||||
AppUserStore.focusedState(ScopeMessageList);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -73,7 +67,7 @@ export class MailBoxUserScreen extends AbstractScreen {
|
|||
* @returns {void}
|
||||
*/
|
||||
onRoute(folderHash, page, search, messageUid) {
|
||||
const folder = getFolderFromCacheList(getFolderFullName(folderHash.replace(/~([\d]+)$/, '')));
|
||||
const folder = getFolderFromHashMap(folderHash.replace(/~([\d]+)$/, ''));
|
||||
if (folder) {
|
||||
FolderUserStore.currentFolder(folder);
|
||||
MessagelistUserStore.page(1 > page ? 1 : page);
|
||||
|
|
@ -101,7 +95,7 @@ export class MailBoxUserScreen extends AbstractScreen {
|
|||
FolderUserStore.foldersInboxUnreadCount(e.detail);
|
||||
/* // Disabled in SystemDropDown.html
|
||||
const email = AccountUserStore.email();
|
||||
AccountUserStore.accounts.forEach(item =>
|
||||
AccountUserStore.forEach(item =>
|
||||
email === item?.email && item?.count(e.detail)
|
||||
);
|
||||
*/
|
||||
|
|
@ -113,24 +107,6 @@ export class MailBoxUserScreen extends AbstractScreen {
|
|||
* @returns {void}
|
||||
*/
|
||||
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 =>
|
||||
event.target.closest('#rl-right') && moveAction(false)
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
import { Scope } from 'Common/Enums';
|
||||
import { keyScope, leftPanelDisabled, SettingsCapa } from 'Common/Globals';
|
||||
import { ScopeSettings } from 'Common/Enums';
|
||||
import { keyScope, SettingsCapa } from 'Common/Globals';
|
||||
import { runSettingsViewModelHooks } from 'Common/Plugins';
|
||||
import { initOnStartOrLangChange, i18n } from 'Common/Translator';
|
||||
|
||||
import { AppUserStore } from 'Stores/User/App';
|
||||
import { AccountUserStore } from 'Stores/User/Account';
|
||||
import { ThemeStore } from 'Stores/Theme';
|
||||
|
||||
import { AbstractSettingsScreen, settingsAddViewModel } from 'Screen/AbstractSettings';
|
||||
|
||||
|
|
@ -23,7 +22,7 @@ import { SettingsPaneUserView } from 'View/User/Settings/Pane';
|
|||
|
||||
export class SettingsUserScreen extends AbstractSettingsScreen {
|
||||
constructor() {
|
||||
super([SystemDropDownUserView, SettingsMenuUserView, SettingsPaneUserView]);
|
||||
super([SettingsMenuUserView, SettingsPaneUserView, SystemDropDownUserView]);
|
||||
|
||||
const views = [
|
||||
UserSettingsGeneral
|
||||
|
|
@ -37,6 +36,8 @@ export class SettingsUserScreen extends AbstractSettingsScreen {
|
|||
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')) {
|
||||
views.push(UserSettingsFilters);
|
||||
}
|
||||
|
|
@ -66,12 +67,11 @@ export class SettingsUserScreen extends AbstractSettingsScreen {
|
|||
|
||||
onShow() {
|
||||
this.setSettingsTitle();
|
||||
keyScope(Scope.Settings);
|
||||
ThemeStore.isMobile() && leftPanelDisabled(true);
|
||||
keyScope(ScopeSettings);
|
||||
}
|
||||
|
||||
setSettingsTitle() {
|
||||
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,
|
||||
coreVersion: '',
|
||||
coreVersionCompare: -2,
|
||||
load1: 0,
|
||||
load5: 0,
|
||||
load15: 0,
|
||||
errorDesc: ''
|
||||
});
|
||||
this.coreChecking = ko.observable(false).extend({ throttle: 100 });
|
||||
|
|
@ -51,18 +54,21 @@ export class AdminSettingsAbout /*extends AbstractViewSettings*/ {
|
|||
}
|
||||
|
||||
onBuild() {
|
||||
Remote.request('AdminPHPExtensions', (iError, data) => iError || this.phpextensions(data.Result));
|
||||
|
||||
// beforeShow() {
|
||||
this.coreChecking(true);
|
||||
Remote.request('AdminUpdateInfo', (iError, data) => {
|
||||
Remote.request('AdminInfo', (iError, data) => {
|
||||
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.coreUpdatable(!!data.Result.Updatable);
|
||||
this.coreWarning(!!data.Result.Warning);
|
||||
this.coreVersion(data.Result.Version || '');
|
||||
this.coreVersionCompare(data.Result.VersionCompare);
|
||||
this.coreUpdatable(!!data.core.updatable);
|
||||
this.coreWarning(!!data.core.warning);
|
||||
this.coreVersion(data.core.version || '');
|
||||
this.coreVersionCompare(data.core.versionCompare);
|
||||
} else {
|
||||
this.coreReal(false);
|
||||
this.coreWarning(false);
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@ import { AbstractViewSettings } from 'Knoin/AbstractViews';
|
|||
export class AdminSettingsBranding extends AbstractViewSettings {
|
||||
constructor() {
|
||||
super();
|
||||
this.addSetting('Title');
|
||||
this.addSetting('LoadingDescription');
|
||||
this.addSetting('FaviconUrl');
|
||||
this.addSetting('title');
|
||||
this.addSetting('loadingDescription');
|
||||
this.addSetting('faviconUrl');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,16 +13,20 @@ export class AdminSettingsContacts extends AbstractViewSettings {
|
|||
super();
|
||||
this.defaultOptionsAfterRender = defaultOptionsAfterRender;
|
||||
|
||||
this.addSetting('ContactsPdoDsn');
|
||||
this.addSetting('ContactsPdoUser');
|
||||
this.addSetting('ContactsPdoPassword');
|
||||
this.addSetting('ContactsPdoType', () => {
|
||||
this.addSetting('contactsPdoDsn');
|
||||
this.addSetting('contactsPdoUser');
|
||||
this.addSetting('contactsPdoPassword');
|
||||
this.addSetting('contactsPdoType', () => {
|
||||
this.testContactsSuccess(false);
|
||||
this.testContactsError(false);
|
||||
this.testContactsErrorMessage('');
|
||||
});
|
||||
|
||||
this.addSettings(['ContactsEnable','ContactsSync']);
|
||||
this.addSettings(['contactsEnable','contactsSync']);
|
||||
|
||||
this.addSetting('contactsMySQLSSLCA');
|
||||
this.addSetting('contactsMySQLSSLVerify');
|
||||
this.addSetting('contactsMySQLSSLCiphers');
|
||||
|
||||
addObservablesTo(this, {
|
||||
testing: false,
|
||||
|
|
@ -31,6 +35,8 @@ export class AdminSettingsContacts extends AbstractViewSettings {
|
|||
testContactsErrorMessage: ''
|
||||
});
|
||||
|
||||
this.addSetting('contactsSuggestionsLimit');
|
||||
|
||||
const supportedTypes = SettingsGet('supportedPdoDrivers') || [],
|
||||
types = [{
|
||||
id:'sqlite',
|
||||
|
|
@ -90,10 +96,13 @@ export class AdminSettingsContacts extends AbstractViewSettings {
|
|||
|
||||
this.testing(false);
|
||||
}, {
|
||||
ContactsPdoType: this.contactsPdoType(),
|
||||
ContactsPdoDsn: this.contactsPdoDsn(),
|
||||
ContactsPdoUser: this.contactsPdoUser(),
|
||||
ContactsPdoPassword: this.contactsPdoPassword()
|
||||
PdoType: this.contactsPdoType(),
|
||||
PdoDsn: this.contactsPdoDsn(),
|
||||
PdoUser: this.contactsPdoUser(),
|
||||
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*/ {
|
||||
constructor() {
|
||||
this.domains = DomainAdminStore;
|
||||
|
||||
this.username = ko.observable('');
|
||||
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() {
|
||||
showScreenPopup(DomainPopupView);
|
||||
}
|
||||
|
|
@ -26,15 +41,15 @@ export class AdminSettingsDomains /*extends AbstractViewSettings*/ {
|
|||
deleteDomain(domain) {
|
||||
DomainAdminStore.remove(domain);
|
||||
Remote.request('AdminDomainDelete', DomainAdminStore.fetch, {
|
||||
Name: domain.name
|
||||
name: domain.name
|
||||
});
|
||||
}
|
||||
|
||||
disableDomain(domain) {
|
||||
domain.disabled(!domain.disabled());
|
||||
Remote.request('AdminDomainDisable', DomainAdminStore.fetch, {
|
||||
Name: domain.name,
|
||||
Disabled: domain.disabled() ? 1 : 0
|
||||
name: domain.name,
|
||||
disabled: domain.disabled() ? 1 : 0
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -44,7 +59,7 @@ export class AdminSettingsDomains /*extends AbstractViewSettings*/ {
|
|||
el && ko.dataFor(el) && Remote.request('AdminDomainLoad',
|
||||
(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 {
|
||||
isArray,
|
||||
changeTheme,
|
||||
convertThemeName
|
||||
isArray
|
||||
} from 'Common/Utils';
|
||||
|
||||
import { addObservablesTo, addSubscribablesTo, addComputablesTo } from 'External/ko';
|
||||
|
|
@ -17,7 +15,7 @@ import { showScreenPopup } from 'Knoin/Knoin';
|
|||
|
||||
import Remote from 'Remote/Admin/Fetch';
|
||||
|
||||
import { ThemeStore } from 'Stores/Theme';
|
||||
import { ThemeStore, convertThemeName, changeTheme } from 'Stores/Theme';
|
||||
import { LanguageStore } from 'Stores/Language';
|
||||
import { LanguagesPopupView } from 'View/Popup/Languages';
|
||||
|
||||
|
|
@ -30,16 +28,14 @@ export class AdminSettingsGeneral extends AbstractViewSettings {
|
|||
|
||||
const aLanguagesAdmin = Settings.app('languagesAdmin');
|
||||
this.languagesAdmin = ko.observableArray(isArray(aLanguagesAdmin) ? aLanguagesAdmin : []);
|
||||
this.languageAdmin = ko.observable(SettingsGet('LanguageAdmin'));
|
||||
this.languageAdmin = ko.observable(SettingsGet('languageAdmin'));
|
||||
|
||||
this.theme = ThemeStore.theme;
|
||||
this.themes = ThemeStore.themes;
|
||||
|
||||
this.addSettings(['AllowLanguagesOnSettings']);
|
||||
this.addSettings(['allowLanguagesOnSettings']);
|
||||
|
||||
addObservablesTo(this, {
|
||||
attachmentLimitTrigger: SaveSettingStatus.Idle,
|
||||
themeTrigger: SaveSettingStatus.Idle,
|
||||
capaThemes: SettingsCapa('Themes'),
|
||||
capaUserBackground: SettingsCapa('UserBackground'),
|
||||
capaAdditionalAccounts: SettingsCapa('AdditionalAccounts'),
|
||||
|
|
@ -57,14 +53,14 @@ export class AdminSettingsGeneral extends AbstractViewSettings {
|
|||
*/
|
||||
|
||||
this.attachmentLimit = ko
|
||||
.observable(SettingsGet('AttachmentLimit') / (1024 * 1024))
|
||||
.observable(SettingsGet('attachmentLimit') / (1024 * 1024))
|
||||
.extend({ debounce: 500 });
|
||||
|
||||
this.addSetting('Language');
|
||||
this.addSetting('AttachmentLimit');
|
||||
this.addSetting('language');
|
||||
this.addSetting('attachmentLimit');
|
||||
this.addSetting('Theme', value => changeTheme(value, this.themeTrigger));
|
||||
|
||||
this.uploadData = SettingsGet('PhpUploadSizes');
|
||||
this.uploadData = SettingsGet('phpUploadSizes');
|
||||
this.uploadDataDesc =
|
||||
(this.uploadData?.upload_max_filesize || this.uploadData?.post_max_size)
|
||||
? [
|
||||
|
|
@ -93,9 +89,9 @@ export class AdminSettingsGeneral extends AbstractViewSettings {
|
|||
addSubscribablesTo(this, {
|
||||
languageAdmin: value => {
|
||||
this.languageAdminTrigger(SaveSettingStatus.Saving);
|
||||
translatorReload(true, value)
|
||||
translatorReload(value, 1)
|
||||
.then(fReloadLanguageHelper(SaveSettingStatus.Success), fReloadLanguageHelper(SaveSettingStatus.Failed))
|
||||
.then(() => Remote.saveSetting('LanguageAdmin', value));
|
||||
.then(() => Remote.saveSetting('languageAdmin', value));
|
||||
},
|
||||
|
||||
capaAdditionalAccounts: fSaveHelper('CapaAdditionalAccounts'),
|
||||
|
|
@ -118,7 +114,7 @@ export class AdminSettingsGeneral extends AbstractViewSettings {
|
|||
showScreenPopup(LanguagesPopupView, [
|
||||
this.languageAdmin,
|
||||
this.languagesAdmin(),
|
||||
SettingsGet('UserLanguageAdmin')
|
||||
SettingsGet('languageUsers')
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { AbstractViewSettings } from 'Knoin/AbstractViews';
|
|||
export class AdminSettingsLogin extends AbstractViewSettings {
|
||||
constructor() {
|
||||
super();
|
||||
this.addSetting('LoginDefaultDomain');
|
||||
this.addSettings(['DetermineUserLanguage','DetermineUserDomain','AllowLanguagesOnLogin']);
|
||||
this.addSetting('loginDefaultDomain');
|
||||
this.addSettings(['determineUserLanguage','determineUserDomain','allowLanguagesOnLogin']);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import ko from 'ko';
|
||||
|
||||
import { Notification } from 'Common/Enums';
|
||||
import { Notifications } from 'Common/Enums';
|
||||
import { getNotification } from 'Common/Translator';
|
||||
|
||||
import { PackageAdminStore } from 'Stores/Admin/Package';
|
||||
|
|
@ -15,7 +15,7 @@ export class AdminSettingsPackages extends AbstractViewSettings {
|
|||
constructor() {
|
||||
super();
|
||||
|
||||
this.addSettings(['EnabledPlugins']);
|
||||
this.addSettings(['pluginsEnable']);
|
||||
|
||||
addObservablesTo(this, {
|
||||
packagesError: ''
|
||||
|
|
@ -46,7 +46,7 @@ export class AdminSettingsPackages extends AbstractViewSettings {
|
|||
data && Remote.request('AdminPluginLoad',
|
||||
(iError, data) => iError || showScreenPopup(PluginPopupView, [data.Result]),
|
||||
{
|
||||
Id: data.id
|
||||
id: data.id
|
||||
}
|
||||
);
|
||||
// disablePlugin
|
||||
|
|
@ -67,7 +67,7 @@ export class AdminSettingsPackages extends AbstractViewSettings {
|
|||
|
||||
if (iError) {
|
||||
this.packagesError(
|
||||
getNotification(install ? Notification.CantInstallPackage : Notification.CantDeletePackage)
|
||||
getNotification(install ? Notifications.CantInstallPackage : Notifications.CantDeletePackage)
|
||||
+ (data.ErrorMessage ? ':\n' + data.ErrorMessage : '')
|
||||
);
|
||||
} else if (data.Result.Reload) {
|
||||
|
|
@ -84,7 +84,7 @@ export class AdminSettingsPackages extends AbstractViewSettings {
|
|||
Remote.request('AdminPackageDelete',
|
||||
this.requestHelper(packageToDelete, false),
|
||||
{
|
||||
Id: packageToDelete.id
|
||||
id: packageToDelete.id
|
||||
}
|
||||
);
|
||||
}
|
||||
|
|
@ -96,9 +96,9 @@ export class AdminSettingsPackages extends AbstractViewSettings {
|
|||
Remote.request('AdminPackageInstall',
|
||||
this.requestHelper(packageToInstall, true),
|
||||
{
|
||||
Id: packageToInstall.id,
|
||||
Type: packageToInstall.type,
|
||||
File: packageToInstall.file
|
||||
id: packageToInstall.id,
|
||||
type: packageToInstall.type,
|
||||
file: packageToInstall.file
|
||||
},
|
||||
60000
|
||||
);
|
||||
|
|
@ -113,15 +113,15 @@ export class AdminSettingsPackages extends AbstractViewSettings {
|
|||
if (iError) {
|
||||
plugin.enabled(disable);
|
||||
this.packagesError(
|
||||
(Notification.UnsupportedPluginPackage === iError && data?.ErrorMessage)
|
||||
(Notifications.UnsupportedPluginPackage === iError && data?.ErrorMessage)
|
||||
? data.ErrorMessage
|
||||
: getNotification(iError)
|
||||
);
|
||||
}
|
||||
// PackageAdminStore.fetch();
|
||||
}, {
|
||||
Id: plugin.id,
|
||||
Disabled: disable ? 1 : 0
|
||||
id: plugin.id,
|
||||
disabled: disable ? 1 : 0
|
||||
}
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,12 +10,12 @@ export class AdminSettingsSecurity extends AbstractViewSettings {
|
|||
constructor() {
|
||||
super();
|
||||
|
||||
this.addSettings(['UseLocalProxyForExternalImages','VerifySslCertificate','AllowSelfSigned']);
|
||||
this.addSettings(['useLocalProxyForExternalImages']);
|
||||
|
||||
this.weakPassword = rl.app.weakPassword;
|
||||
|
||||
addObservablesTo(this, {
|
||||
adminLogin: SettingsGet('AdminLogin'),
|
||||
adminLogin: SettingsGet('adminLogin'),
|
||||
adminLoginError: false,
|
||||
adminPassword: '',
|
||||
adminPasswordNew: '',
|
||||
|
|
@ -68,7 +68,7 @@ export class AdminSettingsSecurity extends AbstractViewSettings {
|
|||
capaOpenPGP: value => Remote.saveSetting('CapaOpenPGP', value)
|
||||
});
|
||||
|
||||
this.adminTOTP(SettingsGet('AdminTOTP'));
|
||||
this.adminTOTP(SettingsGet('adminTOTP'));
|
||||
|
||||
decorateKoCommands(this, {
|
||||
saveAdminUserCommand: self => self.adminLogin().trim() && self.adminPassword()
|
||||
|
|
@ -102,10 +102,10 @@ export class AdminSettingsSecurity extends AbstractViewSettings {
|
|||
this.weakPassword(!!data.Result.Weak);
|
||||
}
|
||||
}, {
|
||||
'Login': this.adminLogin(),
|
||||
'Password': this.adminPassword(),
|
||||
'NewPassword': this.adminPasswordNew(),
|
||||
'TOTP': this.adminTOTP()
|
||||
Login: this.adminLogin(),
|
||||
Password: this.adminPassword(),
|
||||
newPassword: this.adminPasswordNew(),
|
||||
TOTP: this.adminTOTP()
|
||||
});
|
||||
|
||||
return true;
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
import ko from 'ko';
|
||||
|
||||
//import { koComputable } from 'External/ko';
|
||||
import { SettingsCapa, SettingsGet } from 'Common/Globals';
|
||||
|
||||
import { AccountUserStore } from 'Stores/User/Account';
|
||||
import { IdentityUserStore } from 'Stores/User/Identity';
|
||||
import { SettingsUserStore } from 'Stores/User/Settings';
|
||||
import Remote from 'Remote/User/Fetch';
|
||||
|
||||
import { showScreenPopup } from 'Knoin/Knoin';
|
||||
|
|
@ -16,13 +18,18 @@ export class UserSettingsAccounts /*extends AbstractViewSettings*/ {
|
|||
this.allowAdditionalAccount = SettingsCapa('AdditionalAccounts');
|
||||
this.allowIdentities = SettingsCapa('Identities');
|
||||
|
||||
this.accounts = AccountUserStore.accounts;
|
||||
this.accounts = AccountUserStore;
|
||||
this.loading = AccountUserStore.loading;
|
||||
this.identities = IdentityUserStore;
|
||||
this.mainEmail = SettingsGet('MainEmail');
|
||||
this.mainEmail = SettingsGet('mainEmail');
|
||||
|
||||
this.accountForDeletion = 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() {
|
||||
|
|
@ -60,7 +67,7 @@ export class UserSettingsAccounts /*extends AbstractViewSettings*/ {
|
|||
rl.app.accountsAndIdentities();
|
||||
}
|
||||
}, {
|
||||
EmailToDelete: accountToRemove.email
|
||||
emailToDelete: accountToRemove.email
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -74,14 +81,14 @@ export class UserSettingsAccounts /*extends AbstractViewSettings*/ {
|
|||
this.identityForDeletion(null);
|
||||
IdentityUserStore.remove(oIdentity => identityToRemove === oIdentity);
|
||||
Remote.request('IdentityDelete', () => rl.app.accountsAndIdentities(), {
|
||||
IdToDelete: identityToRemove.id()
|
||||
idToDelete: identityToRemove.id()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
accountsAndIdentitiesAfterMove() {
|
||||
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() : ""))
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { koComputable, addObservablesTo } from 'External/ko';
|
||||
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*/ {
|
||||
|
|
@ -27,6 +27,14 @@ export class UserSettingsFilters /*extends AbstractViewSettings*/ {
|
|||
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() {
|
||||
this.editScript();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,11 @@
|
|||
import ko from 'ko';
|
||||
|
||||
import { Notification } from 'Common/Enums';
|
||||
import { Notifications } from 'Common/Enums';
|
||||
import { FolderMetadataKeys } from 'Common/EnumsUser';
|
||||
import { getNotification } from 'Common/Translator';
|
||||
|
||||
import { setFolder, getFolderFromCacheList, removeFolderFromCacheList } from 'Common/Cache';
|
||||
import { getFolderFromCacheList, removeFolderFromCacheList } from 'Common/Cache';
|
||||
import { defaultOptionsAfterRender } from 'Common/Utils';
|
||||
import { sortFolders } from 'Common/Folders';
|
||||
import { initOnStartOrLangChange, i18n } from 'Common/Translator';
|
||||
|
||||
import { FolderUserStore } from 'Stores/User/Folder';
|
||||
|
|
@ -18,7 +17,6 @@ import { showScreenPopup } from 'Knoin/Knoin';
|
|||
|
||||
import { FolderCreatePopupView } from 'View/Popup/FolderCreate';
|
||||
import { FolderSystemPopupView } from 'View/Popup/FolderSystem';
|
||||
import { loadFolders } from 'Model/FolderCollection';
|
||||
|
||||
const folderForDeletion = ko.observable(null).askDeleteHelper();
|
||||
|
||||
|
|
@ -52,52 +50,10 @@ export class UserSettingsFolders /*extends AbstractViewSettings*/ {
|
|||
|
||||
this.folderForDeletion = folderForDeletion;
|
||||
|
||||
this.folderForEdit = ko.observable(null).extend({ toggleSubscribeProperty: [this, 'editing'] });
|
||||
|
||||
SettingsUserStore.hideUnsubscribed.subscribe(value => Remote.saveSetting('HideUnsubscribed', 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() {
|
||||
FolderUserStore.folderListError('');
|
||||
}
|
||||
|
|
@ -119,17 +75,17 @@ export class UserSettingsFolders /*extends AbstractViewSettings*/ {
|
|||
&& folderToRemove.askDelete()
|
||||
) {
|
||||
if (0 < folderToRemove.totalEmails()) {
|
||||
// FolderUserStore.folderListError(getNotification(Notification.CantDeleteNonEmptyFolder));
|
||||
folderToRemove.errorMsg(getNotification(Notification.CantDeleteNonEmptyFolder));
|
||||
// FolderUserStore.folderListError(getNotification(Notifications.CantDeleteNonEmptyFolder));
|
||||
folderToRemove.errorMsg(getNotification(Notifications.CantDeleteNonEmptyFolder));
|
||||
} else {
|
||||
folderForDeletion(null);
|
||||
|
||||
if (folderToRemove) {
|
||||
Remote.abort('Folders').post('FolderDelete', FolderUserStore.foldersDeleting, {
|
||||
Folder: folderToRemove.fullName
|
||||
folder: folderToRemove.fullName
|
||||
}).then(
|
||||
() => {
|
||||
// folderToRemove.flags.push('\\nonexistent');
|
||||
// folderToRemove.attributes.push('\\nonexistent');
|
||||
folderToRemove.selectable(false);
|
||||
// folderToRemove.isSubscribed(false);
|
||||
// folderToRemove.checkable(false);
|
||||
|
|
@ -141,7 +97,7 @@ export class UserSettingsFolders /*extends AbstractViewSettings*/ {
|
|||
},
|
||||
error => {
|
||||
FolderUserStore.folderListError(
|
||||
getNotification(error.code, '', Notification.CantDeleteFolder)
|
||||
getNotification(error.code, '', Notifications.CantDeleteFolder)
|
||||
+ '.\n' + error.message
|
||||
);
|
||||
}
|
||||
|
|
@ -151,13 +107,17 @@ export class UserSettingsFolders /*extends AbstractViewSettings*/ {
|
|||
}
|
||||
}
|
||||
|
||||
hideError() {
|
||||
this.folderListError('');
|
||||
}
|
||||
|
||||
toggleFolderKolabType(folder, event) {
|
||||
let type = event.target.value;
|
||||
// TODO: append '.default' ?
|
||||
Remote.request('FolderSetMetadata', null, {
|
||||
Folder: folder.fullName,
|
||||
Key: FolderMetadataKeys.KolabFolderType,
|
||||
Value: type
|
||||
folder: folder.fullName,
|
||||
key: FolderMetadataKeys.KolabFolderType,
|
||||
value: type
|
||||
});
|
||||
folder.kolabType(type);
|
||||
}
|
||||
|
|
@ -165,8 +125,8 @@ export class UserSettingsFolders /*extends AbstractViewSettings*/ {
|
|||
toggleFolderSubscription(folder) {
|
||||
let subscribe = !folder.isSubscribed();
|
||||
Remote.request('FolderSubscribe', null, {
|
||||
Folder: folder.fullName,
|
||||
Subscribe: subscribe ? 1 : 0
|
||||
folder: folder.fullName,
|
||||
subscribe: subscribe ? 1 : 0
|
||||
});
|
||||
folder.isSubscribed(subscribe);
|
||||
}
|
||||
|
|
@ -174,8 +134,8 @@ export class UserSettingsFolders /*extends AbstractViewSettings*/ {
|
|||
toggleFolderCheckable(folder) {
|
||||
let checkable = !folder.checkable();
|
||||
Remote.request('FolderCheckable', null, {
|
||||
Folder: folder.fullName,
|
||||
Checkable: checkable ? 1 : 0
|
||||
folder: folder.fullName,
|
||||
checkable: checkable ? 1 : 0
|
||||
});
|
||||
folder.checkable(checkable);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,8 @@ import ko from 'ko';
|
|||
|
||||
import { SMAudio } from 'Common/Audio';
|
||||
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 { isArray } from 'Common/Utils';
|
||||
import { addSubscribablesTo, addComputablesTo } from 'External/ko';
|
||||
|
|
@ -16,7 +17,6 @@ import { LanguageStore } from 'Stores/Language';
|
|||
import { SettingsUserStore } from 'Stores/User/Settings';
|
||||
import { IdentityUserStore } from 'Stores/User/Identity';
|
||||
import { NotificationUserStore } from 'Stores/User/Notification';
|
||||
import { MessageUserStore } from 'Stores/User/Message';
|
||||
import { MessagelistUserStore } from 'Stores/User/Messagelist';
|
||||
|
||||
import Remote from 'Remote/User/Fetch';
|
||||
|
|
@ -30,23 +30,26 @@ export class UserSettingsGeneral extends AbstractViewSettings {
|
|||
|
||||
this.language = LanguageStore.language;
|
||||
this.languages = LanguageStore.languages;
|
||||
this.hourCycle = LanguageStore.hourCycle;
|
||||
|
||||
this.soundNotification = SMAudio.notifications;
|
||||
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.threadsAllowed = AppUserStore.threadsAllowed;
|
||||
|
||||
['layout', 'messageReadDelay', 'messagesPerPage',
|
||||
'editorDefaultType', 'requestReadReceipt', 'requestDsn', 'pgpSign', 'pgpEncrypt',
|
||||
'viewHTML', 'showImages', 'removeColors', 'hideDeleted', 'listInlineAttachments',
|
||||
'useCheckboxesInList', 'useThreads', 'replySameFolder', 'msgDefaultAction'
|
||||
['layout', 'messageReadDelay', 'messagesPerPage', 'checkMailInterval',
|
||||
'editorDefaultType', 'requestReadReceipt', 'requestDsn', 'requireTLS', 'pgpSign', 'pgpEncrypt',
|
||||
'viewHTML', 'viewImages', 'viewImagesWhitelist', 'removeColors', 'allowStyles', 'allowDraftAutosave',
|
||||
'hideDeleted', 'listInlineAttachments', 'simpleAttachmentsList', 'collapseBlockquotes', 'maxBlockquotesLevel',
|
||||
'useCheckboxesInList', 'listGrouped', 'useThreads', 'replySameFolder', 'msgDefaultAction', 'allowSpellcheck',
|
||||
'showNextMessage'
|
||||
].forEach(name => this[name] = SettingsUserStore[name]);
|
||||
|
||||
this.allowLanguagesOnSettings = !!SettingsGet('AllowLanguagesOnSettings');
|
||||
this.allowLanguagesOnSettings = !!SettingsGet('allowLanguagesOnSettings');
|
||||
|
||||
this.languageTrigger = ko.observable(SaveSettingStatus.Idle);
|
||||
|
||||
|
|
@ -68,8 +71,8 @@ export class UserSettingsGeneral extends AbstractViewSettings {
|
|||
editorDefaultTypes: () => {
|
||||
translateTrigger();
|
||||
return [
|
||||
{ id: EditorDefaultType.Html, name: i18n('SETTINGS_GENERAL/EDITOR_HTML') },
|
||||
{ id: EditorDefaultType.Plain, name: i18n('SETTINGS_GENERAL/EDITOR_PLAIN') }
|
||||
{ id: 'Html', name: i18n('SETTINGS_GENERAL/EDITOR_HTML') },
|
||||
{ id: 'Plain', name: i18n('SETTINGS_GENERAL/EDITOR_PLAIN') }
|
||||
];
|
||||
},
|
||||
|
||||
|
|
@ -84,9 +87,9 @@ export class UserSettingsGeneral extends AbstractViewSettings {
|
|||
layoutTypes: () => {
|
||||
translateTrigger();
|
||||
return [
|
||||
{ id: Layout.NoPreview, name: i18n('SETTINGS_GENERAL/LAYOUT_NO_SPLIT') },
|
||||
{ id: Layout.SidePreview, name: i18n('SETTINGS_GENERAL/LAYOUT_VERTICAL_SPLIT') },
|
||||
{ id: Layout.BottomPreview, name: i18n('SETTINGS_GENERAL/LAYOUT_HORIZONTAL_SPLIT') }
|
||||
{ id: 0, name: i18n('SETTINGS_GENERAL/LAYOUT_NO_SPLIT') },
|
||||
{ id: LayoutSideView, name: i18n('SETTINGS_GENERAL/LAYOUT_VERTICAL_SPLIT') },
|
||||
{ id: LayoutBottomView, name: i18n('SETTINGS_GENERAL/LAYOUT_HORIZONTAL_SPLIT') }
|
||||
];
|
||||
}
|
||||
});
|
||||
|
|
@ -95,12 +98,14 @@ export class UserSettingsGeneral extends AbstractViewSettings {
|
|||
this.addSetting('MsgDefaultAction');
|
||||
this.addSetting('MessageReadDelay');
|
||||
this.addSetting('MessagesPerPage');
|
||||
this.addSetting('CheckMailInterval');
|
||||
this.addSetting('Layout');
|
||||
this.addSetting('MaxBlockquotesLevel');
|
||||
|
||||
this.addSettings(['ViewHTML', 'ShowImages', 'HideDeleted', 'ListInlineAttachments',
|
||||
'UseCheckboxesInList', 'ReplySameFolder',
|
||||
'requestReadReceipt', 'requestDsn', 'pgpSign', 'pgpEncrypt',
|
||||
'DesktopNotifications', 'SoundNotification']);
|
||||
this.addSettings(['ViewHTML', 'ViewImages', 'ViewImagesWhitelist', 'HideDeleted', 'RemoveColors', 'AllowStyles',
|
||||
'ListInlineAttachments', 'simpleAttachmentsList', 'UseCheckboxesInList', 'listGrouped', 'ReplySameFolder',
|
||||
'requestReadReceipt', 'requestDsn', 'requireTLS', 'pgpSign', 'pgpEncrypt', 'allowSpellcheck',
|
||||
'DesktopNotifications', 'SoundNotification', 'CollapseBlockquotes', 'AllowDraftAutosave', 'showNextMessage']);
|
||||
|
||||
const fReloadLanguageHelper = (saveSettingsStep) => () => {
|
||||
this.languageTrigger(saveSettingsStep);
|
||||
|
|
@ -110,18 +115,13 @@ export class UserSettingsGeneral extends AbstractViewSettings {
|
|||
addSubscribablesTo(this, {
|
||||
language: value => {
|
||||
this.languageTrigger(SaveSettingStatus.Saving);
|
||||
translatorReload(false, value)
|
||||
translatorReload(value)
|
||||
.then(fReloadLanguageHelper(SaveSettingStatus.Success), fReloadLanguageHelper(SaveSettingStatus.Failed))
|
||||
.then(() => Remote.saveSetting('Language', value));
|
||||
.then(() => Remote.saveSetting('language', value));
|
||||
},
|
||||
|
||||
removeColors: value => {
|
||||
let dom = MessageUserStore.bodiesDom();
|
||||
if (dom) {
|
||||
dom.innerHTML = '';
|
||||
}
|
||||
Remote.saveSetting('RemoveColors', value);
|
||||
},
|
||||
hourCycle: value =>
|
||||
Remote.saveSetting('hourCycle', value),
|
||||
|
||||
notificationSound: value => {
|
||||
Remote.saveSetting('NotificationSound', value);
|
||||
|
|
@ -131,6 +131,10 @@ export class UserSettingsGeneral extends AbstractViewSettings {
|
|||
useThreads: value => {
|
||||
MessagelistUserStore([]);
|
||||
Remote.saveSetting('UseThreads', value);
|
||||
},
|
||||
|
||||
checkMailInterval: () => {
|
||||
setRefreshFoldersInterval(SettingsUserStore.checkMailInterval());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { koComputable } from 'External/ko';
|
||||
|
||||
import { SettingsCapa } from 'Common/Globals';
|
||||
import { i18n, translateTrigger } from 'Common/Translator';
|
||||
import { i18n, translateTrigger, relativeTime } from 'Common/Translator';
|
||||
|
||||
import { AbstractViewSettings } from 'Knoin/AbstractViews';
|
||||
|
||||
|
|
@ -10,30 +10,29 @@ import { SettingsUserStore } from 'Stores/User/Settings';
|
|||
import { GnuPGUserStore } from 'Stores/User/GnuPG';
|
||||
import { OpenPGPUserStore } from 'Stores/User/OpenPGP';
|
||||
|
||||
import Remote from 'Remote/User/Fetch';
|
||||
|
||||
import { showScreenPopup } from 'Knoin/Knoin';
|
||||
|
||||
import { OpenPgpImportPopupView } from 'View/Popup/OpenPgpImport';
|
||||
import { OpenPgpGeneratePopupView } from 'View/Popup/OpenPgpGenerate';
|
||||
|
||||
//import Remote from 'Remote/User/Fetch';
|
||||
|
||||
export class UserSettingsSecurity extends AbstractViewSettings {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
let i18nLogout = (key, params) => i18n('SETTINGS_SECURITY/AUTOLOGIN_' + key, params);
|
||||
this.autoLogout = SettingsUserStore.autoLogout;
|
||||
this.autoLogoutOptions = koComputable(() => {
|
||||
translateTrigger();
|
||||
return [
|
||||
{ id: 0, name: i18nLogout('NEVER_OPTION_NAME') },
|
||||
{ id: 5, name: i18nLogout('MINUTES_OPTION_NAME', { MINUTES: 5 }) },
|
||||
{ id: 10, name: i18nLogout('MINUTES_OPTION_NAME', { MINUTES: 10 }) },
|
||||
{ id: 30, name: i18nLogout('MINUTES_OPTION_NAME', { MINUTES: 30 }) },
|
||||
{ id: 60, name: i18nLogout('MINUTES_OPTION_NAME', { MINUTES: 60 }) },
|
||||
{ id: 60 * 2, name: i18nLogout('HOURS_OPTION_NAME', { HOURS: 2 }) },
|
||||
{ id: 60 * 5, name: i18nLogout('HOURS_OPTION_NAME', { HOURS: 5 }) },
|
||||
{ id: 60 * 10, name: i18nLogout('HOURS_OPTION_NAME', { HOURS: 10 }) }
|
||||
{ id: 0, name: i18n('SETTINGS_SECURITY/AUTOLOGIN_NEVER_OPTION_NAME') },
|
||||
{ id: 5, name: relativeTime(300) },
|
||||
{ id: 10, name: relativeTime(600) },
|
||||
{ id: 30, name: relativeTime(1800) },
|
||||
{ id: 60, name: relativeTime(3600) },
|
||||
{ id: 120, name: relativeTime(7200) },
|
||||
{ id: 300, name: relativeTime(18000) },
|
||||
{ id: 600, name: relativeTime(36000) }
|
||||
];
|
||||
});
|
||||
this.addSetting('AutoLogout');
|
||||
|
|
@ -47,10 +46,6 @@ export class UserSettingsSecurity extends AbstractViewSettings {
|
|||
this.canOpenPGP = SettingsCapa('OpenPGP');
|
||||
this.canGnuPG = GnuPGUserStore.isSupported();
|
||||
this.canMailvelope = !!window.mailvelope;
|
||||
|
||||
this.allowDraftAutosave = SettingsUserStore.allowDraftAutosave;
|
||||
|
||||
this.allowDraftAutosave.subscribe(value => Remote.saveSetting('AllowDraftAutosave', value))
|
||||
}
|
||||
|
||||
addOpenPgpKey() {
|
||||
|
|
@ -67,5 +62,11 @@ export class UserSettingsSecurity extends AbstractViewSettings {
|
|||
* The iframe will be injected into the container identified by selector.
|
||||
*/
|
||||
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 { SaveSettingStatus, UploadErrorCode } from 'Common/Enums';
|
||||
import { changeTheme, convertThemeName } from 'Common/Utils';
|
||||
import { themePreviewLink, serverRequest } from 'Common/Links';
|
||||
import { i18n } from 'Common/Translator';
|
||||
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';
|
||||
|
||||
|
|
@ -25,20 +25,22 @@ export class UserSettingsThemes /*extends AbstractViewSettings*/ {
|
|||
this.fontSansSerif = ThemeStore.fontSansSerif;
|
||||
this.fontSerif = ThemeStore.fontSerif;
|
||||
this.fontMono = ThemeStore.fontMono;
|
||||
ThemeStore.fontSansSerif.subscribe(value => {
|
||||
Remote.saveSettings(null, {
|
||||
fontSansSerif: value
|
||||
});
|
||||
});
|
||||
ThemeStore.fontSerif.subscribe(value => {
|
||||
Remote.saveSettings(null, {
|
||||
fontSerif: value
|
||||
});
|
||||
});
|
||||
ThemeStore.fontMono.subscribe(value => {
|
||||
Remote.saveSettings(null, {
|
||||
fontMono: value
|
||||
});
|
||||
addSubscribablesTo(ThemeStore, {
|
||||
fontSansSerif: value => {
|
||||
Remote.saveSettings(null, {
|
||||
fontSansSerif: value
|
||||
});
|
||||
},
|
||||
fontSerif: value => {
|
||||
Remote.saveSettings(null, {
|
||||
fontSerif: value
|
||||
});
|
||||
},
|
||||
fontMono: value => {
|
||||
Remote.saveSettings(null, {
|
||||
fontMono: value
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
this.theme = ThemeStore.theme;
|
||||
|
|
@ -61,6 +63,10 @@ export class UserSettingsThemes /*extends AbstractViewSettings*/ {
|
|||
});
|
||||
}
|
||||
|
||||
setTheme(theme) {
|
||||
ThemeStore.theme(theme.name);
|
||||
}
|
||||
|
||||
onBuild() {
|
||||
const currentTheme = ThemeStore.theme();
|
||||
|
||||
|
|
@ -89,8 +95,8 @@ export class UserSettingsThemes /*extends AbstractViewSettings*/ {
|
|||
})
|
||||
.on('onComplete', (id, result, data) => {
|
||||
themeBackground.loading(false);
|
||||
themeBackground.name(data?.Result?.Name || '');
|
||||
themeBackground.hash(data?.Result?.Hash || '');
|
||||
themeBackground.name(data?.Result?.name || '');
|
||||
themeBackground.hash(data?.Result?.hash || '');
|
||||
if (!themeBackground.name() || !themeBackground.hash()) {
|
||||
let errorMsg = '';
|
||||
if (data.ErrorCode) {
|
||||
|
|
|
|||
|
|
@ -82,11 +82,9 @@ export class AbstractModel {
|
|||
}
|
||||
|
||||
revivePropertiesFromJson(json) {
|
||||
let model = this.constructor;
|
||||
if (!model.validJson(json)) {
|
||||
return false;
|
||||
}
|
||||
forEachObjectEntry(json, (key, value) => {
|
||||
const model = this.constructor,
|
||||
valid = model.validJson(json);
|
||||
valid && forEachObjectEntry(json, (key, value) => {
|
||||
if ('@' !== key[0]) try {
|
||||
key = key[0].toLowerCase() + key.slice(1);
|
||||
switch (typeof this[key])
|
||||
|
|
@ -104,9 +102,12 @@ export class AbstractModel {
|
|||
case 'string':
|
||||
this[key] = typeCast(this[key], value);
|
||||
break;
|
||||
// fall through
|
||||
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');
|
||||
}
|
||||
} catch (e) {
|
||||
|
|
@ -114,7 +115,7 @@ export class AbstractModel {
|
|||
console.error(e);
|
||||
}
|
||||
});
|
||||
return true;
|
||||
return valid;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -53,10 +53,10 @@ export class FilterModel extends AbstractModel {
|
|||
actionValueFourth: '',
|
||||
actionValueFourthError: false,
|
||||
|
||||
actionMarkAsRead: false,
|
||||
markAsRead: false,
|
||||
|
||||
actionKeep: true,
|
||||
actionNoStop: false,
|
||||
keep: true,
|
||||
stop: true,
|
||||
|
||||
actionType: FilterAction.MoveTo
|
||||
});
|
||||
|
|
@ -181,24 +181,24 @@ export class FilterModel extends AbstractModel {
|
|||
return true;
|
||||
}
|
||||
|
||||
toJson() {
|
||||
toJSON() {
|
||||
return {
|
||||
// '@Object': 'Object/Filter',
|
||||
ID: this.id,
|
||||
Enabled: this.enabled() ? 1 : 0,
|
||||
Name: this.name(),
|
||||
Conditions: this.conditions.map(item => item.toJson()),
|
||||
ConditionsType: this.conditionsType(),
|
||||
Name: this.name,
|
||||
Conditions: this.conditions,
|
||||
ConditionsType: this.conditionsType,
|
||||
|
||||
ActionType: this.actionType(),
|
||||
ActionValue: this.actionValue(),
|
||||
ActionValueSecond: this.actionValueSecond(),
|
||||
ActionValueThird: this.actionValueThird(),
|
||||
ActionValueFourth: this.actionValueFourth(),
|
||||
ActionValue: this.actionValue,
|
||||
ActionValueSecond: this.actionValueSecond,
|
||||
ActionValueThird: this.actionValueThird,
|
||||
ActionValueFourth: this.actionValueFourth,
|
||||
|
||||
Keep: this.actionKeep() ? 1 : 0,
|
||||
Stop: this.actionNoStop() ? 0 : 1,
|
||||
MarkAsRead: this.actionMarkAsRead() ? 1 : 0
|
||||
Keep: this.keep() ? 1 : 0,
|
||||
Stop: this.stop() ? 1 : 0,
|
||||
MarkAsRead: this.markAsRead() ? 1 : 0
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -210,25 +210,20 @@ export class FilterModel extends AbstractModel {
|
|||
this.conditions.remove(oConditionToDelete);
|
||||
}
|
||||
|
||||
setRecipients() {
|
||||
// this.actionValueFourth(AccountUserStore.getEmailAddresses().join(', '));
|
||||
}
|
||||
|
||||
/**
|
||||
* @static
|
||||
* @param {FetchJsonFilter} json
|
||||
* @returns {?FilterModel}
|
||||
*/
|
||||
static reviveFromJson(json) {
|
||||
json.id = json.ID;
|
||||
delete json.ID;
|
||||
const filter = super.reviveFromJson(json);
|
||||
if (filter) {
|
||||
filter.id = '' + (filter.id || '');
|
||||
filter.conditions(
|
||||
(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;
|
||||
}
|
||||
|
|
@ -245,7 +240,7 @@ export class FilterModel extends AbstractModel {
|
|||
|
||||
filter.conditionsType(this.conditionsType());
|
||||
|
||||
filter.actionMarkAsRead(this.actionMarkAsRead());
|
||||
filter.markAsRead(this.markAsRead());
|
||||
|
||||
filter.actionType(this.actionType());
|
||||
|
||||
|
|
@ -256,8 +251,8 @@ export class FilterModel extends AbstractModel {
|
|||
filter.actionValueThird(this.actionValueThird());
|
||||
filter.actionValueFourth(this.actionValueFourth());
|
||||
|
||||
filter.actionKeep(this.actionKeep());
|
||||
filter.actionNoStop(this.actionNoStop());
|
||||
filter.keep(this.keep());
|
||||
filter.stop(this.stop());
|
||||
|
||||
filter.conditions(this.conditions.map(item => item.cloneSelf()));
|
||||
|
||||
|
|
|
|||
|
|
@ -81,13 +81,13 @@ export class FilterConditionModel extends AbstractModel {
|
|||
|
||||
// static reviveFromJson(json) {}
|
||||
|
||||
toJson() {
|
||||
toJSON() {
|
||||
return {
|
||||
// '@Object': 'Object/FilterCondition',
|
||||
Field: this.field(),
|
||||
Type: this.type(),
|
||||
Value: this.value(),
|
||||
ValueSecond: this.valueSecond()
|
||||
Field: this.field,
|
||||
Type: this.type,
|
||||
Value: this.value,
|
||||
ValueSecond: this.valueSecond
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -125,7 +125,7 @@ function filtersToSieveScript(filters)
|
|||
// actions
|
||||
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;
|
||||
result.push(sTab + 'addflag "\\\\Seen";');
|
||||
}
|
||||
|
|
@ -182,7 +182,7 @@ function filtersToSieveScript(filters)
|
|||
break; }
|
||||
case 'Forward':
|
||||
if (value) {
|
||||
if (filter.actionKeep()) {
|
||||
if (filter.keep()) {
|
||||
require.fileinto = 1;
|
||||
result.push(sTab + 'fileinto "INBOX";');
|
||||
}
|
||||
|
|
@ -201,7 +201,7 @@ function filtersToSieveScript(filters)
|
|||
break;
|
||||
}
|
||||
|
||||
filter.actionNoStop() || result.push(sTab + 'stop;');
|
||||
filter.stop() && result.push(sTab + 'stop;');
|
||||
|
||||
block && result.push('}');
|
||||
|
||||
|
|
@ -213,7 +213,7 @@ function filtersToSieveScript(filters)
|
|||
'/*',
|
||||
'BEGIN:FILTER:' + filter.id,
|
||||
'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 ',
|
||||
filterToString(filter, require),
|
||||
|
|
@ -290,13 +290,12 @@ export class SieveScriptModel extends AbstractModel
|
|||
return !this.nameError();
|
||||
}
|
||||
|
||||
toJson() {
|
||||
toJSON() {
|
||||
return {
|
||||
name: this.name(),
|
||||
active: this.active() ? 1 : 0,
|
||||
body: this.body()
|
||||
name: this.name,
|
||||
active: this.active,
|
||||
body: this.body
|
||||
// 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);
|
||||
if (script) {
|
||||
if (script.allowFilters()) {
|
||||
script.filters(
|
||||
Array.isArray(json.filters) && json.filters.length
|
||||
? json.filters.map(aData => FilterModel.reviveFromJson(aData)).filter(v => v)
|
||||
: sieveScriptToFilters(script.body())
|
||||
);
|
||||
} else {
|
||||
script.filters([]);
|
||||
script.filters(sieveScriptToFilters(script.body()));
|
||||
}
|
||||
script.canBeDeleted(SIEVE_FILE_NAME !== json.name);
|
||||
script.exists(true);
|
||||
|
|
|
|||
|
|
@ -25,13 +25,13 @@ export const
|
|||
arr.map(item => item.toString?.() || item).join(separator),
|
||||
/*
|
||||
getNotificationMessage = code => {
|
||||
let key = getKeyByValue(Notification, code);
|
||||
let key = getKeyByValue(Notifications, code);
|
||||
return key ? I18N_DATA.NOTIFICATIONS[i18nKey(key).replace('_NOTIFICATION', '_ERROR')] : '';
|
||||
rl.i18n('NOTIFICATIONS/')
|
||||
},
|
||||
getNotification = (code, message = '', defCode = 0) => {
|
||||
code = parseInt(code, 10) || 0;
|
||||
if (Notification.ClientViewError === code && message) {
|
||||
if (Notifications.ClientViewError === code && message) {
|
||||
return message;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -17,31 +17,29 @@ const
|
|||
|
||||
// import { folderListOptionsBuilder } from 'Common/Folders';
|
||||
/**
|
||||
* @param {Array=} aDisabled
|
||||
* @param {Array=} aHeaderLines
|
||||
* @param {Function=} fRenameCallback
|
||||
* @returns {Array}
|
||||
*/
|
||||
folderListOptionsBuilder = (
|
||||
aDisabled,
|
||||
aHeaderLines,
|
||||
fRenameCallback
|
||||
) => {
|
||||
folderListOptionsBuilder = () => {
|
||||
const
|
||||
aResult = [],
|
||||
aResult = [{
|
||||
id: '',
|
||||
name: '',
|
||||
system: false,
|
||||
disabled: false
|
||||
}],
|
||||
sDeepPrefix = '\u00A0\u00A0\u00A0',
|
||||
showUnsubscribed = true/*!SettingsUserStore.hideUnsubscribed()*/,
|
||||
|
||||
disabled = rl.settings.get('sieveAllowFileintoInbox') ? '' : 'INBOX',
|
||||
|
||||
foldersWalk = folders => {
|
||||
folders.forEach(oItem => {
|
||||
if (showUnsubscribed || oItem.hasSubscriptions() || !oItem.exists) {
|
||||
aResult.push({
|
||||
id: oItem.fullName,
|
||||
name:
|
||||
sDeepPrefix.repeat(oItem.deep) +
|
||||
fRenameCallback(oItem),
|
||||
name: sDeepPrefix.repeat(oItem.deep) + oItem.detailedName(),
|
||||
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()
|
||||
foldersWalk(window.Sieve.folderList() || []);
|
||||
|
||||
|
|
@ -82,13 +68,7 @@ export class FilterPopupView extends rl.pluginPopupView {
|
|||
});
|
||||
|
||||
this.defaultOptionsAfterRender = defaultOptionsAfterRender;
|
||||
this.folderSelectList = koComputable(() =>
|
||||
folderListOptionsBuilder(
|
||||
[rl.settings.get('SieveAllowFileintoInbox') ? '' : 'INBOX'],
|
||||
[['', '']],
|
||||
item => item?.localName() || ''
|
||||
)
|
||||
);
|
||||
this.folderSelectList = koComputable(() => folderListOptionsBuilder());
|
||||
|
||||
this.selectedFolderValue.subscribe(() => this.filter().actionValueError(false));
|
||||
|
||||
|
|
|
|||
|
|
@ -86,7 +86,7 @@ export class SieveScriptPopupView extends rl.pluginPopupView {
|
|||
// this.close();
|
||||
}
|
||||
},
|
||||
script.toJson()
|
||||
script.toJSON()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,8 +27,8 @@ try {
|
|||
setItem: (key, value) => {
|
||||
data[key] = ''+value; // forces the value to a string
|
||||
document.cookie = sName+'='+encodeURIComponent(JSON.stringify(data))
|
||||
+"; expires="+((new Date(Date.now()+(365*24*60*60*1000))).toGMTString())
|
||||
+"; path=/; samesite=strict";
|
||||
+";expires="+((new Date(Date.now()+(365*24*60*60*1000))).toGMTString())
|
||||
+";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 { isArray } from 'Common/Utils';
|
||||
import { getNotification } from 'Common/Translator';
|
||||
import Remote from 'Remote/Admin/Fetch';
|
||||
|
||||
export const PackageAdminStore = ko.observableArray();
|
||||
|
|
@ -16,6 +17,11 @@ PackageAdminStore.fetch = () => {
|
|||
PackageAdminStore.loading(false);
|
||||
if (iError) {
|
||||
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 {
|
||||
PackageAdminStore.real(!!data.Result.Real);
|
||||
PackageAdminStore.error(data.Result.Error);
|
||||
|
|
|
|||
|
|
@ -6,11 +6,13 @@ export const LanguageStore = {
|
|||
language: ko.observable(''),
|
||||
languages: ko.observableArray(),
|
||||
userLanguage: ko.observable(''),
|
||||
hourCycle: ko.observable(''),
|
||||
|
||||
populate: function() {
|
||||
const aLanguages = Settings.app('languages');
|
||||
this.languages(isArray(aLanguages) ? aLanguages : []);
|
||||
this.language(SettingsGet('Language'));
|
||||
this.userLanguage(SettingsGet('UserLanguage'));
|
||||
this.language(SettingsGet('language'));
|
||||
this.userLanguage(SettingsGet('userLanguage'));
|
||||
this.hourCycle(SettingsGet('hourCycle'));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,69 +1,109 @@
|
|||
import ko from 'ko';
|
||||
import { doc, $htmlCL, elementById, leftPanelDisabled, Settings, SettingsGet } from 'Common/Globals';
|
||||
import { isArray } from 'Common/Utils';
|
||||
import { serverRequestRaw } from 'Common/Links';
|
||||
import { $htmlCL, appEl, elementById, leftPanelDisabled, Settings, SettingsGet } from 'Common/Globals';
|
||||
import { isArray, arrayLength } from 'Common/Utils';
|
||||
import { cssLink, serverRequestRaw } from 'Common/Links';
|
||||
import { SaveSettingStatus } from 'Common/Enums';
|
||||
import { addSubscribablesTo } from 'External/ko';
|
||||
|
||||
export const ThemeStore = {
|
||||
theme: ko.observable(''),
|
||||
themes: ko.observableArray(),
|
||||
userBackgroundName: ko.observable(''),
|
||||
userBackgroundHash: ko.observable(''),
|
||||
fontSansSerif: ko.observable(''),
|
||||
fontSerif: ko.observable(''),
|
||||
fontMono: ko.observable(''),
|
||||
isMobile: ko.observable($htmlCL.contains('rl-mobile')),
|
||||
let __themeTimer = 0;
|
||||
|
||||
populate: () => {
|
||||
const themes = Settings.app('themes');
|
||||
export const
|
||||
// Also see Styles/_Values.less @maxMobileWidth
|
||||
isMobile = matchMedia('(max-width: 799px)'),
|
||||
|
||||
ThemeStore = {
|
||||
theme: ko.observable(''),
|
||||
themes: ko.observableArray(),
|
||||
userBackgroundName: ko.observable(''),
|
||||
userBackgroundHash: ko.observable(''),
|
||||
fontSansSerif: ko.observable(''),
|
||||
fontSerif: ko.observable(''),
|
||||
fontMono: ko.observable(''),
|
||||
isMobile: ko.observable(false)
|
||||
},
|
||||
|
||||
initThemes = () => {
|
||||
const theme = SettingsGet('Theme'),
|
||||
themes = Settings.app('themes');
|
||||
|
||||
ThemeStore.themes(isArray(themes) ? themes : []);
|
||||
ThemeStore.theme(SettingsGet('Theme'));
|
||||
ThemeStore.theme(theme);
|
||||
changeTheme(theme);
|
||||
if (!ThemeStore.isMobile()) {
|
||||
ThemeStore.userBackgroundName(SettingsGet('UserBackgroundName'));
|
||||
ThemeStore.userBackgroundHash(SettingsGet('UserBackgroundHash'));
|
||||
ThemeStore.userBackgroundName(SettingsGet('userBackgroundName'));
|
||||
ThemeStore.userBackgroundHash(SettingsGet('userBackgroundHash'));
|
||||
}
|
||||
ThemeStore.fontSansSerif(SettingsGet('fontSansSerif'));
|
||||
ThemeStore.fontSerif(SettingsGet('fontSerif'));
|
||||
ThemeStore.fontMono(SettingsGet('fontMono'));
|
||||
|
||||
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);
|
||||
}
|
||||
},
|
||||
|
||||
convertThemeName = theme => theme.replace(/@[a-z]+$/, '').replace(/([A-Z])/g, ' $1').trim();
|
||||
|
||||
addSubscribablesTo(ThemeStore, {
|
||||
fontSansSerif: value => {
|
||||
if (null != value) {
|
||||
let cl = appEl.classList;
|
||||
cl.forEach(name => {
|
||||
if (name.startsWith('font') && !/font(Serif|Mono)/.test(name)) {
|
||||
cl.remove(name);
|
||||
}
|
||||
});
|
||||
value && cl.add('font'+value);
|
||||
}
|
||||
},
|
||||
|
||||
fontSerif: value => {
|
||||
if (null != value) {
|
||||
let cl = appEl.classList;
|
||||
cl.forEach(name => name.startsWith('fontSerif') && cl.remove(name));
|
||||
value && cl.add('fontSerif'+value);
|
||||
}
|
||||
},
|
||||
|
||||
fontMono: value => {
|
||||
if (null != value) {
|
||||
let cl = appEl.classList;
|
||||
cl.forEach(name => name.startsWith('fontMono') && cl.remove(name));
|
||||
value && cl.add('fontMono'+value);
|
||||
}
|
||||
},
|
||||
|
||||
userBackgroundHash: value => {
|
||||
appEl.classList.toggle('UserBackground', !!value);
|
||||
appEl.style.backgroundImage = value ? "url("+serverRequestRaw('UserBackground', value)+")" : null;
|
||||
}
|
||||
});
|
||||
|
||||
isMobile.onchange = e => {
|
||||
ThemeStore.isMobile(e.matches);
|
||||
$htmlCL.toggle('rl-mobile', e.matches);
|
||||
leftPanelDisabled(e.matches);
|
||||
};
|
||||
|
||||
ThemeStore.isMobile.subscribe(value => $htmlCL.toggle('rl-mobile', value));
|
||||
|
||||
ThemeStore.fontSansSerif.subscribe(value => {
|
||||
if (null != value) {
|
||||
let cl = elementById('rl-app').classList;
|
||||
cl.forEach(name => {
|
||||
if (name.startsWith('font') && !/font(Serif|Mono)/.test(name)) {
|
||||
cl.remove(name);
|
||||
}
|
||||
});
|
||||
value && cl.add('font'+value);
|
||||
}
|
||||
});
|
||||
ThemeStore.fontSerif.subscribe(value => {
|
||||
if (null != value) {
|
||||
let cl = elementById('rl-app').classList;
|
||||
cl.forEach(name => name.startsWith('fontSerif') && cl.remove(name));
|
||||
value && cl.add('fontSerif'+value);
|
||||
}
|
||||
});
|
||||
ThemeStore.fontMono.subscribe(value => {
|
||||
if (null != value) {
|
||||
let cl = elementById('rl-app').classList;
|
||||
cl.forEach(name => name.startsWith('fontMono') && cl.remove(name));
|
||||
value && cl.add('fontMono'+value);
|
||||
}
|
||||
});
|
||||
|
||||
ThemeStore.userBackgroundHash.subscribe(value => {
|
||||
if (value) {
|
||||
$htmlCL.add('UserBackground');
|
||||
doc.body.style.backgroundImage = "url("+serverRequestRaw('UserBackground', value)+")";
|
||||
} else {
|
||||
$htmlCL.remove('UserBackground');
|
||||
doc.body.removeAttribute('style');
|
||||
}
|
||||
});
|
||||
isMobile.onchange(isMobile);
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
import { addObservablesTo, koArrayWithDestroy } from 'External/ko';
|
||||
|
||||
export const AccountUserStore = {
|
||||
accounts: koArrayWithDestroy(),
|
||||
loading: ko.observable(false).extend({ debounce: 100 }),
|
||||
export const AccountUserStore = koArrayWithDestroy();
|
||||
|
||||
getEmailAddresses: () => AccountUserStore.accounts.map(item => item.email)
|
||||
};
|
||||
AccountUserStore.loading = ko.observable(false).extend({ debounce: 100 });
|
||||
|
||||
AccountUserStore.getEmailAddresses = () => AccountUserStore.map(item => item.email);
|
||||
|
||||
addObservablesTo(AccountUserStore, {
|
||||
email: '',
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
import { keyScope, leftPanelDisabled, SettingsGet, elementById } from 'Common/Globals';
|
||||
import { addObservablesTo } from 'External/ko';
|
||||
import { ThemeStore } from 'Stores/Theme';
|
||||
import { arePopupsVisible } from 'Knoin/Knoin';
|
||||
|
||||
export const AppUserStore = {
|
||||
allowContacts: () => !!SettingsGet('ContactsIsAllowed')
|
||||
allowContacts: () => !!SettingsGet('contactsAllowed')
|
||||
};
|
||||
|
||||
addObservablesTo(AppUserStore, {
|
||||
|
|
@ -15,10 +16,9 @@ addObservablesTo(AppUserStore, {
|
|||
AppUserStore.focusedState.subscribe(value => {
|
||||
['FolderList','MessageList','MessageView'].forEach(name => {
|
||||
if (name === value) {
|
||||
keyScope(value);
|
||||
arePopupsVisible() || keyScope(value);
|
||||
ThemeStore.isMobile() && leftPanelDisabled('FolderList' !== value);
|
||||
}
|
||||
let dom = elementById('V-Mail'+name);
|
||||
dom?.classList.toggle('focused', name === value);
|
||||
elementById('V-Mail'+name).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