Merge branch 'the-djmaze:master' into master
|
|
@ -273,7 +273,6 @@ curl_proxy_auth = ""
|
||||||
force_https = Off
|
force_https = Off
|
||||||
custom_login_link = ""
|
custom_login_link = ""
|
||||||
custom_logout_link = ""
|
custom_logout_link = ""
|
||||||
allow_external_login = Off
|
|
||||||
http_client_ip_check_proxy = Off
|
http_client_ip_check_proxy = Off
|
||||||
fast_cache_memcache_host = "127.0.0.1"
|
fast_cache_memcache_host = "127.0.0.1"
|
||||||
fast_cache_memcache_port = 11211
|
fast_cache_memcache_port = 11211
|
||||||
|
|
@ -290,5 +289,5 @@ dev_email = ""
|
||||||
dev_password = ""
|
dev_password = ""
|
||||||
|
|
||||||
[version]
|
[version]
|
||||||
current = "2.20.6"
|
current = "2.22.2"
|
||||||
saved = "Fri, 30 Sep 2022 12:34:56 +0000"
|
saved = "Fri, 30 Sep 2022 12:34:56 +0000"
|
||||||
|
|
|
||||||
355
CHANGELOG.md
|
|
@ -1,3 +1,358 @@
|
||||||
|
## 2.22.2 – 2022-11-24
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Support cPanel #697
|
||||||
|
|
||||||
|
|
||||||
|
## 2.22.1 – 2022-11-23
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- AddressBookInterface::GetContactByEmail() to support sender image/avatar extension
|
||||||
|
[#115](https://github.com/the-djmaze/snappymail/pull/115)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- All the attachment zone is not clickable, even if the cursor is a hand
|
||||||
|
[#691](https://github.com/the-djmaze/snappymail/pull/691)
|
||||||
|
- Different approach for "update button duplicated in admin panel"
|
||||||
|
[#677](https://github.com/the-djmaze/snappymail/pull/677)
|
||||||
|
- Better drag & drop solution for leftPanel
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- The page does not change after batch deletion
|
||||||
|
[#684](https://github.com/the-djmaze/snappymail/pull/684)
|
||||||
|
- Prevent domain uppercase issues found in
|
||||||
|
[#689](https://github.com/the-djmaze/snappymail/pull/689)
|
||||||
|
- Login invalid response: VXNlcm5hbWU6CG
|
||||||
|
[#693](https://github.com/the-djmaze/snappymail/pull/693)
|
||||||
|
|
||||||
|
|
||||||
|
## 2.21.4 – 2022-11-22
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Added domain matcher test for
|
||||||
|
[#689](https://github.com/the-djmaze/snappymail/pull/689)
|
||||||
|
- Download all Attachments of selected Emails
|
||||||
|
[#361](https://github.com/the-djmaze/snappymail/pull/361)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Log current shortcuts scope for
|
||||||
|
[#690](https://github.com/the-djmaze/snappymail/pull/690)
|
||||||
|
- CSS everything to be box-sizing: border-box;
|
||||||
|
- Make messageview a bit larger so that it is the same height as the messagelist
|
||||||
|
- Cleanup and rearrange some fontastic glyphs
|
||||||
|
- Also show From email address by default
|
||||||
|
[#683](https://github.com/the-djmaze/snappymail/pull/683)
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Contact.display() returns [object Object]
|
||||||
|
- When left panel disabled and drag messages, show it
|
||||||
|
- Issue with admin domain connection type settings selectbox
|
||||||
|
[#689](https://github.com/the-djmaze/snappymail/pull/689)
|
||||||
|
- Mobile View on cellphones: automatic scrolling not working near the visual keyboard
|
||||||
|
[#686](https://github.com/the-djmaze/snappymail/pull/686)
|
||||||
|
- Unable to separate runtime from installation
|
||||||
|
[#685](https://github.com/the-djmaze/snappymail/pull/685)
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
- Removed inline parameter of checkbox and select components
|
||||||
|
|
||||||
|
|
||||||
|
## 2.21.3 – 2022-11-16
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Click on PGP KEY attachment opens "Import key" dialog
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Increase visible reading area for small screens
|
||||||
|
[#672](https://github.com/the-djmaze/snappymail/pull/672)
|
||||||
|
- Improved message spam score detailed view
|
||||||
|
- Improved DAV connection logging
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Handling attachments MIME type / content-type
|
||||||
|
- Message responsive resizing width/height of elements
|
||||||
|
[#678](https://github.com/the-djmaze/snappymail/pull/678)
|
||||||
|
- Focus on textarea when creating a new plain text email
|
||||||
|
[#501](https://github.com/the-djmaze/snappymail/pull/501)
|
||||||
|
- CardDav remove photos of my contacts when synchronizing
|
||||||
|
[#679](https://github.com/the-djmaze/snappymail/pull/679)
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
- \MailSo\Mime\Enumerations\MimeType
|
||||||
|
|
||||||
|
### Nextcloud
|
||||||
|
- Use fontastic in Nextcloud Files selector dialog
|
||||||
|
- Firefox < 98 dialogs
|
||||||
|
[#673](https://github.com/the-djmaze/snappymail/pull/673)
|
||||||
|
|
||||||
|
## 2.21.2 – 2022-11-15
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Allow browser Spellchecker
|
||||||
|
[#574](https://github.com/the-djmaze/snappymail/pull/574)
|
||||||
|
- Decode MIME charset of .EML attachments
|
||||||
|
[#662](https://github.com/the-djmaze/snappymail/pull/662)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Increase message visible text area
|
||||||
|
[#672](https://github.com/the-djmaze/snappymail/pull/672)
|
||||||
|
- When copy/paste image use the raw data instead of clipboard HTML
|
||||||
|
[#654](https://github.com/the-djmaze/snappymail/pull/654)
|
||||||
|
- When application.ini debug.enable is true, also debug js and css
|
||||||
|
- JavaScript rl.setWindowTitle() renamed to rl.setTitle()
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
- Message toggle fullscreen button which was only in mobile view
|
||||||
|
|
||||||
|
### Nextcloud
|
||||||
|
- Workaround Nextcloud calendar crashes
|
||||||
|
[#622](https://github.com/the-djmaze/snappymail/pull/622)
|
||||||
|
[#661](https://github.com/the-djmaze/snappymail/pull/661)
|
||||||
|
- Added share public/internal file link
|
||||||
|
[#569](https://github.com/the-djmaze/snappymail/pull/569)
|
||||||
|
|
||||||
|
|
||||||
|
## 2.21.1 – 2022-11-13
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Crypt crashes when Sodium not installed
|
||||||
|
[#641](https://github.com/the-djmaze/snappymail/pull/641)
|
||||||
|
[#657](https://github.com/the-djmaze/snappymail/pull/657)
|
||||||
|
[#663](https://github.com/the-djmaze/snappymail/pull/663)
|
||||||
|
[#664](https://github.com/the-djmaze/snappymail/pull/664)
|
||||||
|
[#668](https://github.com/the-djmaze/snappymail/pull/668)
|
||||||
|
[#669](https://github.com/the-djmaze/snappymail/pull/669)
|
||||||
|
- Personalised favicon not working - default Snappymail favicon showing
|
||||||
|
[#665](https://github.com/the-djmaze/snappymail/pull/665)
|
||||||
|
|
||||||
|
### Nextcloud
|
||||||
|
- v23 ContentSecurityPolicy versions issue
|
||||||
|
[#666](https://github.com/the-djmaze/snappymail/pull/666)
|
||||||
|
|
||||||
|
|
||||||
|
## 2.21.0 – 2022-11-11
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Put messagelist top bar buttons also in dropdown
|
||||||
|
- Allow setting additional Sec-Fetch rules, as discussed by
|
||||||
|
[#585](https://github.com/the-djmaze/snappymail/pull/585)
|
||||||
|
- Light/Dark favicon.svg
|
||||||
|
[#643](https://github.com/the-djmaze/snappymail/pull/643)
|
||||||
|
- Allow an account name/label
|
||||||
|
[#571](https://github.com/the-djmaze/snappymail/pull/571)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Moved ServiceRemoteAutoLogin to plugin/extension
|
||||||
|
- Moved ServiceExternalSso to plugin/extension
|
||||||
|
- Moved ServiceExternalLogin to plugin/extension
|
||||||
|
- Renamed ManageSieveClient to SieveClient
|
||||||
|
- New Net/Imap/Smtp/Sieve Settings object system which allows
|
||||||
|
setting SSL options per domain and verify_certificate by default
|
||||||
|
- Update plugins to use new Net/Imap/Smtp/Sieve Settings object
|
||||||
|
- Removed message double-click to full screen
|
||||||
|
[#638](https://github.com/the-djmaze/snappymail/pull/638)
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- ldap-identities-plugin by @cm-schl
|
||||||
|
[#647](https://github.com/the-djmaze/snappymail/pull/647)
|
||||||
|
- OpenSSL v3 ciphers issue
|
||||||
|
[#641](https://github.com/the-djmaze/snappymail/pull/641)
|
||||||
|
|
||||||
|
### Nextcloud
|
||||||
|
- Style PopupsNextcloudFiles view
|
||||||
|
- Link to internal files in composer
|
||||||
|
|
||||||
|
|
||||||
|
## 2.20.6 – 2022-11-08
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- ?admin login failed
|
||||||
|
[#642](https://github.com/the-djmaze/snappymail/pull/642)
|
||||||
|
- Resolve PHP 8.2 Creation of dynamic property is deprecated
|
||||||
|
|
||||||
|
|
||||||
|
## 2.20.5 – 2022-11-08
|
||||||
|
|
||||||
|
### Nextcloud
|
||||||
|
- Improved workaround for Nextcloud Content-Security-Policy bug
|
||||||
|
Safari [#631](https://github.com/the-djmaze/snappymail/issues/631)
|
||||||
|
Edge [#633](https://github.com/the-djmaze/snappymail/issues/633)
|
||||||
|
Reported [#35013](https://github.com/nextcloud/server/issues/35013)
|
||||||
|
|
||||||
|
|
||||||
|
## 2.20.4 – 2022-11-07
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Nextcloud no-embed use iframe mode failed
|
||||||
|
|
||||||
|
### Nextcloud
|
||||||
|
- Workaround Nextcloud Content-Security-Policy bug
|
||||||
|
Safari [#631](https://github.com/the-djmaze/snappymail/issues/631)
|
||||||
|
Edge [#633](https://github.com/the-djmaze/snappymail/issues/633)
|
||||||
|
Reported [#35013](https://github.com/nextcloud/server/issues/35013)
|
||||||
|
|
||||||
|
|
||||||
|
## 2.20.3 – 2022-11-07
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Throw decrypt errors
|
||||||
|
[#632](https://github.com/the-djmaze/snappymail/issues/632)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Better multiple WYSIWYG registration system (not finished)
|
||||||
|
- Better handling of admin token cookie
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Cookie “name” has been rejected because it is already expired.
|
||||||
|
[#636](https://github.com/the-djmaze/snappymail/issues/636)
|
||||||
|
- Content-Security-Policy 'strict-dynamic' was missing
|
||||||
|
|
||||||
|
### Nextcloud
|
||||||
|
- Better handling of Content-Security-Policy
|
||||||
|
[#631](https://github.com/the-djmaze/snappymail/issues/631)
|
||||||
|
[#633](https://github.com/the-djmaze/snappymail/issues/633)
|
||||||
|
- Nextcloud 23 Error Call to undefined method useStrictDynamic()
|
||||||
|
[#634](https://github.com/the-djmaze/snappymail/issues/634)
|
||||||
|
- Use snappymail icon as favicon-mask.svg instead default nextcloud logo
|
||||||
|
[#635](https://github.com/the-djmaze/snappymail/issues/635)
|
||||||
|
|
||||||
|
|
||||||
|
## 2.20.2 – 2022-11-05
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Add more search operators (i.e. copy lots of Gmail ones)
|
||||||
|
[#625](https://github.com/the-djmaze/snappymail/issues/625)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Some CSS borders to var(--border-color)
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- pgpDecrypt() using MailVelope the decrypt message was not green
|
||||||
|
- Shift + F in search bar resulted in forwarding message
|
||||||
|
[#624](https://github.com/the-djmaze/snappymail/issues/624)
|
||||||
|
|
||||||
|
### Nextcloud
|
||||||
|
- auto login mechanism not working anymore
|
||||||
|
[#627](https://github.com/the-djmaze/snappymail/issues/627)
|
||||||
|
|
||||||
|
|
||||||
|
## 2.20.1 – 2022-11-04
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Added CSS --dialog-border-clr and --dialog-border-radius
|
||||||
|
- Show lock (lock) glyph in messagelist for encrypted messages
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Decrypt failed when OpenPGP.js not loaded
|
||||||
|
|
||||||
|
### Nextcloud
|
||||||
|
- Now integrate with Nextcloud by default, but keep iframe option available
|
||||||
|
- Better theme integration with Nextcloud
|
||||||
|
- Use Nextcloud 18+ IEventDispatcher
|
||||||
|
- Solve Nextcloud 25 CSS issues
|
||||||
|
[#620](https://github.com/the-djmaze/snappymail/issues/620)
|
||||||
|
- PutinICS does is not working for all calendar events
|
||||||
|
[#622](https://github.com/the-djmaze/snappymail/issues/622)
|
||||||
|
- Update readme by @cm-schl
|
||||||
|
[#617](https://github.com/the-djmaze/snappymail/issues/617)
|
||||||
|
|
||||||
|
|
||||||
|
## 2.20.0 – 2022-11-03
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Strip mailchimp tracking
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Use some PHP typed properties
|
||||||
|
- Move bootstrap @less variables to CSS var()
|
||||||
|
- Improved theme styling
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- CSS --dropdown-menu-background-color should be --dropdown-menu-bg-color
|
||||||
|
|
||||||
|
### Nextcloud
|
||||||
|
- Disable Nextcloud Impersonate check due to login/logout complications
|
||||||
|
[#561](https://github.com/the-djmaze/snappymail/issues/561)
|
||||||
|
- Improved theme integration and be compatible with Breeze Dark
|
||||||
|
|
||||||
|
|
||||||
|
## 2.19.7 – 2022-11-02
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Make it clear that you are on the admin panel login screen
|
||||||
|
- Force PHP opcache_invalidate due to upgrade error reports "Missing version directory"
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Switching user (impersonate plugin) keeps old Email logged in
|
||||||
|
[#561](https://github.com/the-djmaze/snappymail/issues/561)
|
||||||
|
- PGP Decryption / Encryption Failures
|
||||||
|
[#600](https://github.com/the-djmaze/snappymail/issues/600)
|
||||||
|
- Undefined constant "OCA\SnappyMail\Util\RAINLOOP_APP_LIBRARIES_PATH
|
||||||
|
[#601](https://github.com/the-djmaze/snappymail/issues/601)
|
||||||
|
- Cannot access admin panel
|
||||||
|
[#602](https://github.com/the-djmaze/snappymail/issues/602)
|
||||||
|
- Wont show my emails
|
||||||
|
[#604](https://github.com/the-djmaze/snappymail/issues/604)
|
||||||
|
- Return type of MailSo\Base\StreamFilters\LineEndings::filter
|
||||||
|
[#610](https://github.com/the-djmaze/snappymail/issues/610)
|
||||||
|
- Create .pgp directory was missing
|
||||||
|
|
||||||
|
### Security
|
||||||
|
- Logger leaked some passwords
|
||||||
|
|
||||||
|
## 2.19.6 – 2022-10-31
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Put sign and encrypt options in composer dropdown menu and simplify te two existing buttons with a glyph
|
||||||
|
- Filter scripts UI let user understand which filter is active
|
||||||
|
[#590](https://github.com/the-djmaze/snappymail/issues/590)
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Method 'GetRequest' not found in \MailSo\Base\Http
|
||||||
|
[#585](https://github.com/the-djmaze/snappymail/issues/585)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Base Domain setup enhancements
|
||||||
|
- Cleanup MailSo MailClient using __call()
|
||||||
|
- Domain settings handling and store as JSON instead of ini
|
||||||
|
- Some JavaScript changes
|
||||||
|
- When try to login IMAP/SMTP/SIEVE but STARTTLS is required, force STARTTLS
|
||||||
|
- Embed admin panel into Nextcloud (with autologin, no need for separate login)
|
||||||
|
- Don't set default_domain in Nextcloud when already set
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
- Nextcloud dark mode, it is incomplete
|
||||||
|
|
||||||
|
### Deprecated
|
||||||
|
- nothing
|
||||||
|
## 2.21.0 – 2022-11-11
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Put messagelist top bar buttons also in dropdown
|
||||||
|
- Allow setting additional Sec-Fetch rules, as discussed by #585
|
||||||
|
- Light/Dark favicon.svg #643
|
||||||
|
- Allow an account name/label #571
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Moved ServiceRemoteAutoLogin to plugin/extension
|
||||||
|
- Moved ServiceExternalSso to plugin/extension
|
||||||
|
- Moved ServiceExternalLogin to plugin/extension
|
||||||
|
- Renamed ManageSieveClient to SieveClient
|
||||||
|
- New Net/Imap/Smtp/Sieve Settings object system which allows
|
||||||
|
setting SSL options per domain and verify_certificate by default
|
||||||
|
- Update plugins to use new Net/Imap/Smtp/Sieve Settings object
|
||||||
|
- Removed message double-click to full screen #638
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- ldap-identities-plugin by @cm-schl
|
||||||
|
[#647](https://github.com/the-djmaze/snappymail/pull/647)
|
||||||
|
- OpenSSL v3 ciphers issue #641
|
||||||
|
|
||||||
|
### Nextcloud
|
||||||
|
- Style PopupsNextcloudFiles view
|
||||||
|
- Link to internal files in composer
|
||||||
|
|
||||||
|
|
||||||
## 2.20.6 – 2022-11-08
|
## 2.20.6 – 2022-11-08
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
|
||||||
40
README.md
|
|
@ -41,7 +41,7 @@ Copyright (c) 2013 - 2022 RainLoop
|
||||||
|
|
||||||
This fork of RainLoop has the following changes:
|
This fork of RainLoop has the following changes:
|
||||||
|
|
||||||
* Privacy/GDPR friendly (no: Social, Gravatar, Facebook, Google, Twitter, DropBox, OwnCloud, X-Mailer)
|
* Privacy/GDPR friendly (no: Social, Gravatar, Facebook, Google, Twitter, DropBox, X-Mailer)
|
||||||
* Admin uses password_hash/password_verify
|
* Admin uses password_hash/password_verify
|
||||||
* Auth failed attempts written to syslog
|
* Auth failed attempts written to syslog
|
||||||
* Added Fail2ban instructions
|
* Added Fail2ban instructions
|
||||||
|
|
@ -57,10 +57,11 @@ This fork of RainLoop has the following changes:
|
||||||
* Removed background video support
|
* Removed background video support
|
||||||
* Removed Sentry (Application Monitoring and Error Tracking Software)
|
* Removed Sentry (Application Monitoring and Error Tracking Software)
|
||||||
* Removed Spyc yaml
|
* Removed Spyc yaml
|
||||||
|
* Removed OwnCloud
|
||||||
* Replaced gulp-uglify with gulp-terser
|
* Replaced gulp-uglify with gulp-terser
|
||||||
* CRLF => LF line endings
|
* CRLF => LF line endings
|
||||||
* Embed boot.js and boot.css into index.html
|
* Embed boot.js and boot.css into index.html
|
||||||
* Ongoing removal of old JavaScript code (things are native these days)
|
* Removal of old JavaScript code (things are native these days)
|
||||||
* Added modified [Squire](https://github.com/neilj/Squire) HTML editor as replacement for CKEditor
|
* Added modified [Squire](https://github.com/neilj/Squire) HTML editor as replacement for CKEditor
|
||||||
* Updated [Sabre/VObject](https://github.com/sabre-io/vobject)
|
* Updated [Sabre/VObject](https://github.com/sabre-io/vobject)
|
||||||
* Split Admin specific JavaScript code from User code
|
* Split Admin specific JavaScript code from User code
|
||||||
|
|
@ -105,7 +106,6 @@ Supported are:
|
||||||
The result is faster and smaller download code (good for mobile networks).
|
The result is faster and smaller download code (good for mobile networks).
|
||||||
|
|
||||||
* Added dev/prototype.js for some additional features
|
* Added dev/prototype.js for some additional features
|
||||||
* boot.js without webpack overhead
|
|
||||||
* Modified Jua.js to be without jQuery
|
* Modified Jua.js to be without jQuery
|
||||||
* Replaced Autolinker with simple https/email detection
|
* Replaced Autolinker with simple https/email detection
|
||||||
* Replaced momentToNode with proper HTML5 `<time>`
|
* Replaced momentToNode with proper HTML5 `<time>`
|
||||||
|
|
@ -118,6 +118,7 @@ The result is faster and smaller download code (good for mobile networks).
|
||||||
* Replaced simplestatemanager with CSS @media
|
* Replaced simplestatemanager with CSS @media
|
||||||
* Replaced inputosaurus with own code
|
* Replaced inputosaurus with own code
|
||||||
* Replaced keymaster with own shortcuts handler
|
* Replaced keymaster with own shortcuts handler
|
||||||
|
* Replaced OpenPGP.js v2 with OpenPGP.js v5
|
||||||
* Removed ifvisible.js
|
* Removed ifvisible.js
|
||||||
* Removed pikaday
|
* Removed pikaday
|
||||||
* Removed underscore
|
* Removed underscore
|
||||||
|
|
@ -140,26 +141,26 @@ RainLoop 1.17 vs SnappyMail
|
||||||
|
|
||||||
|js/* |RainLoop |Snappy |
|
|js/* |RainLoop |Snappy |
|
||||||
|--------------- |--------: |--------: |
|
|--------------- |--------: |--------: |
|
||||||
|admin.js |2.170.153 | 81.239 |
|
|admin.js |2.170.153 | 82.419 |
|
||||||
|app.js |4.207.787 | 406.270 |
|
|app.js |4.207.787 | 407.238 |
|
||||||
|boot.js | 868.735 | 2.055 |
|
|boot.js | 868.735 | 1.989 |
|
||||||
|libs.js | 658.812 | 192.965 |
|
|libs.js | 658.812 | 193.075 |
|
||||||
|sieve.js | 0 | 86.121 |
|
|sieve.js | 0 | 86.121 |
|
||||||
|polyfills.js | 334.608 | 0 |
|
|polyfills.js | 334.608 | 0 |
|
||||||
|serviceworker.js | 0 | 285 |
|
|serviceworker.js | 0 | 285 |
|
||||||
|TOTAL |8.240.095 | 768.935 |
|
|TOTAL |8.240.095 | 771.127 |
|
||||||
|
|
||||||
|js/min/* |RainLoop |Snappy |RL gzip |SM gzip |RL brotli |SM brotli |
|
|js/min/* |RainLoop |Snappy |RL gzip |SM gzip |RL brotli |SM brotli |
|
||||||
|--------------- |--------: |--------: |------: |------: |--------: |--------: |
|
|--------------- |--------: |--------: |------: |------: |--------: |--------: |
|
||||||
|admin.min.js | 256.831 | 40.167 | 73.606 | 13.399 | 60.877 | 12.006 |
|
|admin.min.js | 256.831 | 40.933 | 73.606 | 13.575 | 60.877 | 12.185 |
|
||||||
|app.min.js | 515.367 | 188.083 |139.456 | 62.512 |110.485 | 53.609 |
|
|app.min.js | 515.367 | 188.830 |139.456 | 62.816 |110.485 | 53.944 |
|
||||||
|boot.min.js | 84.659 | 1.261 | 26.998 | 776 | 23.643 | 633 |
|
|boot.min.js | 84.659 | 1.216 | 26.998 | 761 | 23.643 | 614 |
|
||||||
|libs.min.js | 584.772 | 92.055 |180.901 | 34.113 |155.182 | 30.641 |
|
|libs.min.js | 584.772 | 92.084 |180.901 | 34.119 |155.182 | 30.621 |
|
||||||
|sieve.min.js | 0 | 41.926 | 0 | 10.484 | 0 | 9.451 |
|
|sieve.min.js | 0 | 41.926 | 0 | 10.484 | 0 | 9.451 |
|
||||||
|polyfills.min.js | 32.837 | 0 | 11.406 | 0 | 10.175 | 0 |
|
|polyfills.min.js | 32.837 | 0 | 11.406 | 0 | 10.175 | 0 |
|
||||||
|TOTAL user |1.217.635 | 281.399 |358.761 | 97.401 |299.485 | 84.883 |
|
|TOTAL user |1.217.635 | 282.130 |358.761 | 97.696 |299.485 | 85.179 |
|
||||||
|TOTAL user+sieve |1.217.635 | 323.325 |358.761 |107.885 |299.485 | 94.334 |
|
|TOTAL user+sieve |1.217.635 | 324.056 |358.761 |108.180 |299.485 | 94.630 |
|
||||||
|TOTAL admin | 959.099 | 133.483 |292.911 | 48.288 |249.877 | 43.280 |
|
|TOTAL admin | 959.099 | 134.233 |292.911 | 48.455 |249.877 | 43.420 |
|
||||||
|
|
||||||
For a user its around 70% smaller and faster than traditional RainLoop.
|
For a user its around 70% smaller and faster than traditional RainLoop.
|
||||||
|
|
||||||
|
|
@ -169,6 +170,7 @@ For a user its around 70% smaller and faster than traditional RainLoop.
|
||||||
* Themes work in mobile mode
|
* Themes work in mobile mode
|
||||||
* Bugfix invalid/conflicting css rules
|
* Bugfix invalid/conflicting css rules
|
||||||
* Use flexbox
|
* Use flexbox
|
||||||
|
* Use border-box
|
||||||
* Split app.css to have separate admin.css
|
* Split app.css to have separate admin.css
|
||||||
* Remove oldschool 'float'
|
* Remove oldschool 'float'
|
||||||
* Remove unused css
|
* Remove unused css
|
||||||
|
|
@ -187,12 +189,12 @@ For a user its around 70% smaller and faster than traditional RainLoop.
|
||||||
|
|
||||||
|css/* |RainLoop |Snappy |RL gzip |SM gzip |SM brotli |
|
|css/* |RainLoop |Snappy |RL gzip |SM gzip |SM brotli |
|
||||||
|------------ |-------: |------: |------: |------: |--------: |
|
|------------ |-------: |------: |------: |------: |--------: |
|
||||||
|app.css | 340.331 | 83.746 | 46.946 | 17.262 | 14.779 |
|
|app.css | 340.331 | 82.425 | 46.946 | 17.049 | 14.595 |
|
||||||
|app.min.css | 274.947 | 67.483 | 39.647 | 15.311 | 13.365 |
|
|app.min.css | 274.947 | 66.502 | 39.647 | 15.049 | 13.221 |
|
||||||
|boot.css | | 1.326 | | 664 | 545 |
|
|boot.css | | 1.326 | | 664 | 545 |
|
||||||
|boot.min.css | | 1.071 | | 590 | 474 |
|
|boot.min.css | | 1.071 | | 590 | 474 |
|
||||||
|admin.css | | 30.816 | | 6.939 | 6.028 |
|
|admin.css | | 29.807 | | 6.744 | 5.851 |
|
||||||
|admin.min.css | | 24.969 | | 6.300 | 5.531 |
|
|admin.min.css | | 24.223 | | 6.142 | 5.397 |
|
||||||
|
|
||||||
### PGP
|
### PGP
|
||||||
RainLoop uses the old OpenPGP.js v2
|
RainLoop uses the old OpenPGP.js v2
|
||||||
|
|
|
||||||
39
build/cpanel.php
Executable file
|
|
@ -0,0 +1,39 @@
|
||||||
|
<?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('_include.php', 'snappymail/app/_include.php');
|
||||||
|
$cpanel_tar->addFile('README.md', "{$cpanel_path}README.md");
|
||||||
|
$cpanel_tar->addFromString('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";
|
||||||
|
|
@ -36,9 +36,6 @@ export class AbstractApp {
|
||||||
createViewModel: (params, componentInfo) => {
|
createViewModel: (params, componentInfo) => {
|
||||||
params = params || {};
|
params = params || {};
|
||||||
i18nToNodes(componentInfo.element);
|
i18nToNodes(componentInfo.element);
|
||||||
if (params.inline) {
|
|
||||||
componentInfo.element.style.display = 'inline-block';
|
|
||||||
}
|
|
||||||
return new ClassObject(params);
|
return new ClassObject(params);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -212,7 +212,7 @@ export class AppUser extends AbstractApp {
|
||||||
|
|
||||||
start() {
|
start() {
|
||||||
if (SettingsGet('Auth')) {
|
if (SettingsGet('Auth')) {
|
||||||
rl.setWindowTitle(i18n('GLOBAL/LOADING'));
|
rl.setTitle(i18n('GLOBAL/LOADING'));
|
||||||
|
|
||||||
SMAudio.notifications(!!SettingsGet('SoundNotification'));
|
SMAudio.notifications(!!SettingsGet('SoundNotification'));
|
||||||
NotificationUserStore.enabled(!!SettingsGet('DesktopNotifications'));
|
NotificationUserStore.enabled(!!SettingsGet('DesktopNotifications'));
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,8 @@ const
|
||||||
ics: 'text/calendar',
|
ics: 'text/calendar',
|
||||||
xml: 'text/xml',
|
xml: 'text/xml',
|
||||||
json: app+'json',
|
json: app+'json',
|
||||||
asc: app+'pgp-signature',
|
// asc: app+'pgp-signature',
|
||||||
|
// asc: app+'pgp-keys',
|
||||||
p10: app+'pkcs10',
|
p10: app+'pkcs10',
|
||||||
p7c: app+'pkcs7-mime',
|
p7c: app+'pkcs7-mime',
|
||||||
p7m: app+'pkcs7-mime',
|
p7m: app+'pkcs7-mime',
|
||||||
|
|
|
||||||
|
|
@ -18,8 +18,8 @@ export const
|
||||||
dropdowns = [],
|
dropdowns = [],
|
||||||
dropdownVisibility = ko.observable(false).extend({ rateLimit: 0 }),
|
dropdownVisibility = ko.observable(false).extend({ rateLimit: 0 }),
|
||||||
|
|
||||||
moveAction = ko.observable(false),
|
|
||||||
leftPanelDisabled = ko.observable(false),
|
leftPanelDisabled = ko.observable(false),
|
||||||
|
toggleLeftPanel = () => leftPanelDisabled(!leftPanelDisabled()),
|
||||||
|
|
||||||
createElement = (name, attr) => {
|
createElement = (name, attr) => {
|
||||||
let el = doc.createElement(name);
|
let el = doc.createElement(name);
|
||||||
|
|
@ -68,10 +68,4 @@ dropdownVisibility.subscribe(value => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
leftPanelDisabled.toggle = () => leftPanelDisabled(!leftPanelDisabled());
|
leftPanelDisabled.subscribe(value => $htmlCL.toggle('rl-left-panel-disabled', value));
|
||||||
leftPanelDisabled.subscribe(value => {
|
|
||||||
value && moveAction() && moveAction(false);
|
|
||||||
$htmlCL.toggle('rl-left-panel-disabled', value);
|
|
||||||
});
|
|
||||||
|
|
||||||
moveAction.subscribe(value => value && leftPanelDisabled() && leftPanelDisabled(false));
|
|
||||||
|
|
|
||||||
|
|
@ -229,34 +229,30 @@ export const
|
||||||
|
|
||||||
// if ('TABLE' === name || 'TD' === name || 'TH' === name) {
|
// if ('TABLE' === name || 'TD' === name || 'TH' === name) {
|
||||||
if (!oStyle.backgroundImage) {
|
if (!oStyle.backgroundImage) {
|
||||||
value = oStyle.width;
|
if ('TD' !== name && 'TH' !== name) {
|
||||||
|
// Make width responsive
|
||||||
if (hasAttribute('width')) {
|
if (hasAttribute('width')) {
|
||||||
if (!value) {
|
|
||||||
value = getAttribute('width');
|
value = getAttribute('width');
|
||||||
if (1 < pInt(value)) {
|
|
||||||
oStyle.width = value.includes('%') ? value : value + 'px';
|
oStyle.width = value.includes('%') ? value : value + 'px';
|
||||||
}
|
|
||||||
}
|
|
||||||
delAttribute('width');
|
delAttribute('width');
|
||||||
}
|
}
|
||||||
if (value && !value.includes('%')) {
|
value = oStyle.removeProperty('width');
|
||||||
|
if (value) {
|
||||||
oStyle.maxWidth = value;
|
oStyle.maxWidth = value;
|
||||||
if ('TD' !== name && 'TH' !== name) {
|
|
||||||
oStyle.width = '100%';
|
oStyle.width = '100%';
|
||||||
}
|
}
|
||||||
}
|
// Make height responsive
|
||||||
if (hasAttribute('height')) {
|
if (hasAttribute('height')) {
|
||||||
value = getAttribute('height');
|
value = getAttribute('height');
|
||||||
if (pInt(value)) {
|
|
||||||
oStyle.height = value.includes('%') ? value : value + 'px';
|
oStyle.height = value.includes('%') ? value : value + 'px';
|
||||||
}
|
|
||||||
delAttribute('height');
|
delAttribute('height');
|
||||||
}
|
}
|
||||||
value = oStyle.removeProperty('height');
|
value = oStyle.removeProperty('height');
|
||||||
if (value && !value.includes('%')) {
|
if (value) {
|
||||||
oStyle.maxHeight = value;
|
oStyle.maxHeight = value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
// } else
|
// } else
|
||||||
if ('A' === name) {
|
if ('A' === name) {
|
||||||
value = oElement.href;
|
value = oElement.href;
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { MessageFlagsCache } from 'Common/Cache';
|
import { MessageFlagsCache } from 'Common/Cache';
|
||||||
import { Notification } from 'Common/Enums';
|
import { Notification } from 'Common/Enums';
|
||||||
import { MessageSetAction, ComposeType/*, FolderType*/ } from 'Common/EnumsUser';
|
import { MessageSetAction, ComposeType/*, FolderType*/ } from 'Common/EnumsUser';
|
||||||
import { doc, createElement, elementById, dropdowns, dropdownVisibility, SettingsGet } from 'Common/Globals';
|
import { doc, createElement, elementById, dropdowns, dropdownVisibility, SettingsGet, leftPanelDisabled } from 'Common/Globals';
|
||||||
import { plainToHtml } from 'Common/Html';
|
import { plainToHtml } from 'Common/Html';
|
||||||
import { getNotification } from 'Common/Translator';
|
import { getNotification } from 'Common/Translator';
|
||||||
import { EmailModel } from 'Model/Email';
|
import { EmailModel } from 'Model/Email';
|
||||||
|
|
@ -15,6 +15,8 @@ import Remote from 'Remote/User/Fetch';
|
||||||
|
|
||||||
export const
|
export const
|
||||||
|
|
||||||
|
moveAction = ko.observable(false),
|
||||||
|
|
||||||
dropdownsDetectVisibility = (() =>
|
dropdownsDetectVisibility = (() =>
|
||||||
dropdownVisibility(!!dropdowns.find(item => item.classList.contains('show')))
|
dropdownVisibility(!!dropdowns.find(item => item.classList.contains('show')))
|
||||||
).debounce(50),
|
).debounce(50),
|
||||||
|
|
@ -356,3 +358,6 @@ populateMessageBody = (oMessage, popup) => {
|
||||||
}, oMessage.folder, oMessage.uid);
|
}, oMessage.folder, oMessage.uid);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
leftPanelDisabled.subscribe(value => value && moveAction(false));
|
||||||
|
moveAction.subscribe(value => value && leftPanelDisabled(false));
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@ export class CheckboxComponent {
|
||||||
: ko.observable(undefined === params.enable || !!params.enable);
|
: ko.observable(undefined === params.enable || !!params.enable);
|
||||||
|
|
||||||
this.label = params.label;
|
this.label = params.label;
|
||||||
this.inline = params.inline;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
click() {
|
click() {
|
||||||
|
|
|
||||||
7
dev/External/SquireUI.js
vendored
|
|
@ -493,7 +493,12 @@ class SquireUI
|
||||||
}
|
}
|
||||||
|
|
||||||
focus() {
|
focus() {
|
||||||
('plain' == this.mode ? this.plain : this.squire).focus();
|
if ('plain' == this.mode) {
|
||||||
|
this.plain.focus();
|
||||||
|
this.plain.setSelectionRange(0, 0);
|
||||||
|
} else {
|
||||||
|
this.squire.focus();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
4
dev/External/User/ko.js
vendored
|
|
@ -2,7 +2,7 @@ import 'External/ko';
|
||||||
import ko from 'ko';
|
import ko from 'ko';
|
||||||
import { HtmlEditor } from 'Common/Html';
|
import { HtmlEditor } from 'Common/Html';
|
||||||
import { timeToNode } from 'Common/Translator';
|
import { timeToNode } from 'Common/Translator';
|
||||||
import { doc, elementById, addEventsListeners, dropdowns } from 'Common/Globals';
|
import { doc, elementById, addEventsListeners, dropdowns, leftPanelDisabled } from 'Common/Globals';
|
||||||
import { dropdownsDetectVisibility } from 'Common/UtilsUser';
|
import { dropdownsDetectVisibility } from 'Common/UtilsUser';
|
||||||
import { EmailAddressesComponent } from 'Component/EmailAddresses';
|
import { EmailAddressesComponent } from 'Component/EmailAddresses';
|
||||||
import { ThemeStore } from 'Stores/Theme';
|
import { ThemeStore } from 'Stores/Theme';
|
||||||
|
|
@ -144,6 +144,8 @@ Object.assign(ko.bindingHandlers, {
|
||||||
|
|
||||||
// Remove the Chrome visibility
|
// Remove the Chrome visibility
|
||||||
dragImage.style.cssText = '';
|
dragImage.style.cssText = '';
|
||||||
|
|
||||||
|
leftPanelDisabled(false);
|
||||||
} else {
|
} else {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import ko from 'ko';
|
import ko from 'ko';
|
||||||
|
|
||||||
import { addObservablesTo, addComputablesTo, addSubscribablesTo } from 'External/ko';
|
import { addObservablesTo, addComputablesTo, addSubscribablesTo } from 'External/ko';
|
||||||
import { keyScope, addShortcut, SettingsGet, leftPanelDisabled, elementById } from 'Common/Globals';
|
import { keyScope, addShortcut, SettingsGet, leftPanelDisabled, toggleLeftPanel, elementById } from 'Common/Globals';
|
||||||
import { ViewTypePopup, showScreenPopup } from 'Knoin/Knoin';
|
import { ViewTypePopup, showScreenPopup } from 'Knoin/Knoin';
|
||||||
|
|
||||||
import { SaveSettingStatus } from 'Common/Enums';
|
import { SaveSettingStatus } from 'Common/Enums';
|
||||||
|
|
@ -97,6 +97,7 @@ export class AbstractViewLeft extends AbstractView
|
||||||
{
|
{
|
||||||
super(templateID, 'left');
|
super(templateID, 'left');
|
||||||
this.leftPanelDisabled = leftPanelDisabled;
|
this.leftPanelDisabled = leftPanelDisabled;
|
||||||
|
this.toggleLeftPanel = toggleLeftPanel;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,16 @@
|
||||||
import ko from 'ko';
|
import ko from 'ko';
|
||||||
import { koComputable } from 'External/ko';
|
import { koComputable } from 'External/ko';
|
||||||
import { doc, $htmlCL, elementById, fireEvent } from 'Common/Globals';
|
import { doc, $htmlCL, elementById, fireEvent } from 'Common/Globals';
|
||||||
import { forEachObjectValue, forEachObjectEntry } from 'Common/Utils';
|
import { forEachObjectEntry } from 'Common/Utils';
|
||||||
import { i18nToNodes } from 'Common/Translator';
|
import { i18nToNodes } from 'Common/Translator';
|
||||||
|
|
||||||
let
|
let
|
||||||
SCREENS = {},
|
|
||||||
currentScreen = null,
|
currentScreen = null,
|
||||||
defaultScreenName = '';
|
defaultScreenName = '';
|
||||||
|
|
||||||
const
|
const
|
||||||
|
SCREENS = new Map,
|
||||||
|
|
||||||
autofocus = dom => dom.querySelector('[autofocus]')?.focus(),
|
autofocus = dom => dom.querySelector('[autofocus]')?.focus(),
|
||||||
|
|
||||||
visiblePopups = new Set,
|
visiblePopups = new Set,
|
||||||
|
|
@ -18,7 +19,7 @@ const
|
||||||
* @param {string} screenName
|
* @param {string} screenName
|
||||||
* @returns {?Object}
|
* @returns {?Object}
|
||||||
*/
|
*/
|
||||||
screen = screenName => (screenName && SCREENS[screenName]) || null,
|
screen = screenName => (screenName && SCREENS.get(screenName)) || null,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Function} ViewModelClass
|
* @param {Function} ViewModelClass
|
||||||
|
|
@ -35,8 +36,6 @@ const
|
||||||
dialog = ViewTypePopup === vm.viewType,
|
dialog = ViewTypePopup === vm.viewType,
|
||||||
vmPlace = doc.getElementById(position);
|
vmPlace = doc.getElementById(position);
|
||||||
|
|
||||||
fireEvent('rl-view-model.create', vm);
|
|
||||||
|
|
||||||
ViewModelClass.__builded = true;
|
ViewModelClass.__builded = true;
|
||||||
ViewModelClass.__vm = vm;
|
ViewModelClass.__vm = vm;
|
||||||
|
|
||||||
|
|
@ -53,6 +52,7 @@ const
|
||||||
|
|
||||||
// Firefox < 98 / Safari < 15.4 HTMLDialogElement not defined
|
// Firefox < 98 / Safari < 15.4 HTMLDialogElement not defined
|
||||||
if (!vmDom.showModal) {
|
if (!vmDom.showModal) {
|
||||||
|
vmDom.className = 'polyfill';
|
||||||
vmDom.showModal = () => {
|
vmDom.showModal = () => {
|
||||||
vmDom.backdrop ||
|
vmDom.backdrop ||
|
||||||
vmDom.before(vmDom.backdrop = Element.fromHTML('<div class="dialog-backdrop"></div>'));
|
vmDom.before(vmDom.backdrop = Element.fromHTML('<div class="dialog-backdrop"></div>'));
|
||||||
|
|
@ -113,6 +113,8 @@ const
|
||||||
vmDom.addEventListener('transitionend', endShowHide);
|
vmDom.addEventListener('transitionend', endShowHide);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fireEvent('rl-view-model.create', vm);
|
||||||
|
|
||||||
ko.applyBindingAccessorsToNode(
|
ko.applyBindingAccessorsToNode(
|
||||||
vmDom,
|
vmDom,
|
||||||
{
|
{
|
||||||
|
|
@ -129,7 +131,7 @@ const
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return ViewModelClass && ViewModelClass.__vm;
|
return ViewModelClass?.__vm;
|
||||||
},
|
},
|
||||||
|
|
||||||
forEachViewModel = (screen, fn) => {
|
forEachViewModel = (screen, fn) => {
|
||||||
|
|
@ -157,11 +159,9 @@ const
|
||||||
* @param {Function} ViewModelClassToHide
|
* @param {Function} ViewModelClassToHide
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
hideScreenPopup = ViewModelClassToHide => {
|
hideScreenPopup = ViewModelClassToHide =>
|
||||||
if (ViewModelClassToHide?.__vm && ViewModelClassToHide?.__dom) {
|
ViewModelClassToHide?.__vm && ViewModelClassToHide.__dom
|
||||||
ViewModelClassToHide.__vm.modalVisible(false);
|
&& ViewModelClassToHide.__vm.modalVisible(false),
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} screenName
|
* @param {string} screenName
|
||||||
|
|
@ -208,9 +208,7 @@ const
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
// hide screen
|
// hide screen
|
||||||
if (currentScreen && !isSameScreen) {
|
currentScreen && !isSameScreen && hideScreen(currentScreen);
|
||||||
hideScreen(currentScreen);
|
|
||||||
}
|
|
||||||
// --
|
// --
|
||||||
|
|
||||||
currentScreen = vmScreen;
|
currentScreen = vmScreen;
|
||||||
|
|
@ -267,21 +265,19 @@ export const
|
||||||
*/
|
*/
|
||||||
startScreens = screensClasses => {
|
startScreens = screensClasses => {
|
||||||
hasher.clear();
|
hasher.clear();
|
||||||
forEachObjectValue(SCREENS, screen => hideScreen(screen, 1));
|
SCREENS.forEach(screen => hideScreen(screen, 1));
|
||||||
SCREENS = {};
|
SCREENS.clear();
|
||||||
currentScreen = null,
|
currentScreen = null,
|
||||||
defaultScreenName = '';
|
defaultScreenName = '';
|
||||||
|
|
||||||
screensClasses.forEach(CScreen => {
|
screensClasses.forEach(CScreen => {
|
||||||
if (CScreen) {
|
|
||||||
const vmScreen = new CScreen(),
|
const vmScreen = new CScreen(),
|
||||||
screenName = vmScreen.screenName;
|
screenName = vmScreen.screenName;
|
||||||
defaultScreenName || (defaultScreenName = screenName);
|
defaultScreenName || (defaultScreenName = screenName);
|
||||||
SCREENS[screenName] = vmScreen;
|
SCREENS.set(screenName, vmScreen);
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
forEachObjectValue(SCREENS, vmScreen => {
|
SCREENS.forEach(vmScreen => {
|
||||||
if (!vmScreen.__started) {
|
if (!vmScreen.__started) {
|
||||||
vmScreen.onStart();
|
vmScreen.onStart();
|
||||||
vmScreen.__started = true;
|
vmScreen.__started = true;
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,16 @@
|
||||||
|
|
||||||
const
|
const
|
||||||
// RFC2045
|
// RFC2045
|
||||||
QPDecodeIn = /=([0-9A-F]{2})/g,
|
QPDecodeParams = [/=([0-9A-F]{2})/g, (...args) => String.fromCharCode(parseInt(args[1], 16))],
|
||||||
QPDecodeOut = (...args) => String.fromCharCode(parseInt(args[1], 16));
|
QPDecode = data => data.replace(/=\r?\n/g, '').replace(...QPDecodeParams),
|
||||||
|
decodeText = (charset, data) => {
|
||||||
|
try {
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/API/Encoding_API/Encodings
|
||||||
|
return new TextDecoder(charset).decode(Uint8Array.from(data, c => c.charCodeAt(0)));
|
||||||
|
} catch (e) {
|
||||||
|
console.error({charset:charset,error:e});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export function ParseMime(text)
|
export function ParseMime(text)
|
||||||
{
|
{
|
||||||
|
|
@ -40,22 +48,14 @@ export function ParseMime(text)
|
||||||
|
|
||||||
get body() {
|
get body() {
|
||||||
let body = this.bodyRaw,
|
let body = this.bodyRaw,
|
||||||
// charset = this.header('content-type')?.params.charset,
|
charset = this.header('content-type')?.params.charset,
|
||||||
encoding = this.headerValue('content-transfer-encoding');
|
encoding = this.headerValue('content-transfer-encoding');
|
||||||
if ('quoted-printable' == encoding) {
|
if ('quoted-printable' == encoding) {
|
||||||
body = body.replace(/=\r?\n/g, '').replace(QPDecodeIn, QPDecodeOut);
|
body = QPDecode(body);
|
||||||
} else if ('base64' == encoding) {
|
} else if ('base64' == encoding) {
|
||||||
body = atob(body.replace(/\r?\n/g, ''));
|
body = atob(body.replace(/\r?\n/g, ''));
|
||||||
}
|
}
|
||||||
/*
|
return decodeText(charset, body);
|
||||||
try {
|
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/API/Encoding_API/Encodings
|
|
||||||
return new TextDecoder(charset).decode(Uint8Array.from(body, c => c.charCodeAt(0)));
|
|
||||||
// return new TextDecoder(charset).decode(new TextEncoder().encode(body));
|
|
||||||
} catch (e) {
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
return body;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get dataUrl() {
|
get dataUrl() {
|
||||||
|
|
@ -65,7 +65,7 @@ export function ParseMime(text)
|
||||||
body = body.replace(/\r?\n/g, '');
|
body = body.replace(/\r?\n/g, '');
|
||||||
} else {
|
} else {
|
||||||
if ('quoted-printable' == encoding) {
|
if ('quoted-printable' == encoding) {
|
||||||
body = body.replace(/=\r?\n/g, '').replace(QPDecodeIn, QPDecodeOut);
|
body = QPDecode(body);
|
||||||
}
|
}
|
||||||
body = btoa(body);
|
body = btoa(body);
|
||||||
}
|
}
|
||||||
|
|
@ -111,8 +111,12 @@ export function ParseMime(text)
|
||||||
[...header.matchAll(/;\s*([^;=]+)=\s*"?([^;"]+)"?/g)].forEach(param =>
|
[...header.matchAll(/;\s*([^;=]+)=\s*"?([^;"]+)"?/g)].forEach(param =>
|
||||||
params[param[1].trim().toLowerCase()] = param[2].trim()
|
params[param[1].trim().toLowerCase()] = param[2].trim()
|
||||||
);
|
);
|
||||||
|
// encoded-word = "=?" charset "?" encoding "?" encoded-text "?="
|
||||||
|
match[2] = match[2].trim().replace(/=\?([^?]+)\?(B|Q)\?(.+?)\?=/g, (m, charset, encoding, text) =>
|
||||||
|
decodeText(charset, 'B' == encoding ? atob(text) : QPDecode(text))
|
||||||
|
);
|
||||||
headers[match[1].trim().toLowerCase()] = {
|
headers[match[1].trim().toLowerCase()] = {
|
||||||
value: match[2].trim(),
|
value: match[2],
|
||||||
params: params
|
params: params
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ export class ComposeAttachmentModel extends AbstractModel {
|
||||||
fileName: fileName,
|
fileName: fileName,
|
||||||
size: size,
|
size: size,
|
||||||
tempName: '',
|
tempName: '',
|
||||||
|
type: '', // application/octet-stream
|
||||||
|
|
||||||
progress: 0,
|
progress: 0,
|
||||||
error: '',
|
error: '',
|
||||||
|
|
@ -54,7 +55,7 @@ export class ComposeAttachmentModel extends AbstractModel {
|
||||||
return null === localSize ? '' : FileInfo.friendlySize(localSize);
|
return null === localSize ? '' : FileInfo.friendlySize(localSize);
|
||||||
},
|
},
|
||||||
|
|
||||||
mimeType: () => FileInfo.getContentType(this.fileName()),
|
mimeType: () => this.type() || FileInfo.getContentType(this.fileName()),
|
||||||
fileExt: () => FileInfo.getExtension(this.fileName()),
|
fileExt: () => FileInfo.getExtension(this.fileName()),
|
||||||
|
|
||||||
iconClass: () => FileInfo.getIconClass(this.fileExt(), this.mimeType())
|
iconClass: () => FileInfo.getIconClass(this.fileExt(), this.mimeType())
|
||||||
|
|
|
||||||
|
|
@ -119,7 +119,7 @@ export class ContactModel extends AbstractModel {
|
||||||
|
|
||||||
display: () => {
|
display: () => {
|
||||||
let a = this.fullName(),
|
let a = this.fullName(),
|
||||||
b = this.email()?.[0]?.value,
|
b = this.email()?.[0]?.value(),
|
||||||
c = this.nickname();
|
c = this.nickname();
|
||||||
return a || b || c;
|
return a || b || c;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,6 @@ export class LoginAdminScreen extends AbstractScreen {
|
||||||
}
|
}
|
||||||
|
|
||||||
onShow() {
|
onShow() {
|
||||||
rl.setWindowTitle();
|
rl.setTitle();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,6 @@ export class SettingsAdminScreen extends AbstractSettingsScreen {
|
||||||
}
|
}
|
||||||
|
|
||||||
onShow() {
|
onShow() {
|
||||||
rl.setWindowTitle();
|
rl.setTitle();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,6 @@ export class LoginUserScreen extends AbstractScreen {
|
||||||
}
|
}
|
||||||
|
|
||||||
onShow() {
|
onShow() {
|
||||||
rl.setWindowTitle();
|
rl.setTitle();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import { Scope } from 'Common/Enums';
|
import { Scope } from 'Common/Enums';
|
||||||
import { Layout, ClientSideKeyNameMessageListSize } from 'Common/EnumsUser';
|
import { Layout, ClientSideKeyNameMessageListSize } from 'Common/EnumsUser';
|
||||||
import { doc, createElement, leftPanelDisabled, moveAction, Settings, elementById } from 'Common/Globals';
|
import { doc, createElement, leftPanelDisabled, Settings, elementById } from 'Common/Globals';
|
||||||
import { pString, pInt } from 'Common/Utils';
|
import { pString, pInt } from 'Common/Utils';
|
||||||
import { setLayoutResizer } from 'Common/UtilsUser';
|
import { setLayoutResizer, moveAction } from 'Common/UtilsUser';
|
||||||
import { getFolderFromCacheList, getFolderFullName, getFolderInboxName } from 'Common/Cache';
|
import { getFolderFromCacheList, getFolderFullName, getFolderInboxName } from 'Common/Cache';
|
||||||
import { i18n, initOnStartOrLangChange } from 'Common/Translator';
|
import { i18n, initOnStartOrLangChange } from 'Common/Translator';
|
||||||
import { SettingsUserStore } from 'Stores/User/Settings';
|
import { SettingsUserStore } from 'Stores/User/Settings';
|
||||||
|
|
@ -46,7 +46,7 @@ export class MailBoxUserScreen extends AbstractScreen {
|
||||||
const count = Settings.app('listPermanentFiltered') ? 0 : FolderUserStore.foldersInboxUnreadCount(),
|
const count = Settings.app('listPermanentFiltered') ? 0 : FolderUserStore.foldersInboxUnreadCount(),
|
||||||
email = AccountUserStore.email();
|
email = AccountUserStore.email();
|
||||||
|
|
||||||
rl.setWindowTitle(
|
rl.setTitle(
|
||||||
(email
|
(email
|
||||||
? '' + (0 < count ? '(' + count + ') ' : ' ') + email + ' - '
|
? '' + (0 < count ? '(' + count + ') ' : ' ') + email + ' - '
|
||||||
: ''
|
: ''
|
||||||
|
|
|
||||||
|
|
@ -72,6 +72,6 @@ export class SettingsUserScreen extends AbstractSettingsScreen {
|
||||||
|
|
||||||
setSettingsTitle() {
|
setSettingsTitle() {
|
||||||
const sEmail = AccountUserStore.email();
|
const sEmail = AccountUserStore.email();
|
||||||
rl.setWindowTitle((sEmail ? sEmail + ' - ' : '') + this.sSettingsTitle);
|
rl.setTitle((sEmail ? sEmail + ' - ' : '') + this.sSettingsTitle);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,10 +11,26 @@ import { DomainAliasPopupView } from 'View/Popup/DomainAlias';
|
||||||
export class AdminSettingsDomains /*extends AbstractViewSettings*/ {
|
export class AdminSettingsDomains /*extends AbstractViewSettings*/ {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.domains = DomainAdminStore;
|
this.domains = DomainAdminStore;
|
||||||
|
this.username = ko.observable('');
|
||||||
this.domainForDeletion = ko.observable(null).askDeleteHelper();
|
this.domainForDeletion = ko.observable(null).askDeleteHelper();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
testUsername() {
|
||||||
|
// TODO: find domain matching username
|
||||||
|
Remote.request('AdminDomainMatch',
|
||||||
|
(iError, oData) => {
|
||||||
|
if (oData?.Result?.domain) {
|
||||||
|
alert(`${oData.Result.domain.email} matched domain: ${oData.Result.domain.name}`);
|
||||||
|
} else {
|
||||||
|
alert('No domain match');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
username: this.username()
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
createDomain() {
|
createDomain() {
|
||||||
showScreenPopup(DomainPopupView);
|
showScreenPopup(DomainPopupView);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -42,8 +42,8 @@ export class UserSettingsGeneral extends AbstractViewSettings {
|
||||||
|
|
||||||
['layout', 'messageReadDelay', 'messagesPerPage',
|
['layout', 'messageReadDelay', 'messagesPerPage',
|
||||||
'editorDefaultType', 'requestReadReceipt', 'requestDsn', 'pgpSign', 'pgpEncrypt',
|
'editorDefaultType', 'requestReadReceipt', 'requestDsn', 'pgpSign', 'pgpEncrypt',
|
||||||
'viewHTML', 'showImages', 'removeColors', 'hideDeleted', 'listInlineAttachments',
|
'viewHTML', 'showImages', 'removeColors', 'hideDeleted', 'listInlineAttachments', 'simpleAttachmentsList',
|
||||||
'useCheckboxesInList', 'useThreads', 'replySameFolder', 'msgDefaultAction'
|
'useCheckboxesInList', 'useThreads', 'replySameFolder', 'msgDefaultAction', 'allowSpellcheck'
|
||||||
].forEach(name => this[name] = SettingsUserStore[name]);
|
].forEach(name => this[name] = SettingsUserStore[name]);
|
||||||
|
|
||||||
this.allowLanguagesOnSettings = !!SettingsGet('AllowLanguagesOnSettings');
|
this.allowLanguagesOnSettings = !!SettingsGet('AllowLanguagesOnSettings');
|
||||||
|
|
@ -97,9 +97,9 @@ export class UserSettingsGeneral extends AbstractViewSettings {
|
||||||
this.addSetting('MessagesPerPage');
|
this.addSetting('MessagesPerPage');
|
||||||
this.addSetting('Layout');
|
this.addSetting('Layout');
|
||||||
|
|
||||||
this.addSettings(['ViewHTML', 'ShowImages', 'HideDeleted', 'ListInlineAttachments',
|
this.addSettings(['ViewHTML', 'ShowImages', 'HideDeleted', 'ListInlineAttachments', 'simpleAttachmentsList',
|
||||||
'UseCheckboxesInList', 'ReplySameFolder',
|
'UseCheckboxesInList', 'ReplySameFolder',
|
||||||
'requestReadReceipt', 'requestDsn', 'pgpSign', 'pgpEncrypt',
|
'requestReadReceipt', 'requestDsn', 'pgpSign', 'pgpEncrypt', 'allowSpellcheck',
|
||||||
'DesktopNotifications', 'SoundNotification']);
|
'DesktopNotifications', 'SoundNotification']);
|
||||||
|
|
||||||
const fReloadLanguageHelper = (saveSettingsStep) => () => {
|
const fReloadLanguageHelper = (saveSettingsStep) => () => {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import ko from 'ko';
|
import ko from 'ko';
|
||||||
import { doc, $htmlCL, elementById, leftPanelDisabled, Settings, SettingsGet } from 'Common/Globals';
|
import { $htmlCL, elementById, leftPanelDisabled, Settings, SettingsGet } from 'Common/Globals';
|
||||||
import { isArray, arrayLength } from 'Common/Utils';
|
import { isArray, arrayLength } from 'Common/Utils';
|
||||||
import { serverRequestRaw } from 'Common/Links';
|
import { serverRequestRaw } from 'Common/Links';
|
||||||
import { SaveSettingStatus } from 'Common/Enums';
|
import { SaveSettingStatus } from 'Common/Enums';
|
||||||
|
|
@ -7,6 +7,8 @@ import { SaveSettingStatus } from 'Common/Enums';
|
||||||
let __themeTimer = 0;
|
let __themeTimer = 0;
|
||||||
|
|
||||||
export const
|
export const
|
||||||
|
appEl = () => elementById('rl-app'),
|
||||||
|
|
||||||
ThemeStore = {
|
ThemeStore = {
|
||||||
theme: ko.observable(''),
|
theme: ko.observable(''),
|
||||||
themes: ko.observableArray(),
|
themes: ko.observableArray(),
|
||||||
|
|
@ -65,7 +67,7 @@ ThemeStore.isMobile.subscribe(value => $htmlCL.toggle('rl-mobile', value));
|
||||||
|
|
||||||
ThemeStore.fontSansSerif.subscribe(value => {
|
ThemeStore.fontSansSerif.subscribe(value => {
|
||||||
if (null != value) {
|
if (null != value) {
|
||||||
let cl = elementById('rl-app').classList;
|
let cl = appEl().classList;
|
||||||
cl.forEach(name => {
|
cl.forEach(name => {
|
||||||
if (name.startsWith('font') && !/font(Serif|Mono)/.test(name)) {
|
if (name.startsWith('font') && !/font(Serif|Mono)/.test(name)) {
|
||||||
cl.remove(name);
|
cl.remove(name);
|
||||||
|
|
@ -76,25 +78,20 @@ ThemeStore.fontSansSerif.subscribe(value => {
|
||||||
});
|
});
|
||||||
ThemeStore.fontSerif.subscribe(value => {
|
ThemeStore.fontSerif.subscribe(value => {
|
||||||
if (null != value) {
|
if (null != value) {
|
||||||
let cl = elementById('rl-app').classList;
|
let cl = appEl().classList;
|
||||||
cl.forEach(name => name.startsWith('fontSerif') && cl.remove(name));
|
cl.forEach(name => name.startsWith('fontSerif') && cl.remove(name));
|
||||||
value && cl.add('fontSerif'+value);
|
value && cl.add('fontSerif'+value);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
ThemeStore.fontMono.subscribe(value => {
|
ThemeStore.fontMono.subscribe(value => {
|
||||||
if (null != value) {
|
if (null != value) {
|
||||||
let cl = elementById('rl-app').classList;
|
let cl = appEl().classList;
|
||||||
cl.forEach(name => name.startsWith('fontMono') && cl.remove(name));
|
cl.forEach(name => name.startsWith('fontMono') && cl.remove(name));
|
||||||
value && cl.add('fontMono'+value);
|
value && cl.add('fontMono'+value);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ThemeStore.userBackgroundHash.subscribe(value => {
|
ThemeStore.userBackgroundHash.subscribe(value => {
|
||||||
if (value) {
|
appEl().classList.toggle('UserBackground', !!value);
|
||||||
$htmlCL.add('UserBackground');
|
appEl().style.backgroundImage = value ? "url("+serverRequestRaw('UserBackground', value)+")" : null;
|
||||||
doc.body.style.backgroundImage = "url("+serverRequestRaw('UserBackground', value)+")";
|
|
||||||
} else {
|
|
||||||
$htmlCL.remove('UserBackground');
|
|
||||||
doc.body.removeAttribute('style');
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import ko from 'ko';
|
import ko from 'ko';
|
||||||
import { SettingsGet } from 'Common/Globals';
|
import { SettingsGet } from 'Common/Globals';
|
||||||
import { pInt } from 'Common/Utils';
|
|
||||||
import { koComputable, addObservablesTo, koArrayWithDestroy } from 'External/ko';
|
import { koComputable, addObservablesTo, koArrayWithDestroy } from 'External/ko';
|
||||||
import Remote from 'Remote/User/Fetch';
|
import Remote from 'Remote/User/Fetch';
|
||||||
|
|
||||||
|
|
@ -51,16 +50,14 @@ ContactUserStore.sync = fResultFunc => {
|
||||||
};
|
};
|
||||||
|
|
||||||
ContactUserStore.init = () => {
|
ContactUserStore.init = () => {
|
||||||
let value = !!SettingsGet('ContactsSyncIsAllowed');
|
let config = SettingsGet('ContactsSync');
|
||||||
ContactUserStore.allowSync(value);
|
ContactUserStore.allowSync(!!config);
|
||||||
if (value) {
|
if (config) {
|
||||||
ContactUserStore.syncMode(SettingsGet('ContactsSyncMode'));
|
ContactUserStore.syncMode(config.Mode);
|
||||||
ContactUserStore.syncUrl(SettingsGet('ContactsSyncUrl'));
|
ContactUserStore.syncUrl(config.Url);
|
||||||
ContactUserStore.syncUser(SettingsGet('ContactsSyncUser'));
|
ContactUserStore.syncUser(config.User);
|
||||||
ContactUserStore.syncPass(SettingsGet('ContactsSyncPassword'));
|
ContactUserStore.syncPass(config.Password);
|
||||||
setTimeout(ContactUserStore.sync, 10000);
|
setTimeout(ContactUserStore.sync, 10000);
|
||||||
value = pInt(SettingsGet('ContactsSyncInterval'));
|
setInterval(ContactUserStore.sync, config.Interval * 60000 + 5000);
|
||||||
value = 5 <= value ? (320 >= value ? value : 320) : 20;
|
|
||||||
setInterval(ContactUserStore.sync, value * 60000 + 5000);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -186,7 +186,7 @@ export const GnuPGUserStore = new class {
|
||||||
}
|
}
|
||||||
if (null !== params.Passphrase) {
|
if (null !== params.Passphrase) {
|
||||||
const result = await Remote.post('GnupgDecrypt', null, params);
|
const result = await Remote.post('GnupgDecrypt', null, params);
|
||||||
if (result?.Result) {
|
if (result?.Result && false !== result.Result.data) {
|
||||||
return result.Result;
|
return result.Result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -347,7 +347,7 @@ MessagelistUserStore.removeMessagesFromList = (
|
||||||
fromFolderFullName, oUids, toFolderFullName = '', copy = false
|
fromFolderFullName, oUids, toFolderFullName = '', copy = false
|
||||||
) => {
|
) => {
|
||||||
let unseenCount = 0,
|
let unseenCount = 0,
|
||||||
messageList = MessagelistUserStore,
|
setPage = 0,
|
||||||
currentMessage = MessageUserStore.message();
|
currentMessage = MessageUserStore.message();
|
||||||
|
|
||||||
const trashFolder = FolderUserStore.trashFolder(),
|
const trashFolder = FolderUserStore.trashFolder(),
|
||||||
|
|
@ -356,7 +356,7 @@ MessagelistUserStore.removeMessagesFromList = (
|
||||||
toFolder = toFolderFullName ? getFolderFromCacheList(toFolderFullName) : null,
|
toFolder = toFolderFullName ? getFolderFromCacheList(toFolderFullName) : null,
|
||||||
messages =
|
messages =
|
||||||
FolderUserStore.currentFolderFullName() === fromFolderFullName
|
FolderUserStore.currentFolderFullName() === fromFolderFullName
|
||||||
? messageList.filter(item => item && oUids.has(item.uid))
|
? MessagelistUserStore.filter(item => item && oUids.has(item.uid))
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
messages.forEach(item => item?.isUnseen() && ++unseenCount);
|
messages.forEach(item => item?.isUnseen() && ++unseenCount);
|
||||||
|
|
@ -406,47 +406,47 @@ MessagelistUserStore.removeMessagesFromList = (
|
||||||
item.deleted(true);
|
item.deleted(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
setTimeout(() => messages.forEach(item => messageList.remove(item)), 350);
|
setTimeout(() => messages.forEach(item => MessagelistUserStore.remove(item)), 350);
|
||||||
|
|
||||||
|
const
|
||||||
|
count = MessagelistUserStore.count() - messages.length,
|
||||||
|
page = MessagelistUserStore.page();
|
||||||
|
MessagelistUserStore.count(count);
|
||||||
|
if (page > MessagelistUserStore.pageCount()) {
|
||||||
|
setPage = MessagelistUserStore.pageCount();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (MessagelistUserStore.threadUid()) {
|
if (MessagelistUserStore.threadUid()
|
||||||
if (
|
&& MessagelistUserStore.length
|
||||||
messageList.length &&
|
&& MessagelistUserStore.find(item => item?.deleted() && item.uid == MessagelistUserStore.threadUid())
|
||||||
messageList.find(item => item?.deleted() && item.uid == MessagelistUserStore.threadUid())
|
|
||||||
) {
|
) {
|
||||||
const message = messageList.find(item => item && !item.deleted());
|
const message = MessagelistUserStore.find(item => item && !item.deleted());
|
||||||
let setHash;
|
|
||||||
if (!message) {
|
if (!message) {
|
||||||
if (1 < MessagelistUserStore.page()) {
|
if (1 < MessagelistUserStore.page()) {
|
||||||
MessagelistUserStore.page(MessagelistUserStore.page() - 1);
|
setPage = MessagelistUserStore.page() - 1;
|
||||||
setHash = 1;
|
|
||||||
} else {
|
} else {
|
||||||
MessagelistUserStore.threadUid(0);
|
MessagelistUserStore.threadUid(0);
|
||||||
replaceHash(
|
setPage = MessagelistUserStore.pageBeforeThread();
|
||||||
mailBox(
|
|
||||||
FolderUserStore.currentFolderFullNameHash(),
|
|
||||||
MessagelistUserStore.pageBeforeThread(),
|
|
||||||
MessagelistUserStore.listSearch()
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
} else if (MessagelistUserStore.threadUid() != message.uid) {
|
} else if (MessagelistUserStore.threadUid() != message.uid) {
|
||||||
MessagelistUserStore.threadUid(message.uid);
|
MessagelistUserStore.threadUid(message.uid);
|
||||||
setHash = 1;
|
setPage = MessagelistUserStore.page();
|
||||||
}
|
}
|
||||||
if (setHash) {
|
}
|
||||||
|
|
||||||
|
if (setPage) {
|
||||||
|
MessagelistUserStore.page(setPage);
|
||||||
replaceHash(
|
replaceHash(
|
||||||
mailBox(
|
mailBox(
|
||||||
FolderUserStore.currentFolderFullNameHash(),
|
FolderUserStore.currentFolderFullNameHash(),
|
||||||
MessagelistUserStore.page(),
|
setPage,
|
||||||
MessagelistUserStore.listSearch(),
|
MessagelistUserStore.listSearch(),
|
||||||
MessagelistUserStore.threadUid()
|
MessagelistUserStore.threadUid()
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
MessagelistUserStore.reloadFlagsAndCachedMessage = () => {
|
MessagelistUserStore.reloadFlagsAndCachedMessage = () => {
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ export const SettingsUserStore = new class {
|
||||||
showImages: 0,
|
showImages: 0,
|
||||||
removeColors: 0,
|
removeColors: 0,
|
||||||
listInlineAttachments: 0,
|
listInlineAttachments: 0,
|
||||||
|
simpleAttachmentsList: 0,
|
||||||
useCheckboxesInList: 1,
|
useCheckboxesInList: 1,
|
||||||
allowDraftAutosave: 1,
|
allowDraftAutosave: 1,
|
||||||
useThreads: 0,
|
useThreads: 0,
|
||||||
|
|
@ -32,6 +33,7 @@ export const SettingsUserStore = new class {
|
||||||
requestDsn: 0,
|
requestDsn: 0,
|
||||||
pgpSign: 0,
|
pgpSign: 0,
|
||||||
pgpEncrypt: 0,
|
pgpEncrypt: 0,
|
||||||
|
allowSpellcheck: 0,
|
||||||
|
|
||||||
layout: 1,
|
layout: 1,
|
||||||
editorDefaultType: 'Html',
|
editorDefaultType: 'Html',
|
||||||
|
|
@ -79,6 +81,7 @@ export const SettingsUserStore = new class {
|
||||||
self.showImages(SettingsGet('ShowImages'));
|
self.showImages(SettingsGet('ShowImages'));
|
||||||
self.removeColors(SettingsGet('RemoveColors'));
|
self.removeColors(SettingsGet('RemoveColors'));
|
||||||
self.listInlineAttachments(SettingsGet('ListInlineAttachments'));
|
self.listInlineAttachments(SettingsGet('ListInlineAttachments'));
|
||||||
|
self.simpleAttachmentsList(SettingsGet('simpleAttachmentsList'));
|
||||||
self.useCheckboxesInList(SettingsGet('UseCheckboxesInList'));
|
self.useCheckboxesInList(SettingsGet('UseCheckboxesInList'));
|
||||||
self.allowDraftAutosave(SettingsGet('AllowDraftAutosave'));
|
self.allowDraftAutosave(SettingsGet('AllowDraftAutosave'));
|
||||||
self.useThreads(SettingsGet('UseThreads'));
|
self.useThreads(SettingsGet('UseThreads'));
|
||||||
|
|
@ -92,5 +95,6 @@ export const SettingsUserStore = new class {
|
||||||
self.requestDsn(SettingsGet('requestDsn'));
|
self.requestDsn(SettingsGet('requestDsn'));
|
||||||
self.pgpSign(SettingsGet('pgpSign'));
|
self.pgpSign(SettingsGet('pgpSign'));
|
||||||
self.pgpEncrypt(SettingsGet('pgpEncrypt'));
|
self.pgpEncrypt(SettingsGet('pgpEncrypt'));
|
||||||
|
self.allowSpellcheck(SettingsGet('allowSpellcheck'));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -38,12 +38,26 @@
|
||||||
html, body {
|
html, body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
-webkit-text-size-adjust: 100%; /* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
#rl-app *, #rl-app *::before, #rl-app *::after {
|
||||||
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
#rl-app {
|
#rl-app {
|
||||||
background-color: var(--main-bg-color, #aaa);
|
background-color: var(--main-bg-color, #aaa);
|
||||||
color: var(--main-color, #333);
|
color: var(--main-color, #333);
|
||||||
font-family: var(--fontSans);
|
font-family: var(--fontSans);
|
||||||
|
|
||||||
|
-webkit-touch-callout: none;
|
||||||
|
font-size: var(--main-font-size, 14px);
|
||||||
|
line-height: 1.25; /* 1 */
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
bottom: 0;
|
||||||
|
right: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
#rl-loading, #rl-loading-error {
|
#rl-loading, #rl-loading-error {
|
||||||
|
|
|
||||||
|
|
@ -57,11 +57,7 @@
|
||||||
@import "_End.less";
|
@import "_End.less";
|
||||||
|
|
||||||
@media print {
|
@media print {
|
||||||
body, html {
|
#rl-app, #rl-right {
|
||||||
height: auto;
|
|
||||||
overflow: auto;
|
|
||||||
}
|
|
||||||
body, #rl-right {
|
|
||||||
position: initial;
|
position: initial;
|
||||||
}
|
}
|
||||||
#rl-left {
|
#rl-left {
|
||||||
|
|
|
||||||
|
|
@ -7,12 +7,10 @@
|
||||||
.domain-name {
|
.domain-name {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.domain-alias {
|
.domain-alias {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
box-sizing: border-box;
|
|
||||||
color: #bbb;
|
color: #bbb;
|
||||||
padding-left: 5px;
|
padding-left: 5px;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -45,17 +45,15 @@
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
||||||
> .b-toolbar {
|
> .b-toolbar {
|
||||||
|
color: #fff;
|
||||||
|
display: flex;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
height: 34px;
|
|
||||||
padding: 8px @rlLowMargin 8px 0;
|
padding: 8px @rlLowMargin 8px 0;
|
||||||
color: #fff;
|
|
||||||
text-shadow: 0 1px 1px #000;
|
text-shadow: 0 1px 1px #000;
|
||||||
|
|
||||||
display: flex;
|
|
||||||
|
|
||||||
h4 {
|
h4 {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
margin: 8px;
|
margin: 8px;
|
||||||
|
|
@ -73,7 +71,6 @@
|
||||||
border: 1px solid var(--border-color, #aaa);
|
border: 1px solid var(--border-color, #aaa);
|
||||||
border-radius: var(--border-radius, 5px);
|
border-radius: var(--border-radius, 5px);
|
||||||
box-shadow: @rlMainShadow;
|
box-shadow: @rlMainShadow;
|
||||||
box-sizing: border-box;
|
|
||||||
height: calc(100% - 50px - @rlLowMargin - @rlLowMargin);
|
height: calc(100% - 50px - @rlLowMargin - @rlLowMargin);
|
||||||
margin: (50px + @rlLowMargin) @rlLowMargin @rlLowMargin 0;
|
margin: (50px + @rlLowMargin) @rlLowMargin @rlLowMargin 0;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
|
|
||||||
|
|
@ -2,17 +2,6 @@
|
||||||
|
|
||||||
max-width: 660px;
|
max-width: 660px;
|
||||||
|
|
||||||
.information {
|
|
||||||
display: inline-block;
|
|
||||||
background-color: #ddd;
|
|
||||||
border-radius: 10px;
|
|
||||||
cursor: pointer;
|
|
||||||
height: 25px;
|
|
||||||
width: 30px;
|
|
||||||
text-align: center;
|
|
||||||
padding-top: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
textarea {
|
textarea {
|
||||||
width: 400px;
|
width: 400px;
|
||||||
height: 70px;
|
height: 70px;
|
||||||
|
|
|
||||||
|
|
@ -80,7 +80,6 @@
|
||||||
|
|
||||||
> div > div {
|
> div > div {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
box-sizing: border-box;
|
|
||||||
margin-top: 1px;
|
margin-top: 1px;
|
||||||
|
|
||||||
animation: checkmark-to-box 200ms ease-out forwards;
|
animation: checkmark-to-box 200ms ease-out forwards;
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
padding: 5px 15px;
|
padding: 5px 15px;
|
||||||
margin: 2px 5px;
|
margin: 2px 5px;
|
||||||
width: 180px;
|
width: 216px;
|
||||||
background-color: rgba(128, 128, 128, 0.1);
|
background-color: rgba(128, 128, 128, 0.1);
|
||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
border-radius: var(--input-border-radius, 3px);
|
border-radius: var(--input-border-radius, 3px);
|
||||||
|
|
@ -34,8 +34,8 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 999px) {
|
@media screen and (max-width: 733px) {
|
||||||
#V-PopupsLanguages label {
|
#V-PopupsLanguages label {
|
||||||
width: calc(100% - 40px);
|
width: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -112,7 +112,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.buttonLogin {
|
.buttonLogin {
|
||||||
margin: 0;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.alert {
|
.alert {
|
||||||
|
|
|
||||||
|
|
@ -3,23 +3,6 @@
|
||||||
* 2. Prevent adjustments of font size after orientation changes in iOS.
|
* 2. Prevent adjustments of font size after orientation changes in iOS.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
html, body {
|
|
||||||
height: 100%;
|
|
||||||
overflow: hidden;
|
|
||||||
-webkit-text-size-adjust: 100%; /* 2 */
|
|
||||||
}
|
|
||||||
|
|
||||||
#rl-app {
|
|
||||||
-webkit-touch-callout: none;
|
|
||||||
font-size: var(--main-font-size, 14px);
|
|
||||||
line-height: 1.15; /* 1 */
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
bottom: 0;
|
|
||||||
right: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
html.list-loading body {
|
html.list-loading body {
|
||||||
/* cursor: wait;*/
|
/* cursor: wait;*/
|
||||||
cursor: progress;
|
cursor: progress;
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,6 @@
|
||||||
.settings-save-trigger {
|
.settings-save-trigger {
|
||||||
|
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
height: 1em;
|
|
||||||
line-height: 1em;
|
line-height: 1em;
|
||||||
margin-left: 0.5em !important;
|
margin-left: 0.5em !important;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -83,7 +83,6 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.iconMain, .iconPreview {
|
.iconMain, .iconPreview {
|
||||||
box-sizing: border-box;
|
|
||||||
padding: 6px 0 0;
|
padding: 6px 0 0;
|
||||||
font-size: 36px;
|
font-size: 36px;
|
||||||
color: #aaa;
|
color: #aaa;
|
||||||
|
|
@ -120,3 +119,22 @@
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.attachmentListSimple {
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
|
.attachmentItem {
|
||||||
|
min-height: 1em;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
padding: 0 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attachmentName {
|
||||||
|
margin:0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkboxAttachment {
|
||||||
|
position: initial;
|
||||||
|
margin-left:.5em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,6 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.textAreaParent, .attachmentAreaParent {
|
.textAreaParent, .attachmentAreaParent {
|
||||||
box-sizing: border-box;
|
|
||||||
height: 100%;
|
height: 100%;
|
||||||
min-height: 200px;
|
min-height: 200px;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
@ -150,6 +149,8 @@
|
||||||
@media screen and (max-width: 999px) {
|
@media screen and (max-width: 999px) {
|
||||||
#V-PopupsCompose {
|
#V-PopupsCompose {
|
||||||
border: 0;
|
border: 0;
|
||||||
|
height: calc(100vh - 100px);
|
||||||
|
margin: 0 auto;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@
|
||||||
border: 1px solid var(--input-border-clr, #ccc);
|
border: 1px solid var(--input-border-clr, #ccc);
|
||||||
border-radius: var(--input-border-radius, 3px);
|
border-radius: var(--input-border-radius, 3px);
|
||||||
box-shadow: inset 0 1px 1px rgba(0,0,0,.075);
|
box-shadow: inset 0 1px 1px rgba(0,0,0,.075);
|
||||||
box-sizing: border-box;
|
|
||||||
color: var(--input-clr, #555);
|
color: var(--input-clr, #555);
|
||||||
cursor: text;
|
cursor: text;
|
||||||
font-size: 90%;
|
font-size: 90%;
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,6 @@
|
||||||
|
|
||||||
.b-toolbar {
|
.b-toolbar {
|
||||||
top: 0;
|
top: 0;
|
||||||
height: 30px;
|
|
||||||
padding: 10px 0 0 @rlLowMargin;
|
padding: 10px 0 0 @rlLowMargin;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
z-index: 101;
|
z-index: 101;
|
||||||
|
|
@ -39,7 +38,6 @@
|
||||||
|
|
||||||
.b-footer {
|
.b-footer {
|
||||||
bottom: 10px;
|
bottom: 10px;
|
||||||
height: 30px;
|
|
||||||
padding: 0 10px 0 5px;
|
padding: 0 10px 0 5px;
|
||||||
z-index: 101;
|
z-index: 101;
|
||||||
|
|
||||||
|
|
@ -69,7 +67,6 @@
|
||||||
display: block;
|
display: block;
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
height: 34px;
|
|
||||||
line-height: 34px;
|
line-height: 34px;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
|
|
@ -222,8 +219,7 @@
|
||||||
max-width: -webkit-fit-content;
|
max-width: -webkit-fit-content;
|
||||||
max-width: fit-content;
|
max-width: fit-content;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
height: 19px;
|
max-height: 2em;
|
||||||
padding: 4px 8px;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
|
|
||||||
.UserBackground body {
|
#rl-app.UserBackground {
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
background-position: center;
|
background-position: center;
|
||||||
|
|
@ -105,7 +105,7 @@ html:not(.rl-left-panel-disabled) #rl-left > .resizer,
|
||||||
|
|
||||||
#V-MailMessageView {
|
#V-MailMessageView {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 50px + @rlLowMargin;
|
top: 50px;
|
||||||
bottom: 13px;
|
bottom: 13px;
|
||||||
right: 0;
|
right: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,6 @@ html.rl-no-preview-pane {
|
||||||
}
|
}
|
||||||
|
|
||||||
#V-MailMessageList .btn-toolbar {
|
#V-MailMessageList .btn-toolbar {
|
||||||
height: 30px;
|
|
||||||
padding: 10px 1px;
|
padding: 10px 1px;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ html.rl-no-preview-pane {
|
||||||
|
|
||||||
.toolbar {
|
.toolbar {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: -50px - @rlLowMargin;
|
top: -50px;
|
||||||
right: 0;
|
right: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
height: 30px;
|
height: 30px;
|
||||||
|
|
@ -44,9 +44,8 @@ html.rl-no-preview-pane {
|
||||||
.b-message-view-checked-helper {
|
.b-message-view-checked-helper {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-size: 70px;
|
font-size: 70px;
|
||||||
line-height: 70px;
|
padding-top: 25vh;
|
||||||
padding-top: 100px;
|
opacity: 0.6;
|
||||||
color: #999;
|
|
||||||
}
|
}
|
||||||
.b-message-view-checked-helper::after {
|
.b-message-view-checked-helper::after {
|
||||||
content: ' ✉';
|
content: ' ✉';
|
||||||
|
|
@ -58,7 +57,7 @@ html.rl-no-preview-pane {
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
line-height: 30px;
|
line-height: 30px;
|
||||||
color: #999;
|
color: #999;
|
||||||
padding: 120px 10px 0 10px;
|
padding: 25vh 10px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.b-message-view-desc.error {
|
.b-message-view-desc.error {
|
||||||
|
|
@ -95,7 +94,6 @@ html.rl-no-preview-pane {
|
||||||
}
|
}
|
||||||
|
|
||||||
.messageTags {
|
.messageTags {
|
||||||
margin-top: 6px;
|
|
||||||
align-items: baseline;
|
align-items: baseline;
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
@ -128,7 +126,6 @@ html.rl-no-preview-pane {
|
||||||
display: flex;
|
display: flex;
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.subject {
|
.subject {
|
||||||
|
|
@ -148,7 +145,7 @@ html.rl-no-preview-pane {
|
||||||
}
|
}
|
||||||
|
|
||||||
.informationShort {
|
.informationShort {
|
||||||
margin: 4px 40px 0 0;
|
margin: 0 40px 0 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
|
|
||||||
|
|
@ -253,33 +250,6 @@ html.rl-no-preview-pane {
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
scroll-behavior: smooth;
|
scroll-behavior: smooth;
|
||||||
|
|
||||||
.buttonFull {
|
|
||||||
display: inline-block;
|
|
||||||
position: fixed;
|
|
||||||
right: 8px;
|
|
||||||
bottom: 8px;
|
|
||||||
height: 30px;
|
|
||||||
width: 30px;
|
|
||||||
text-align: center;
|
|
||||||
vertical-align: middle;
|
|
||||||
line-height: 30px;
|
|
||||||
background-color: transparent;
|
|
||||||
background-color: #fff;
|
|
||||||
border: 1px solid #333;
|
|
||||||
color: #333;
|
|
||||||
z-index: 2;
|
|
||||||
cursor: pointer;
|
|
||||||
border-radius: var(--border-radius, 5px);
|
|
||||||
opacity: 0.5;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
opacity: 0.8;
|
|
||||||
border-color: #666;
|
|
||||||
background-color: #888;
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading {
|
.loading {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
|
|
@ -345,6 +315,7 @@ html.rl-no-preview-pane {
|
||||||
border-bottom: 1px solid var(--border-color, #bbb);
|
border-bottom: 1px solid var(--border-color, #bbb);
|
||||||
}
|
}
|
||||||
.attachmentsControls > * {
|
.attachmentsControls > * {
|
||||||
|
display: inline-block;
|
||||||
margin-right: 0.5em;
|
margin-right: 0.5em;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
@ -539,9 +510,3 @@ html.rl-fullscreen {
|
||||||
border: 0;
|
border: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
html:not(.rl-mobile) #messageItem {
|
|
||||||
.buttonFull {
|
|
||||||
display: none !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -27,11 +27,13 @@
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
display: block;
|
display: block;
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
height: 30px;
|
line-height: 2em;
|
||||||
line-height: 29px;
|
max-height: 2em;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
padding: 4px 10px;
|
padding: 0 10px;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
a:focus, a:hover {
|
a:focus, a:hover {
|
||||||
|
|
@ -54,7 +56,6 @@
|
||||||
top: 0;
|
top: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
height: 34px;
|
|
||||||
padding: 8px 0;
|
padding: 8px 0;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
|
@ -71,7 +72,6 @@
|
||||||
scroll-behavior: smooth;
|
scroll-behavior: smooth;
|
||||||
padding:20px;
|
padding:20px;
|
||||||
|
|
||||||
box-sizing: border-box;
|
|
||||||
height: calc(100% - 50px - @rlLowMargin - @rlLowMargin);
|
height: calc(100% - 50px - @rlLowMargin - @rlLowMargin);
|
||||||
|
|
||||||
background-color: var(--settings-bg-clr, #fff);
|
background-color: var(--settings-bg-clr, #fff);
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,6 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.squire-wysiwyg, .squire-plain {
|
.squire-wysiwyg, .squire-plain {
|
||||||
box-sizing: border-box;
|
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
line-height: 16px;
|
line-height: 16px;
|
||||||
|
|
@ -163,7 +162,3 @@ Secondly, we can't rely on MUA's what to do with :empty
|
||||||
*/
|
*/
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.rl-mobile #V-PopupsCompose {
|
|
||||||
min-height: calc(100% - 12px);
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,6 @@
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
height: 30px;
|
|
||||||
padding: 10px @rlLowMargin;
|
padding: 10px @rlLowMargin;
|
||||||
z-index: 103;
|
z-index: 103;
|
||||||
|
|
||||||
|
|
@ -33,7 +32,7 @@
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
font-size: 30px;
|
font-size: 30px;
|
||||||
height: 30px;
|
height: 1em;
|
||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
width: 1em;
|
width: 1em;
|
||||||
|
|
||||||
|
|
@ -61,8 +60,7 @@
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
height: 29px;
|
line-height: 29px;
|
||||||
line-height: 30px;
|
|
||||||
max-width: 25vw;
|
max-width: 25vw;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
padding: 1px 8px;
|
padding: 1px 8px;
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@
|
||||||
[class^="icon-"],
|
[class^="icon-"],
|
||||||
[class*=" icon-"] {
|
[class*=" icon-"] {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
width: 1em;
|
min-width: 1em;
|
||||||
|
|
||||||
.disabled &,
|
.disabled &,
|
||||||
.btn[disabled] & {
|
.btn[disabled] & {
|
||||||
|
|
@ -35,8 +35,8 @@
|
||||||
|
|
||||||
.icon-spinner {
|
.icon-spinner {
|
||||||
|
|
||||||
height: 0.75em;
|
height: 1.2em;
|
||||||
width: 0.75em;
|
width: 1.2em;
|
||||||
|
|
||||||
margin: 0 -1px;
|
margin: 0 -1px;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,13 @@ import Remote from 'Remote/Admin/Fetch';
|
||||||
|
|
||||||
import { AbstractViewRight } from 'Knoin/AbstractViews';
|
import { AbstractViewRight } from 'Knoin/AbstractViews';
|
||||||
|
|
||||||
import { leftPanelDisabled } from 'Common/Globals';
|
import { leftPanelDisabled, toggleLeftPanel } from 'Common/Globals';
|
||||||
|
|
||||||
export class PaneSettingsAdminView extends AbstractViewRight {
|
export class PaneSettingsAdminView extends AbstractViewRight {
|
||||||
constructor() {
|
constructor() {
|
||||||
super('AdminPane');
|
super('AdminPane');
|
||||||
this.leftPanelDisabled = leftPanelDisabled;
|
this.leftPanelDisabled = leftPanelDisabled;
|
||||||
|
this.toggleLeftPanel = toggleLeftPanel;
|
||||||
}
|
}
|
||||||
|
|
||||||
logoutClick() {
|
logoutClick() {
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ import {
|
||||||
SetSystemFoldersNotification
|
SetSystemFoldersNotification
|
||||||
} from 'Common/EnumsUser';
|
} from 'Common/EnumsUser';
|
||||||
|
|
||||||
import { pInt, isArray, arrayLength, forEachObjectEntry } from 'Common/Utils';
|
import { pInt, isArray, arrayLength } from 'Common/Utils';
|
||||||
import { encodeHtml, HtmlEditor, htmlToPlain } from 'Common/Html';
|
import { encodeHtml, HtmlEditor, htmlToPlain } from 'Common/Html';
|
||||||
import { koArrayWithDestroy, addObservablesTo, addComputablesTo, addSubscribablesTo } from 'External/ko';
|
import { koArrayWithDestroy, addObservablesTo, addComputablesTo, addSubscribablesTo } from 'External/ko';
|
||||||
|
|
||||||
|
|
@ -214,6 +214,7 @@ export class ComposePopupView extends AbstractViewPopup {
|
||||||
|
|
||||||
this.allowContacts = AppUserStore.allowContacts();
|
this.allowContacts = AppUserStore.allowContacts();
|
||||||
this.allowIdentities = SettingsCapa('Identities');
|
this.allowIdentities = SettingsCapa('Identities');
|
||||||
|
this.allowSpellcheck = SettingsUserStore.allowSpellcheck;
|
||||||
|
|
||||||
addObservablesTo(this, {
|
addObservablesTo(this, {
|
||||||
identitiesDropdownTrigger: false,
|
identitiesDropdownTrigger: false,
|
||||||
|
|
@ -271,15 +272,11 @@ export class ComposePopupView extends AbstractViewPopup {
|
||||||
currentIdentity: IdentityUserStore()[0]
|
currentIdentity: IdentityUserStore()[0]
|
||||||
});
|
});
|
||||||
|
|
||||||
// this.to.subscribe((v) => console.log(v));
|
|
||||||
|
|
||||||
// Used by ko.bindingHandlers.emailsTags
|
// Used by ko.bindingHandlers.emailsTags
|
||||||
this.to.focused = ko.observable(false);
|
['to','cc','bcc'].forEach(name => {
|
||||||
this.to.focused.subscribe(value => value && (this.sLastFocusedField = 'to'));
|
this[name].focused = ko.observable(false);
|
||||||
this.cc.focused = ko.observable(false);
|
this[name].focused.subscribe(value => value && (this.sLastFocusedField = name));
|
||||||
this.cc.focused.subscribe(value => value && (this.sLastFocusedField = 'cc'));
|
});
|
||||||
this.bcc.focused = ko.observable(false);
|
|
||||||
this.bcc.focused.subscribe(value => value && (this.sLastFocusedField = 'bcc'));
|
|
||||||
|
|
||||||
this.attachments = koArrayWithDestroy();
|
this.attachments = koArrayWithDestroy();
|
||||||
|
|
||||||
|
|
@ -976,28 +973,22 @@ export class ComposePopupView extends AbstractViewPopup {
|
||||||
if (arrayLength(downloads)) {
|
if (arrayLength(downloads)) {
|
||||||
Remote.request('MessageUploadAttachments',
|
Remote.request('MessageUploadAttachments',
|
||||||
(iError, oData) => {
|
(iError, oData) => {
|
||||||
if (!iError) {
|
const result = oData?.Result;
|
||||||
forEachObjectEntry(oData.Result, (tempName, id) => {
|
downloads.forEach((id, index) => {
|
||||||
const attachment = this.getAttachmentById(id);
|
const attachment = this.getAttachmentById(id);
|
||||||
if (attachment) {
|
if (attachment) {
|
||||||
attachment.tempName(tempName);
|
|
||||||
attachment
|
attachment
|
||||||
.waiting(false)
|
.waiting(false)
|
||||||
.uploading(false)
|
.uploading(false)
|
||||||
.complete(true);
|
.complete(true);
|
||||||
}
|
if (iError || !result?.[index]) {
|
||||||
});
|
attachment.error(getUploadErrorDescByCode(UploadErrorCode.NoFileUploaded));
|
||||||
} else {
|
} else {
|
||||||
this.attachments.forEach(attachment => {
|
attachment.tempName(result[index].TempName);
|
||||||
if (attachment?.fromMessage) {
|
attachment.type(result[index].MimeType);
|
||||||
attachment
|
}
|
||||||
.waiting(false)
|
|
||||||
.uploading(false)
|
|
||||||
.complete(true)
|
|
||||||
.error(getUploadErrorDescByCode(UploadErrorCode.NoFileUploaded));
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Attachments: downloads
|
Attachments: downloads
|
||||||
|
|
@ -1013,9 +1004,8 @@ export class ComposePopupView extends AbstractViewPopup {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (!this.to()) {
|
if (!this.to()) {
|
||||||
this.to.focused(true);
|
this.to.focused(true);
|
||||||
// TODO https://github.com/the-djmaze/snappymail/issues/501#issuecomment-1255906243
|
} else if (!this.subject()) {
|
||||||
// } else if (!this.subject()) {
|
this.viewModelDom.querySelector('input[name="subject"]').focus();
|
||||||
// this.subject.focus();
|
|
||||||
} else {
|
} else {
|
||||||
this.oEditor?.focus();
|
this.oEditor?.focus();
|
||||||
}
|
}
|
||||||
|
|
@ -1127,6 +1117,7 @@ export class ComposePopupView extends AbstractViewPopup {
|
||||||
attachment.size(attachmentJson.Size ? pInt(attachmentJson.Size) : 0);
|
attachment.size(attachmentJson.Size ? pInt(attachmentJson.Size) : 0);
|
||||||
attachment.tempName(attachmentJson.TempName ? attachmentJson.TempName : '');
|
attachment.tempName(attachmentJson.TempName ? attachmentJson.TempName : '');
|
||||||
attachment.isInline = false;
|
attachment.isInline = false;
|
||||||
|
attachment.type(attachmentJson.MimeType);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -1182,20 +1173,6 @@ export class ComposePopupView extends AbstractViewPopup {
|
||||||
return this.attachments.find(item => item && id === item.id);
|
return this.attachments.find(item => item && id === item.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @returns {Object}
|
|
||||||
*/
|
|
||||||
prepareAttachmentsForSendOrSave() {
|
|
||||||
const result = {};
|
|
||||||
this.attachments.forEach(item => {
|
|
||||||
if (item?.complete() && item?.tempName() && item?.enabled()) {
|
|
||||||
result[item.tempName()] = [item.fileName(), item.isInline ? '1' : '0', item.CID, item.contentLocation];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {MessageModel} message
|
* @param {MessageModel} message
|
||||||
*/
|
*/
|
||||||
|
|
@ -1241,6 +1218,7 @@ export class ComposePopupView extends AbstractViewPopup {
|
||||||
if (message) {
|
if (message) {
|
||||||
let reply = [ComposeType.Reply, ComposeType.ReplyAll].includes(type);
|
let reply = [ComposeType.Reply, ComposeType.ReplyAll].includes(type);
|
||||||
if (reply || [ComposeType.Forward, ComposeType.Draft, ComposeType.EditAsNew].includes(type)) {
|
if (reply || [ComposeType.Forward, ComposeType.Draft, ComposeType.EditAsNew].includes(type)) {
|
||||||
|
// item instanceof AttachmentModel
|
||||||
message.attachments.forEach(item => {
|
message.attachments.forEach(item => {
|
||||||
if (!reply || item.isLinked()) {
|
if (!reply || item.isLinked()) {
|
||||||
const attachment = new ComposeAttachmentModel(
|
const attachment = new ComposeAttachmentModel(
|
||||||
|
|
@ -1253,6 +1231,7 @@ export class ComposePopupView extends AbstractViewPopup {
|
||||||
item.contentLocation
|
item.contentLocation
|
||||||
);
|
);
|
||||||
attachment.fromMessage = true;
|
attachment.fromMessage = true;
|
||||||
|
attachment.type(item.mimeType);
|
||||||
this.addAttachment(attachment);
|
this.addAttachment(attachment);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -1409,6 +1388,20 @@ export class ComposePopupView extends AbstractViewPopup {
|
||||||
|
|
||||||
async getMessageRequestParams(sSaveFolder, draft)
|
async getMessageRequestParams(sSaveFolder, draft)
|
||||||
{
|
{
|
||||||
|
// Prepare ComposeAttachmentModel attachments
|
||||||
|
const attachments = {};
|
||||||
|
this.attachments.forEach(item => {
|
||||||
|
if (item?.complete() && item?.tempName() && item?.enabled()) {
|
||||||
|
attachments[item.tempName()] = {
|
||||||
|
name: item.fileName(),
|
||||||
|
inline: item.isInline,
|
||||||
|
cid: item.CID,
|
||||||
|
location: item.contentLocation,
|
||||||
|
type: item.mimeType()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const
|
const
|
||||||
identity = this.currentIdentity(),
|
identity = this.currentIdentity(),
|
||||||
params = {
|
params = {
|
||||||
|
|
@ -1426,7 +1419,7 @@ export class ComposePopupView extends AbstractViewPopup {
|
||||||
InReplyTo: this.sInReplyTo,
|
InReplyTo: this.sInReplyTo,
|
||||||
References: this.sReferences,
|
References: this.sReferences,
|
||||||
MarkAsImportant: this.markAsImportant() ? 1 : 0,
|
MarkAsImportant: this.markAsImportant() ? 1 : 0,
|
||||||
Attachments: this.prepareAttachmentsForSendOrSave(),
|
Attachments: attachments,
|
||||||
// Only used at send, not at save:
|
// Only used at send, not at save:
|
||||||
Dsn: this.requestDsn() ? 1 : 0,
|
Dsn: this.requestDsn() ? 1 : 0,
|
||||||
ReadReceiptRequest: this.requestReadReceipt() ? 1 : 0
|
ReadReceiptRequest: this.requestReadReceipt() ? 1 : 0
|
||||||
|
|
|
||||||
|
|
@ -12,23 +12,48 @@ import { DomainAdminStore } from 'Stores/Admin/Domain';
|
||||||
|
|
||||||
import { AskPopupView } from 'View/Popup/Ask';
|
import { AskPopupView } from 'View/Popup/Ask';
|
||||||
|
|
||||||
const domainToParams = oDomain => ({
|
const
|
||||||
|
capitalize = string => string.charAt(0).toUpperCase() + string.slice(1),
|
||||||
|
domainToParams = oDomain => ({
|
||||||
Name: oDomain.name(),
|
Name: oDomain.name(),
|
||||||
|
IMAP: {
|
||||||
IncHost: oDomain.imapHost(),
|
host: oDomain.imapHost(),
|
||||||
IncPort: oDomain.imapPort(),
|
port: oDomain.imapPort(),
|
||||||
IncSecure: oDomain.imapSecure(),
|
secure: pInt(oDomain.imapType()),
|
||||||
|
shortLogin: !!oDomain.imapShortLogin(),
|
||||||
UseSieve: oDomain.useSieve() ? 1 : 0,
|
ssl: {
|
||||||
SieveHost: oDomain.sieveHost(),
|
verify_peer: !!oDomain.imapSslVerify_peer(),
|
||||||
SievePort: oDomain.sievePort(),
|
verify_peer_name: !!oDomain.imapSslVerify_peer(),
|
||||||
SieveSecure: oDomain.sieveSecure(),
|
allow_self_signed: !!oDomain.imapSslAllow_self_signed()
|
||||||
|
}
|
||||||
OutHost: oDomain.smtpHost(),
|
},
|
||||||
OutPort: oDomain.smtpPort(),
|
SMTP: {
|
||||||
OutSecure: oDomain.smtpSecure(),
|
host: oDomain.smtpHost(),
|
||||||
OutAuth: oDomain.smtpAuth() ? 1 : 0,
|
port: oDomain.smtpPort(),
|
||||||
OutUsePhpMail: oDomain.smtpPhpMail() ? 1 : 0
|
secure: pInt(oDomain.smtpType()),
|
||||||
|
shortLogin: !!oDomain.smtpShortLogin(),
|
||||||
|
ssl: {
|
||||||
|
verify_peer: !!oDomain.smtpSslVerify_peer(),
|
||||||
|
verify_peer_name: !!oDomain.smtpSslVerify_peer(),
|
||||||
|
allow_self_signed: !!oDomain.smtpSslAllow_self_signed()
|
||||||
|
},
|
||||||
|
setSender: !!oDomain.smtpSetSender(),
|
||||||
|
useAuth: !!oDomain.smtpUseAuth(),
|
||||||
|
usePhpMail: !!oDomain.smtpUsePhpMail()
|
||||||
|
},
|
||||||
|
Sieve: {
|
||||||
|
enabled: !!oDomain.sieveEnabled(),
|
||||||
|
host: oDomain.sieveHost(),
|
||||||
|
port: oDomain.sievePort(),
|
||||||
|
secure: pInt(oDomain.sieveType()),
|
||||||
|
shortLogin: !!oDomain.imapShortLogin(),
|
||||||
|
ssl: {
|
||||||
|
verify_peer: !!oDomain.imapSslVerify_peer(),
|
||||||
|
verify_peer_name: !!oDomain.imapSslVerify_peer(),
|
||||||
|
allow_self_signed: !!oDomain.imapSslAllow_self_signed()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
whiteList: oDomain.whiteList()
|
||||||
});
|
});
|
||||||
|
|
||||||
export class DomainPopupView extends AbstractViewPopup {
|
export class DomainPopupView extends AbstractViewPopup {
|
||||||
|
|
@ -82,14 +107,14 @@ export class DomainPopupView extends AbstractViewPopup {
|
||||||
},
|
},
|
||||||
|
|
||||||
domainIsComputed: () => {
|
domainIsComputed: () => {
|
||||||
const usePhpMail = this.smtpPhpMail(),
|
const usePhpMail = this.smtpUsePhpMail(),
|
||||||
useSieve = this.useSieve();
|
sieveEnabled = this.sieveEnabled();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
this.name() &&
|
this.name() &&
|
||||||
this.imapHost() &&
|
this.imapHost() &&
|
||||||
this.imapPort() &&
|
this.imapPort() &&
|
||||||
(useSieve ? this.sieveHost() && this.sievePort() : true) &&
|
(sieveEnabled ? this.sieveHost() && this.sievePort() : true) &&
|
||||||
((this.smtpHost() && this.smtpPort()) || usePhpMail)
|
((this.smtpHost() && this.smtpPort()) || usePhpMail)
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
@ -113,7 +138,7 @@ export class DomainPopupView extends AbstractViewPopup {
|
||||||
smtpHostFocus: value => value && this.imapHost() && !this.smtpHost()
|
smtpHostFocus: value => value && this.imapHost() && !this.smtpHost()
|
||||||
&& this.smtpHost(this.imapHost().replace(/imap/gi, 'smtp')),
|
&& this.smtpHost(this.imapHost().replace(/imap/gi, 'smtp')),
|
||||||
|
|
||||||
imapSecure: value => {
|
imapType: value => {
|
||||||
if (this.enableSmartPorts()) {
|
if (this.enableSmartPorts()) {
|
||||||
const port = pInt(this.imapPort());
|
const port = pInt(this.imapPort());
|
||||||
switch (pInt(value)) {
|
switch (pInt(value)) {
|
||||||
|
|
@ -133,7 +158,7 @@ export class DomainPopupView extends AbstractViewPopup {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
smtpSecure: value => {
|
smtpType: value => {
|
||||||
if (this.enableSmartPorts()) {
|
if (this.enableSmartPorts()) {
|
||||||
const port = pInt(this.smtpPort());
|
const port = pInt(this.smtpPort());
|
||||||
switch (pInt(value)) {
|
switch (pInt(value)) {
|
||||||
|
|
@ -169,14 +194,7 @@ export class DomainPopupView extends AbstractViewPopup {
|
||||||
Remote.request('AdminDomainSave',
|
Remote.request('AdminDomainSave',
|
||||||
this.onDomainCreateOrSaveResponse.bind(this),
|
this.onDomainCreateOrSaveResponse.bind(this),
|
||||||
Object.assign(domainToParams(this), {
|
Object.assign(domainToParams(this), {
|
||||||
Create: this.edit() ? 0 : 1,
|
Create: this.edit() ? 0 : 1
|
||||||
|
|
||||||
IncShortLogin: this.imapShortLogin() ? 1 : 0,
|
|
||||||
|
|
||||||
OutShortLogin: this.smtpShortLogin() ? 1 : 0,
|
|
||||||
OutSetSender: this.smtpSetSender() ? 1 : 0,
|
|
||||||
|
|
||||||
WhiteList: this.whiteList()
|
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -188,8 +206,10 @@ export class DomainPopupView extends AbstractViewPopup {
|
||||||
if (credentials) {
|
if (credentials) {
|
||||||
this.testing(true);
|
this.testing(true);
|
||||||
const params = domainToParams(this);
|
const params = domainToParams(this);
|
||||||
params.username = credentials.username;
|
params.auth = {
|
||||||
params.password = credentials.password;
|
user: credentials.username,
|
||||||
|
pass: credentials.password
|
||||||
|
};
|
||||||
Remote.request('AdminDomainTest',
|
Remote.request('AdminDomainTest',
|
||||||
(iError, oData) => {
|
(iError, oData) => {
|
||||||
this.testing(false);
|
this.testing(false);
|
||||||
|
|
@ -250,7 +270,23 @@ export class DomainPopupView extends AbstractViewPopup {
|
||||||
if (oDomain) {
|
if (oDomain) {
|
||||||
this.enableSmartPorts(false);
|
this.enableSmartPorts(false);
|
||||||
this.edit(true);
|
this.edit(true);
|
||||||
forEachObjectEntry(oDomain, (key, value) => this[key]?.(value));
|
forEachObjectEntry(oDomain, (key, value) => {
|
||||||
|
if ('IMAP' === key || 'SMTP' === key || 'Sieve' === key) {
|
||||||
|
key = key.toLowerCase();
|
||||||
|
forEachObjectEntry(value, (skey, value) => {
|
||||||
|
skey = capitalize(skey);
|
||||||
|
if ('Ssl' == skey) {
|
||||||
|
forEachObjectEntry(value, (sslkey, value) => {
|
||||||
|
this[key + skey + capitalize(sslkey)]?.(value);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this[key + skey]?.(value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this[key]?.(value);
|
||||||
|
}
|
||||||
|
});
|
||||||
this.enableSmartPorts(true);
|
this.enableSmartPorts(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -265,21 +301,27 @@ export class DomainPopupView extends AbstractViewPopup {
|
||||||
|
|
||||||
imapHost: '',
|
imapHost: '',
|
||||||
imapPort: 143,
|
imapPort: 143,
|
||||||
imapSecure: 0,
|
imapType: 0,
|
||||||
imapShortLogin: false,
|
imapShortLogin: false,
|
||||||
|
// SSL
|
||||||
|
imapSslVerify_peer: false,
|
||||||
|
imapSslAllow_self_signed: false,
|
||||||
|
|
||||||
useSieve: false,
|
sieveEnabled: false,
|
||||||
sieveHost: '',
|
sieveHost: '',
|
||||||
sievePort: 4190,
|
sievePort: 4190,
|
||||||
sieveSecure: 0,
|
sieveType: 0,
|
||||||
|
|
||||||
smtpHost: '',
|
smtpHost: '',
|
||||||
smtpPort: 25,
|
smtpPort: 25,
|
||||||
smtpSecure: 0,
|
smtpType: 0,
|
||||||
smtpShortLogin: false,
|
smtpShortLogin: false,
|
||||||
smtpAuth: true,
|
smtpUseAuth: true,
|
||||||
smtpSetSender: false,
|
smtpSetSender: false,
|
||||||
smtpPhpMail: false,
|
smtpUsePhpMail: false,
|
||||||
|
// SSL
|
||||||
|
smtpSslVerify_peer: false,
|
||||||
|
smtpSslAllow_self_signed: false,
|
||||||
|
|
||||||
whiteList: '',
|
whiteList: '',
|
||||||
aliasName: ''
|
aliasName: ''
|
||||||
|
|
|
||||||
|
|
@ -69,8 +69,8 @@ export class OpenPgpImportPopupView extends AbstractViewPopup {
|
||||||
this.close();
|
this.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
onShow() {
|
onShow(key) {
|
||||||
this.key('');
|
this.key(key || '');
|
||||||
this.keyError(false);
|
this.keyError(false);
|
||||||
this.keyErrorMessage('');
|
this.keyErrorMessage('');
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import ko from 'ko';
|
import ko from 'ko';
|
||||||
|
|
||||||
import { Scope } from 'Common/Enums';
|
import { Scope } from 'Common/Enums';
|
||||||
import { moveAction, addShortcut } from 'Common/Globals';
|
import { addShortcut } from 'Common/Globals';
|
||||||
import { mailBox, settings } from 'Common/Links';
|
import { mailBox, settings } from 'Common/Links';
|
||||||
//import { setFolderHash } from 'Common/Cache';
|
//import { setFolderHash } from 'Common/Cache';
|
||||||
import { addComputablesTo } from 'External/ko';
|
import { addComputablesTo } from 'External/ko';
|
||||||
|
|
@ -15,7 +15,7 @@ import { MessagelistUserStore } from 'Stores/User/Messagelist';
|
||||||
import { showScreenPopup } from 'Knoin/Knoin';
|
import { showScreenPopup } from 'Knoin/Knoin';
|
||||||
import { AbstractViewLeft } from 'Knoin/AbstractViews';
|
import { AbstractViewLeft } from 'Knoin/AbstractViews';
|
||||||
|
|
||||||
import { showMessageComposer } from 'Common/UtilsUser';
|
import { showMessageComposer, moveAction } from 'Common/UtilsUser';
|
||||||
import { FolderCreatePopupView } from 'View/Popup/FolderCreate';
|
import { FolderCreatePopupView } from 'View/Popup/FolderCreate';
|
||||||
import { ContactsPopupView } from 'View/Popup/Contacts';
|
import { ContactsPopupView } from 'View/Popup/Contacts';
|
||||||
import { ComposePopupView } from 'View/Popup/Compose';
|
import { ComposePopupView } from 'View/Popup/Compose';
|
||||||
|
|
|
||||||
|
|
@ -5,17 +5,17 @@ import { Scope } from 'Common/Enums';
|
||||||
|
|
||||||
import { ComposeType, FolderType, MessageSetAction } from 'Common/EnumsUser';
|
import { ComposeType, FolderType, MessageSetAction } from 'Common/EnumsUser';
|
||||||
|
|
||||||
import { leftPanelDisabled, moveAction,
|
import { leftPanelDisabled, toggleLeftPanel,
|
||||||
Settings, SettingsCapa,
|
Settings, SettingsCapa,
|
||||||
addEventsListeners,
|
addEventsListeners,
|
||||||
addShortcut, registerShortcut, formFieldFocused
|
addShortcut, registerShortcut, formFieldFocused
|
||||||
} from 'Common/Globals';
|
} from 'Common/Globals';
|
||||||
|
|
||||||
import { computedPaginatorHelper, showMessageComposer, populateMessageBody } from 'Common/UtilsUser';
|
import { computedPaginatorHelper, showMessageComposer, populateMessageBody, download, moveAction } from 'Common/UtilsUser';
|
||||||
import { FileInfo } from 'Common/File';
|
import { FileInfo } from 'Common/File';
|
||||||
import { isFullscreen, toggleFullscreen } from 'Common/Fullscreen';
|
import { isFullscreen, toggleFullscreen } from 'Common/Fullscreen';
|
||||||
|
|
||||||
import { mailBox } from 'Common/Links';
|
import { mailBox, attachmentDownload } from 'Common/Links';
|
||||||
import { Selector } from 'Common/Selector';
|
import { Selector } from 'Common/Selector';
|
||||||
|
|
||||||
import { i18n } from 'Common/Translator';
|
import { i18n } from 'Common/Translator';
|
||||||
|
|
@ -83,6 +83,7 @@ export class MailMessageList extends AbstractViewRight {
|
||||||
|
|
||||||
this.isMobile = ThemeStore.isMobile;
|
this.isMobile = ThemeStore.isMobile;
|
||||||
this.leftPanelDisabled = leftPanelDisabled;
|
this.leftPanelDisabled = leftPanelDisabled;
|
||||||
|
this.toggleLeftPanel = toggleLeftPanel;
|
||||||
|
|
||||||
this.popupVisibility = arePopupsVisible;
|
this.popupVisibility = arePopupsVisible;
|
||||||
|
|
||||||
|
|
@ -258,6 +259,7 @@ export class MailMessageList extends AbstractViewRight {
|
||||||
).throttle(50));
|
).throttle(50));
|
||||||
|
|
||||||
decorateKoCommands(this, {
|
decorateKoCommands(this, {
|
||||||
|
downloadCommand: canBeMovedHelper,
|
||||||
forwardCommand: canBeMovedHelper,
|
forwardCommand: canBeMovedHelper,
|
||||||
deleteWithoutMoveCommand: canBeMovedHelper,
|
deleteWithoutMoveCommand: canBeMovedHelper,
|
||||||
deleteCommand: canBeMovedHelper,
|
deleteCommand: canBeMovedHelper,
|
||||||
|
|
@ -290,6 +292,34 @@ export class MailMessageList extends AbstractViewRight {
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
downloadCommand() {
|
||||||
|
let hashes = [];
|
||||||
|
MessagelistUserStore.forEach(message => {
|
||||||
|
if (message.checked()) {
|
||||||
|
message.attachments.forEach(attachment => {
|
||||||
|
if (!attachment.isLinked() && attachment.download) {
|
||||||
|
hashes.push(attachment.download);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (hashes.length) {
|
||||||
|
Remote.post('AttachmentsActions', null, {
|
||||||
|
Do: 'Zip',
|
||||||
|
Hashes: hashes
|
||||||
|
})
|
||||||
|
.then(result => {
|
||||||
|
let hash = result?.Result?.FileHash;
|
||||||
|
if (hash) {
|
||||||
|
download(attachmentDownload(hash), hash+'.zip');
|
||||||
|
} else {
|
||||||
|
alert('Download failed');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => alert('Download failed'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
deleteWithoutMoveCommand() {
|
deleteWithoutMoveCommand() {
|
||||||
SettingsCapa('DangerousActions')
|
SettingsCapa('DangerousActions')
|
||||||
&& moveMessagesToFolderType(FolderType.Trash, true);
|
&& moveMessagesToFolderType(FolderType.Trash, true);
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,6 @@ import {
|
||||||
elementById,
|
elementById,
|
||||||
leftPanelDisabled,
|
leftPanelDisabled,
|
||||||
keyScopeReal,
|
keyScopeReal,
|
||||||
moveAction,
|
|
||||||
Settings,
|
Settings,
|
||||||
SettingsCapa,
|
SettingsCapa,
|
||||||
fireEvent,
|
fireEvent,
|
||||||
|
|
@ -24,7 +23,7 @@ import {
|
||||||
} from 'Common/Globals';
|
} from 'Common/Globals';
|
||||||
|
|
||||||
import { arrayLength } from 'Common/Utils';
|
import { arrayLength } from 'Common/Utils';
|
||||||
import { download, mailToHelper, showMessageComposer } from 'Common/UtilsUser';
|
import { download, mailToHelper, showMessageComposer, moveAction } from 'Common/UtilsUser';
|
||||||
import { isFullscreen, exitFullscreen, toggleFullscreen } from 'Common/Fullscreen';
|
import { isFullscreen, exitFullscreen, toggleFullscreen } from 'Common/Fullscreen';
|
||||||
|
|
||||||
import { SMAudio } from 'Common/Audio';
|
import { SMAudio } from 'Common/Audio';
|
||||||
|
|
@ -55,10 +54,17 @@ import { MimeToMessage } from 'Mime/Utils';
|
||||||
|
|
||||||
import { MessageModel } from 'Model/Message';
|
import { MessageModel } from 'Model/Message';
|
||||||
|
|
||||||
|
import { showScreenPopup } from 'Knoin/Knoin';
|
||||||
|
import { OpenPgpImportPopupView } from 'View/Popup/OpenPgpImport';
|
||||||
|
import { GnuPGUserStore } from 'Stores/User/GnuPG';
|
||||||
|
import { OpenPGPUserStore } from 'Stores/User/OpenPGP';
|
||||||
|
|
||||||
const
|
const
|
||||||
oMessageScrollerDom = () => elementById('messageItem') || {},
|
oMessageScrollerDom = () => elementById('messageItem') || {},
|
||||||
|
|
||||||
currentMessage = MessageUserStore.message;
|
currentMessage = MessageUserStore.message,
|
||||||
|
|
||||||
|
fetchRaw = url => rl.fetch(url).then(response => response.ok && response.text());
|
||||||
|
|
||||||
export class MailMessageView extends AbstractViewRight {
|
export class MailMessageView extends AbstractViewRight {
|
||||||
constructor() {
|
constructor() {
|
||||||
|
|
@ -92,6 +98,7 @@ export class MailMessageView extends AbstractViewRight {
|
||||||
}, this.messageVisibility);
|
}, this.messageVisibility);
|
||||||
|
|
||||||
this.msgDefaultAction = SettingsUserStore.msgDefaultAction;
|
this.msgDefaultAction = SettingsUserStore.msgDefaultAction;
|
||||||
|
this.simpleAttachmentsList = SettingsUserStore.simpleAttachmentsList;
|
||||||
|
|
||||||
addObservablesTo(this, {
|
addObservablesTo(this, {
|
||||||
showAttachmentControls: !!Local.get(ClientSideKeyNameMessageAttachmentControls),
|
showAttachmentControls: !!Local.get(ClientSideKeyNameMessageAttachmentControls),
|
||||||
|
|
@ -154,9 +161,9 @@ export class MailMessageView extends AbstractViewRight {
|
||||||
case 'none':
|
case 'none':
|
||||||
return '';
|
return '';
|
||||||
case 'pass':
|
case 'pass':
|
||||||
return 'icon-ok iconcolor-green';
|
return 'icon-ok iconcolor-green'; // ✔️
|
||||||
default:
|
default:
|
||||||
return 'icon-warning-alt iconcolor-red';
|
return 'icon-cross iconcolor-red'; // ✖ ❌
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -182,7 +189,8 @@ export class MailMessageView extends AbstractViewRight {
|
||||||
this.scrollMessageToTop();
|
this.scrollMessageToTop();
|
||||||
}
|
}
|
||||||
this.viewHash = message.hash;
|
this.viewHash = message.hash;
|
||||||
this.viewFromShort(message.fromToLine(true, true));
|
// TODO: make first param a user setting #683
|
||||||
|
this.viewFromShort(message.fromToLine(false, true));
|
||||||
this.viewFromDkimData(message.fromDkimData());
|
this.viewFromDkimData(message.fromDkimData());
|
||||||
this.viewToShort(message.toToLine(true, true));
|
this.viewToShort(message.toToLine(true, true));
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -257,8 +265,9 @@ export class MailMessageView extends AbstractViewRight {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (eqs(event, '.attachmentsPlace .attachmentIconParent')) {
|
if (eqs(event, '.attachmentsPlace .showPreview')) {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
el = eqs(event, '.attachmentsPlace .showPreplay');
|
el = eqs(event, '.attachmentsPlace .showPreplay');
|
||||||
|
|
@ -279,26 +288,28 @@ export class MailMessageView extends AbstractViewRight {
|
||||||
// no default
|
// no default
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
el = eqs(event, '.attachmentsPlace .attachmentItem .attachmentNameParent');
|
el = eqs(event, '.attachmentItem');
|
||||||
if (el) {
|
if (el) {
|
||||||
const attachment = ko.dataFor(el);
|
const attachment = ko.dataFor(el), url = attachment?.linkDownload();
|
||||||
if (attachment?.linkDownload()) {
|
if (url) {
|
||||||
if ('message/rfc822' == attachment.mimeType) {
|
if ('application/pgp-keys' == attachment.mimeType
|
||||||
|
&& (OpenPGPUserStore.isSupported() || GnuPGUserStore.isSupported())) {
|
||||||
|
fetchRaw(url).then(text =>
|
||||||
|
showScreenPopup(OpenPgpImportPopupView, [text])
|
||||||
|
);
|
||||||
|
} else if ('message/rfc822' == attachment.mimeType) {
|
||||||
// TODO
|
// TODO
|
||||||
rl.fetch(attachment.linkDownload()).then(response => {
|
fetchRaw(url).then(text => {
|
||||||
if (response.ok) {
|
|
||||||
response.text().then(text => {
|
|
||||||
const oMessage = new MessageModel();
|
const oMessage = new MessageModel();
|
||||||
MimeToMessage(text, oMessage);
|
MimeToMessage(text, oMessage);
|
||||||
// cleanHTML
|
// cleanHTML
|
||||||
oMessage.viewPopupMessage();
|
oMessage.viewPopupMessage();
|
||||||
});
|
});
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
download(attachment.linkDownload(), attachment.fileName);
|
download(url, attachment.fileName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { mailbox } from 'Common/Links';
|
import { mailbox } from 'Common/Links';
|
||||||
import { getFolderInboxName } from 'Common/Cache';
|
import { getFolderInboxName } from 'Common/Cache';
|
||||||
import { leftPanelDisabled } from 'Common/Globals';
|
import { leftPanelDisabled, toggleLeftPanel } from 'Common/Globals';
|
||||||
|
|
||||||
import { MessageUserStore } from 'Stores/User/Message';
|
import { MessageUserStore } from 'Stores/User/Message';
|
||||||
import { ThemeStore } from 'Stores/Theme';
|
import { ThemeStore } from 'Stores/Theme';
|
||||||
|
|
@ -13,6 +13,7 @@ export class SettingsPaneUserView extends AbstractViewRight {
|
||||||
|
|
||||||
this.isMobile = ThemeStore.isMobile;
|
this.isMobile = ThemeStore.isMobile;
|
||||||
this.leftPanelDisabled = leftPanelDisabled;
|
this.leftPanelDisabled = leftPanelDisabled;
|
||||||
|
this.toggleLeftPanel = toggleLeftPanel;
|
||||||
}
|
}
|
||||||
|
|
||||||
onShow() {
|
onShow() {
|
||||||
|
|
|
||||||
15
dev/boot.js
|
|
@ -1,13 +1,11 @@
|
||||||
(doc => {
|
(doc => {
|
||||||
|
|
||||||
navigator.cookieEnabled || doc.location.replace('./?/NoCookie');
|
|
||||||
|
|
||||||
const
|
const
|
||||||
eId = id => doc.getElementById('rl-'+id),
|
eId = id => doc.getElementById('rl-'+id),
|
||||||
app = eId('app'),
|
app = eId('app'),
|
||||||
admin = app && '1' == app.dataset.admin,
|
admin = app && '1' == app.dataset.admin,
|
||||||
layout = doc.cookie.match(/(^|;) ?rllayout=([^;]+)/) || '',
|
layout = doc.cookie.match(/(^|;) ?rllayout=([^;]+)/) || '',
|
||||||
badBrowser = () => doc.location.replace('./?/BadBrowser'),
|
redirect = path => doc.location.replace('./?/'+path),
|
||||||
|
|
||||||
showError = msg => {
|
showError = msg => {
|
||||||
let div = eId('loading-error');
|
let div = eId('loading-error');
|
||||||
|
|
@ -23,14 +21,15 @@ const
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const script = doc.createElement('script');
|
const script = doc.createElement('script');
|
||||||
script.onload = () => resolve();
|
script.onload = () => resolve();
|
||||||
script.onerror = () => reject(new Error('Failed loading ' + src));
|
script.onerror = () => reject('Failed loading ' + src);
|
||||||
script.src = src;
|
script.src = src;
|
||||||
// script.async = true;
|
// script.async = true;
|
||||||
doc.head.append(script);
|
doc.head.append(script);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
[].flat || badBrowser();
|
navigator.cookieEnabled || redirect('NoCookie');
|
||||||
|
[].flat || redirect('BadBrowser');
|
||||||
|
|
||||||
let RL_APP_DATA = {};
|
let RL_APP_DATA = {};
|
||||||
|
|
||||||
|
|
@ -45,13 +44,13 @@ window.rl = {
|
||||||
app: name => RL_APP_DATA.System[name]
|
app: name => RL_APP_DATA.System[name]
|
||||||
},
|
},
|
||||||
|
|
||||||
setWindowTitle: title =>
|
setTitle: title =>
|
||||||
doc.title = RL_APP_DATA.Title ? (title ? title + ' - ' : '') + RL_APP_DATA.Title : (title ? '' + title : ''),
|
doc.title = (title || '') + (RL_APP_DATA.Title ? (title ? ' - ' : '') + RL_APP_DATA.Title : ''),
|
||||||
|
|
||||||
initData: appData => {
|
initData: appData => {
|
||||||
RL_APP_DATA = appData;
|
RL_APP_DATA = appData;
|
||||||
const url = appData.StaticLibsJs,
|
const url = appData.StaticLibsJs,
|
||||||
cb = () => rl.app ? rl.app.bootstart() : badBrowser();
|
cb = () => rl.app.bootstart();
|
||||||
loadScript(url)
|
loadScript(url)
|
||||||
.then(() => loadScript(url.replace('/libs.', `/${admin?'admin':'app'}.`)))
|
.then(() => loadScript(url.replace('/libs.', `/${admin?'admin':'app'}.`)))
|
||||||
.then(() => appData.PluginsLink ? loadScript(appData.PluginsLink) : Promise.resolve())
|
.then(() => appData.PluginsLink ? loadScript(appData.PluginsLink) : Promise.resolve())
|
||||||
|
|
|
||||||
25
dev/bootstrap.js
vendored
|
|
@ -4,18 +4,6 @@ import { i18n } from 'Common/Translator';
|
||||||
import { root } from 'Common/Links';
|
import { root } from 'Common/Links';
|
||||||
|
|
||||||
import { isArray } from 'Common/Utils';
|
import { isArray } from 'Common/Utils';
|
||||||
const FormDataToObject = formData => {
|
|
||||||
var object = {};
|
|
||||||
formData.forEach((value, key) => {
|
|
||||||
if (!Reflect.has(object, key)){
|
|
||||||
object[key] = value;
|
|
||||||
} else {
|
|
||||||
isArray(object[key]) || (object[key] = [object[key]]);
|
|
||||||
object[key].push(value);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return object;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default App => {
|
export default App => {
|
||||||
|
|
||||||
|
|
@ -57,7 +45,18 @@ export default App => {
|
||||||
if (postData) {
|
if (postData) {
|
||||||
init.method = 'POST';
|
init.method = 'POST';
|
||||||
init.headers['Content-Type'] = 'application/json';
|
init.headers['Content-Type'] = 'application/json';
|
||||||
postData = (postData instanceof FormData) ? FormDataToObject(postData) : postData;
|
if (postData instanceof FormData) {
|
||||||
|
const object = {};
|
||||||
|
postData.forEach((value, key) => {
|
||||||
|
if (!Reflect.has(object, key)){
|
||||||
|
object[key] = value;
|
||||||
|
} else {
|
||||||
|
isArray(object[key]) || (object[key] = [object[key]]);
|
||||||
|
object[key].push(value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
postData = object;
|
||||||
|
}
|
||||||
postData.XToken = Settings.app('token');
|
postData.XToken = Settings.app('token');
|
||||||
init.body = JSON.stringify(postData);
|
init.body = JSON.stringify(postData);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,7 @@ const
|
||||||
setScope: value => {
|
setScope: value => {
|
||||||
_scope = value || 'all';
|
_scope = value || 'all';
|
||||||
scope = _scopes[_scope] || {};
|
scope = _scopes[_scope] || {};
|
||||||
|
console.log('Shortcuts scope set to: ' + _scope);
|
||||||
},
|
},
|
||||||
getScope: () => _scope,
|
getScope: () => _scope,
|
||||||
getMetaKey: () => 'meta' === meta ? '⌘' : 'Ctrl'
|
getMetaKey: () => 'meta' === meta ? '⌘' : 'Ctrl'
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
This app packages SnappyMail <upstream>2.20.6</upstream>.
|
This app packages SnappyMail <upstream>2.22.2</upstream>.
|
||||||
|
|
||||||
SnappyMail is a simple, modern, lightweight & fast web-based email client.
|
SnappyMail is a simple, modern, lightweight & fast web-based email client.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ RUN mkdir -p /app/code
|
||||||
WORKDIR /app/code
|
WORKDIR /app/code
|
||||||
|
|
||||||
# If you change the extraction below, be sure to test on scaleway
|
# If you change the extraction below, be sure to test on scaleway
|
||||||
VERSION=2.20.6
|
VERSION=2.22.2
|
||||||
RUN wget https://github.com/the-djmaze/snappymail/releases/download/v${VERSION}/snappymail-${VERSION}.zip -O /tmp/snappymail.zip && \
|
RUN wget https://github.com/the-djmaze/snappymail/releases/download/v${VERSION}/snappymail-${VERSION}.zip -O /tmp/snappymail.zip && \
|
||||||
unzip /tmp/snappymail.zip -d /app/code && \
|
unzip /tmp/snappymail.zip -d /app/code && \
|
||||||
rm /tmp/snappymail.zip && \
|
rm /tmp/snappymail.zip && \
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
url: "/3rdparty/snappymail/?RemoteAutoLogin"
|
||||||
|
displayname: "SnappyMail"
|
||||||
|
icon: "/3rdparty/snappymail/snappymail/v/2.22.1/static/logo-text.png"
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
<id>snappymail</id>
|
<id>snappymail</id>
|
||||||
<name>SnappyMail</name>
|
<name>SnappyMail</name>
|
||||||
<summary>SnappyMail Webmail</summary>
|
<summary>SnappyMail Webmail</summary>
|
||||||
<version>2.20.6</version>
|
<version>2.22.2</version>
|
||||||
<licence>agpl</licence>
|
<licence>agpl</licence>
|
||||||
<author>SnappyMail, RainLoop Team, Nextgen-Networks, Tab Fitts, Nathan Kinkade, Pierre-Alain Bandinelli</author>
|
<author>SnappyMail, RainLoop Team, Nextgen-Networks, Tab Fitts, Nathan Kinkade, Pierre-Alain Bandinelli</author>
|
||||||
<description><![CDATA[**Simple, modern, lightweight & fast web-based email client.**
|
<description><![CDATA[**Simple, modern, lightweight & fast web-based email client.**
|
||||||
|
|
|
||||||
|
|
@ -10,32 +10,6 @@ This stylesheet is used when SnappyMail runs in embed mode.
|
||||||
min-height: 100%;
|
min-height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
#content[class*="app-"] #rl-app * {
|
|
||||||
box-sizing: content-box;
|
|
||||||
}
|
|
||||||
#content[class*="app-"] #rl-app #V-Settings-Domains .domain-name,
|
|
||||||
#content[class*="app-"] #rl-app #V-Settings-Domains .domain-alias
|
|
||||||
#content[class*="app-"] #rl-app #rl-settings-subscreen,
|
|
||||||
#content[class*="app-"] #rl-app .e-checkbox.material-design > div > div,
|
|
||||||
#content[class*="app-"] #rl-app .attachmentItem .iconMain,
|
|
||||||
#content[class*="app-"] #rl-app .attachmentItem .iconPreview,
|
|
||||||
#content[class*="app-"] #rl-app #V-PopupsCompose .textAreaParent,
|
|
||||||
#content[class*="app-"] #rl-app #V-PopupsCompose .attachmentAreaParent,
|
|
||||||
#content[class*="app-"] #rl-app .emailaddresses,
|
|
||||||
#content[class*="app-"] #rl-app .squire-wysiwyg,
|
|
||||||
#content[class*="app-"] #rl-app .squire-plain,
|
|
||||||
/* bootstrap */
|
|
||||||
#content[class*="app-"] #rl-app .btn-block,
|
|
||||||
#content[class*="app-"] #rl-app select,
|
|
||||||
#content[class*="app-"] #rl-app .select,
|
|
||||||
#content[class*="app-"] #rl-app textarea,
|
|
||||||
#content[class*="app-"] #rl-app input,
|
|
||||||
#content[class*="app-"] #rl-app .input-block-level,
|
|
||||||
#content[class*="app-"] #rl-app .icon-spinner,
|
|
||||||
#content[class*="app-"] #rl-app #V-AdminPane #rl-settings-subscreen
|
|
||||||
{
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
.squire-wysiwyg {
|
.squire-wysiwyg {
|
||||||
min-width: 100%;
|
min-width: 100%;
|
||||||
min-height: 100%;
|
min-height: 100%;
|
||||||
|
|
@ -78,7 +52,6 @@ This stylesheet is used when SnappyMail runs in embed mode.
|
||||||
}
|
}
|
||||||
|
|
||||||
#rl-app .squire-plain, #rl-app .squire-wysiwyg {
|
#rl-app .squire-plain, #rl-app .squire-wysiwyg {
|
||||||
box-sizing: border-box;
|
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
line-height: 16px;
|
line-height: 16px;
|
||||||
|
|
@ -105,6 +78,15 @@ body > header ul {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Firefox < 98
|
||||||
|
* https://github.com/the-djmaze/snappymail/issues/673
|
||||||
|
*/
|
||||||
|
#rl-app dialog.polyfill {
|
||||||
|
margin-top: 50px;
|
||||||
|
max-height: calc(100vh - 60px);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* hampoelz
|
* hampoelz
|
||||||
* https://github.com/the-djmaze/snappymail/issues/96#issuecomment-1279783076
|
* https://github.com/the-djmaze/snappymail/issues/96#issuecomment-1279783076
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ class ContentSecurityPolicy extends \OCP\AppFramework\Http\ContentSecurityPolicy
|
||||||
/** @var bool Whether eval in JS scripts is allowed */
|
/** @var bool Whether eval in JS scripts is allowed */
|
||||||
protected $evalScriptAllowed = true;
|
protected $evalScriptAllowed = true;
|
||||||
/** @var bool Whether strict-dynamic should be set */
|
/** @var bool Whether strict-dynamic should be set */
|
||||||
protected $strictDynamicAllowed = true; // NC24+
|
// protected $strictDynamicAllowed = true; // NC24+
|
||||||
/** @var bool Whether inline CSS is allowed */
|
/** @var bool Whether inline CSS is allowed */
|
||||||
protected $inlineStyleAllowed = true;
|
protected $inlineStyleAllowed = true;
|
||||||
|
|
||||||
|
|
@ -21,9 +21,11 @@ class ContentSecurityPolicy extends \OCP\AppFramework\Http\ContentSecurityPolicy
|
||||||
$CSP->script
|
$CSP->script
|
||||||
));
|
));
|
||||||
$this->allowedScriptDomains = \array_diff($this->allowedScriptDomains, ["'unsafe-inline'", "'unsafe-eval'"]);
|
$this->allowedScriptDomains = \array_diff($this->allowedScriptDomains, ["'unsafe-inline'", "'unsafe-eval'"]);
|
||||||
if (\method_exists($this, 'useStrictDynamic')) {
|
|
||||||
$this->allowedScriptDomains = \array_diff($this->allowedScriptDomains, ["'strict-dynamic'"]);
|
// Nextcloud only sets 'strict-dynamic' when browserSupportsCspV3() ?
|
||||||
}
|
\method_exists($this, 'useStrictDynamic')
|
||||||
|
? $this->useStrictDynamic(true) // NC24+
|
||||||
|
: $this->addAllowedScriptDomain("'strict-dynamic'");
|
||||||
|
|
||||||
$this->allowedImageDomains = \array_unique(\array_merge(
|
$this->allowedImageDomains = \array_unique(\array_merge(
|
||||||
$this->allowedImageDomains,
|
$this->allowedImageDomains,
|
||||||
|
|
@ -47,4 +49,16 @@ class ContentSecurityPolicy extends \OCP\AppFramework\Http\ContentSecurityPolicy
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getSnappyMailNonce() {
|
||||||
|
static $sNonce;
|
||||||
|
if (!$sNonce) {
|
||||||
|
$cspManager = \OC::$server->getContentSecurityPolicyNonceManager();
|
||||||
|
$sNonce = $cspManager->getNonce() ?: \SnappyMail\UUID::generate();
|
||||||
|
if (\method_exists($cspManager, 'browserSupportsCspV3') && !$cspManager->browserSupportsCspV3()) {
|
||||||
|
$this->addAllowedScriptDomain("'nonce-{$sNonce}'");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $sNonce;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -55,12 +55,8 @@ class PageController extends Controller
|
||||||
$sAppCssMin = $oConfig->Get('labs', 'use_app_debug_css', false) ? '' : '.min';
|
$sAppCssMin = $oConfig->Get('labs', 'use_app_debug_css', false) ? '' : '.min';
|
||||||
$sLanguage = $oActions->GetLanguage(false);
|
$sLanguage = $oActions->GetLanguage(false);
|
||||||
|
|
||||||
$cspManager = \OC::$server->getContentSecurityPolicyNonceManager();
|
|
||||||
$sNonce = $cspManager->getNonce() ?: \SnappyMail\UUID::generate();
|
|
||||||
$csp = new ContentSecurityPolicy();
|
$csp = new ContentSecurityPolicy();
|
||||||
if (\method_exists($cspManager, 'browserSupportsCspV3') && !$cspManager->browserSupportsCspV3()) {
|
$sNonce = $csp->getSnappyMailNonce();
|
||||||
$csp->addAllowedScriptDomain("'nonce-{$sNonce}'");
|
|
||||||
}
|
|
||||||
|
|
||||||
$params = [
|
$params = [
|
||||||
'Admin' => $bAdmin ? 1 : 0,
|
'Admin' => $bAdmin ? 1 : 0,
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ return "SnappyMail Webmail is a browser-based multilingual IMAP client with an a
|
||||||
# script_snappymail_versions()
|
# script_snappymail_versions()
|
||||||
sub script_snappymail_versions
|
sub script_snappymail_versions
|
||||||
{
|
{
|
||||||
return ( "2.20.6" );
|
return ( "2.22.2" );
|
||||||
}
|
}
|
||||||
|
|
||||||
sub script_snappymail_version_desc
|
sub script_snappymail_version_desc
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
"title": "SnappyMail",
|
"title": "SnappyMail",
|
||||||
"description": "Simple, modern & fast web-based email client",
|
"description": "Simple, modern & fast web-based email client",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "2.20.6",
|
"version": "2.22.2",
|
||||||
"homepage": "https://snappymail.eu",
|
"homepage": "https://snappymail.eu",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "DJ Maze",
|
"name": "DJ Maze",
|
||||||
|
|
|
||||||
|
|
@ -3,13 +3,84 @@ PHP
|
||||||
class Plugin extends \RainLoop\Plugins\AbstractPlugin
|
class Plugin extends \RainLoop\Plugins\AbstractPlugin
|
||||||
{
|
{
|
||||||
public function __construct();
|
public function __construct();
|
||||||
|
|
||||||
|
/** Returns static::NAME */
|
||||||
public function Name(): string;
|
public function Name(): string;
|
||||||
|
|
||||||
|
/** Returns /README file contents or static::DESCRIPTION */
|
||||||
public function Description(): string;
|
public function Description(): string;
|
||||||
|
|
||||||
|
/** When $bLangs is boolean it sets the value, else returns current value */
|
||||||
public function UseLangs(?bool $bLangs = null): bool;
|
public function UseLangs(?bool $bLangs = null): bool;
|
||||||
|
|
||||||
|
/** When true the result is empty string, else the error message */
|
||||||
public function Supported(): string;
|
public function Supported(): string;
|
||||||
|
|
||||||
|
/** Initialize settings */
|
||||||
public function Init(): void;
|
public function Init(): void;
|
||||||
|
|
||||||
public function FilterAppDataPluginSection(bool $bAdmin, bool $bAuth, array &$aConfig): void;
|
public function FilterAppDataPluginSection(bool $bAdmin, bool $bAuth, array &$aConfig): void;
|
||||||
|
|
||||||
|
/** Returns array of all plugin Property options for use in Admin -> Extensions -> Plugin cog wheel */
|
||||||
protected function configMapping(): array;
|
protected function configMapping(): array;
|
||||||
|
|
||||||
|
/** With this function you hook to an event
|
||||||
|
* $sHookName see chapter "Hooks" below for available names
|
||||||
|
* $sFunctionName the name of a function in this class
|
||||||
|
*/
|
||||||
|
final protected function addHook(string $sHookName, string $sFunctionName): self;
|
||||||
|
|
||||||
|
final protected function addCss(string $sFile, bool $bAdminScope = false): self;
|
||||||
|
|
||||||
|
final protected function addJs(string $sFile, bool $bAdminScope = false): self;
|
||||||
|
|
||||||
|
final protected function addTemplate(string $sFile, bool $bAdminScope = false): self;
|
||||||
|
|
||||||
|
final protected function addJsonHook(string $sActionName, string $sFunctionName): self;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* You may register your own service actions.
|
||||||
|
* Url is like /?{actionname}/etc.
|
||||||
|
* Predefined actions of \RainLoop\ServiceActions that can't be registered are:
|
||||||
|
* - admin
|
||||||
|
* - AdminAppData
|
||||||
|
* - AppData
|
||||||
|
* - Append
|
||||||
|
* - Backup
|
||||||
|
* - BadBrowser
|
||||||
|
* - CspReport
|
||||||
|
* - Css
|
||||||
|
* - Json
|
||||||
|
* - Lang
|
||||||
|
* - Mailto
|
||||||
|
* - NoCookie
|
||||||
|
* - NoScript
|
||||||
|
* - Ping
|
||||||
|
* - Plugins
|
||||||
|
* - ProxyExternal
|
||||||
|
* - Raw
|
||||||
|
* - Sso
|
||||||
|
* - Upload
|
||||||
|
* - UploadBackground
|
||||||
|
* - UploadContacts
|
||||||
|
*/
|
||||||
|
final protected function addPartHook(string $sActionName, string $sFunctionName): self
|
||||||
|
|
||||||
|
final public function Config(): \RainLoop\Config\Plugin;
|
||||||
|
final public function Manager(): \RainLoop\Plugins\Manager;
|
||||||
|
final public function Path(): string;
|
||||||
|
final public function ConfigMap(bool $flatten = false): array;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns result of Actions->DefaultResponse($sFunctionName, $mData) or json_encode($mData)
|
||||||
|
*/
|
||||||
|
final protected function jsonResponse(string $sFunctionName, $mData): mixed;
|
||||||
|
|
||||||
|
final public function jsonParam(string $sKey, $mDefault = null): mixed;
|
||||||
|
|
||||||
|
final public function getUserSettings(): array;
|
||||||
|
|
||||||
|
final public function saveUserSettings(array $aSettings): bool;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -72,53 +143,53 @@ $Plugin->addHook('hook.name', 'functionName');
|
||||||
params:
|
params:
|
||||||
\RainLoop\Model\Account $oAccount
|
\RainLoop\Model\Account $oAccount
|
||||||
\MailSo\Imap\ImapClient $oImapClient
|
\MailSo\Imap\ImapClient $oImapClient
|
||||||
array &$aCredentials
|
\MailSo\Imap\Settings $oSettings
|
||||||
|
|
||||||
### imap.after-connect
|
### imap.after-connect
|
||||||
params:
|
params:
|
||||||
\RainLoop\Model\Account $oAccount
|
\RainLoop\Model\Account $oAccount
|
||||||
\MailSo\Imap\ImapClient $oImapClient
|
\MailSo\Imap\ImapClient $oImapClient
|
||||||
array $aCredentials
|
\MailSo\Imap\Settings $oSettings
|
||||||
|
|
||||||
### imap.before-login
|
### imap.before-login
|
||||||
params:
|
params:
|
||||||
\RainLoop\Model\Account $oAccount
|
\RainLoop\Model\Account $oAccount
|
||||||
\MailSo\Imap\ImapClient $oImapClient
|
\MailSo\Imap\ImapClient $oImapClient
|
||||||
array &$aCredentials
|
\MailSo\Imap\Settings $oSettings
|
||||||
|
|
||||||
### imap.after-login
|
### imap.after-login
|
||||||
params:
|
params:
|
||||||
\RainLoop\Model\Account $oAccount
|
\RainLoop\Model\Account $oAccount
|
||||||
\MailSo\Imap\ImapClient $oImapClient
|
\MailSo\Imap\ImapClient $oImapClient
|
||||||
bool $bSuccess
|
bool $bSuccess
|
||||||
array $aCredentials
|
\MailSo\Imap\Settings $oSettings
|
||||||
|
|
||||||
## Sieve
|
## Sieve
|
||||||
|
|
||||||
### sieve.before-connect
|
### sieve.before-connect
|
||||||
params:
|
params:
|
||||||
\RainLoop\Model\Account $oAccount
|
\RainLoop\Model\Account $oAccount
|
||||||
\MailSo\Sieve\ManageSieveClient $oSieveClient
|
\MailSo\Sieve\SieveClient $oSieveClient
|
||||||
array &$aCredentials
|
\MailSo\Sieve\Settings $oSettings
|
||||||
|
|
||||||
### sieve.after-connect
|
### sieve.after-connect
|
||||||
params:
|
params:
|
||||||
\RainLoop\Model\Account $oAccount
|
\RainLoop\Model\Account $oAccount
|
||||||
\MailSo\Sieve\ManageSieveClient $oSieveClient
|
\MailSo\Sieve\SieveClient $oSieveClient
|
||||||
array $aCredentials
|
\MailSo\Sieve\Settings $oSettings
|
||||||
|
|
||||||
### sieve.before-login
|
### sieve.before-login
|
||||||
params:
|
params:
|
||||||
\RainLoop\Model\Account $oAccount
|
\RainLoop\Model\Account $oAccount
|
||||||
\MailSo\Sieve\ManageSieveClient $oSieveClient
|
\MailSo\Sieve\SieveClient $oSieveClient
|
||||||
array &$aCredentials
|
\MailSo\Sieve\Settings $oSettings
|
||||||
|
|
||||||
### sieve.after-login
|
### sieve.after-login
|
||||||
params:
|
params:
|
||||||
\RainLoop\Model\Account $oAccount
|
\RainLoop\Model\Account $oAccount
|
||||||
\MailSo\Sieve\ManageSieveClient $oSieveClient
|
\MailSo\Sieve\SieveClient $oSieveClient
|
||||||
bool $bSuccess
|
bool $bSuccess
|
||||||
array $aCredentials
|
\MailSo\Sieve\Settings $oSettings
|
||||||
|
|
||||||
## SMTP
|
## SMTP
|
||||||
|
|
||||||
|
|
@ -126,26 +197,26 @@ $Plugin->addHook('hook.name', 'functionName');
|
||||||
params:
|
params:
|
||||||
\RainLoop\Model\Account $oAccount
|
\RainLoop\Model\Account $oAccount
|
||||||
\MailSo\Smtp\SmtpClient $oSmtpClient
|
\MailSo\Smtp\SmtpClient $oSmtpClient
|
||||||
array &$aCredentials
|
\MailSo\Smtp\Settings $oSettings
|
||||||
|
|
||||||
### smtp.after-connect
|
### smtp.after-connect
|
||||||
params:
|
params:
|
||||||
\RainLoop\Model\Account $oAccount
|
\RainLoop\Model\Account $oAccount
|
||||||
\MailSo\Smtp\SmtpClient $oSmtpClient
|
\MailSo\Smtp\SmtpClient $oSmtpClient
|
||||||
array $aCredentials
|
\MailSo\Smtp\Settings $oSettings
|
||||||
|
|
||||||
### smtp.before-login
|
### smtp.before-login
|
||||||
params:
|
params:
|
||||||
\RainLoop\Model\Account $oAccount
|
\RainLoop\Model\Account $oAccount
|
||||||
\MailSo\Smtp\SmtpClient $oSmtpClient
|
\MailSo\Smtp\SmtpClient $oSmtpClient
|
||||||
array &$aCredentials
|
\MailSo\Smtp\Settings $oSettings
|
||||||
|
|
||||||
### smtp.after-login
|
### smtp.after-login
|
||||||
params:
|
params:
|
||||||
\RainLoop\Model\Account $oAccount
|
\RainLoop\Model\Account $oAccount
|
||||||
\MailSo\Smtp\SmtpClient $oSmtpClient
|
\MailSo\Smtp\SmtpClient $oSmtpClient
|
||||||
bool $bSuccess
|
bool $bSuccess
|
||||||
array $aCredentials
|
\MailSo\Smtp\Settings $oSettings
|
||||||
|
|
||||||
## Folders
|
## Folders
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,8 +15,9 @@ class AutoDomainGrabPlugin extends \RainLoop\Plugins\AbstractPlugin
|
||||||
{
|
{
|
||||||
const
|
const
|
||||||
NAME = 'Auto Domain Selection',
|
NAME = 'Auto Domain Selection',
|
||||||
VERSION = '2.8',
|
VERSION = '2.9',
|
||||||
REQUIRED = '2.8.0',
|
RELEASE = '2022-11-11',
|
||||||
|
REQUIRED = '2.21.0',
|
||||||
CATEGORY = 'General',
|
CATEGORY = 'General',
|
||||||
DESCRIPTION = 'Sets the IMAP/SMTP host based on the user\'s login';
|
DESCRIPTION = 'Sets the IMAP/SMTP host based on the user\'s login';
|
||||||
|
|
||||||
|
|
@ -32,20 +33,20 @@ class AutoDomainGrabPlugin extends \RainLoop\Plugins\AbstractPlugin
|
||||||
/**
|
/**
|
||||||
* This function detects the IMAP Host, and if it is set to 'auto', replaces it with the MX or email domain.
|
* This function detects the IMAP Host, and if it is set to 'auto', replaces it with the MX or email domain.
|
||||||
*/
|
*/
|
||||||
public function FilterImapCredentials(\RainLoop\Model\Account $oAccount, \MailSo\Imap\ImapClient $oImapClient, array &$aImapCredentials)
|
public function FilterImapCredentials(\RainLoop\Model\Account $oAccount, \MailSo\Imap\ImapClient $oImapClient, \MailSo\Imap\Settings $oSettings)
|
||||||
{
|
{
|
||||||
// Check for mail.$DOMAIN as entered value in RL settings
|
// Check for mail.$DOMAIN as entered value in RL settings
|
||||||
if (!empty($aImapCredentials['Host']) && 'auto' === $aImapCredentials['Host'])
|
if ('auto' === $oSettings->host)
|
||||||
{
|
{
|
||||||
$domain = \substr(\strrchr($oAccount->Email(), '@'), 1);
|
$domain = \substr(\strrchr($oAccount->Email(), '@'), 1);
|
||||||
$mxhosts = array();
|
$mxhosts = array();
|
||||||
if (\getmxrr($domain, $mxhosts) && $mxhosts)
|
if (\getmxrr($domain, $mxhosts) && $mxhosts)
|
||||||
{
|
{
|
||||||
$aImapCredentials['Host'] = $mxhosts[0];
|
$oSettings->host = $mxhosts[0];
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
$aImapCredentials['Host'] = $this->imap_prefix.$domain;
|
$oSettings->host = $this->imap_prefix.$domain;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -53,20 +54,20 @@ class AutoDomainGrabPlugin extends \RainLoop\Plugins\AbstractPlugin
|
||||||
/**
|
/**
|
||||||
* This function detects the SMTP Host, and if it is set to 'auto', replaces it with the MX or email domain.
|
* This function detects the SMTP Host, and if it is set to 'auto', replaces it with the MX or email domain.
|
||||||
*/
|
*/
|
||||||
public function FilterSmtpCredentials(\RainLoop\Model\Account $oAccount, \MailSo\Smtp\SmtpClient $oSmtpClient, array &$aSmtpCredentials)
|
public function FilterSmtpCredentials(\RainLoop\Model\Account $oAccount, \MailSo\Smtp\SmtpClient $oSmtpClient, \MailSo\Smtp\Settings $oSettings)
|
||||||
{
|
{
|
||||||
// Check for mail.$DOMAIN as entered value in RL settings
|
// Check for mail.$DOMAIN as entered value in RL settings
|
||||||
if (!empty($aSmtpCredentials['Host']) && 'auto' === $aSmtpCredentials['Host'])
|
if ('auto' === $oSettings->host)
|
||||||
{
|
{
|
||||||
$domain = \substr(\strrchr($oAccount->Email(), '@'), 1);
|
$domain = \substr(\strrchr($oAccount->Email(), '@'), 1);
|
||||||
$mxhosts = array();
|
$mxhosts = array();
|
||||||
if (\getmxrr($domain, $mxhosts) && $mxhosts)
|
if (\getmxrr($domain, $mxhosts) && $mxhosts)
|
||||||
{
|
{
|
||||||
$aSmtpCredentials['Host'] = $mxhosts[0];
|
$oSettings->host = $mxhosts[0];
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
$aSmtpCredentials['Host'] = $this->smtp_prefix . $domain;
|
$oSettings->host = $this->smtp_prefix . $domain;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
20
plugins/avatars/LICENSE
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2016 RainLoop Team
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
this software and associated documentation files (the "Software"), to deal in
|
||||||
|
the Software without restriction, including without limitation the rights to
|
||||||
|
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
100
plugins/avatars/avatars.js
Normal file
|
|
@ -0,0 +1,100 @@
|
||||||
|
(rl => {
|
||||||
|
// if (rl.settings.get('Nextcloud'))
|
||||||
|
const
|
||||||
|
queue = [],
|
||||||
|
avatars = new Map,
|
||||||
|
templateId = 'MailMessageView',
|
||||||
|
getAvatarUid = msg => {
|
||||||
|
let from = msg.from[0],
|
||||||
|
bimi = 'pass' == from.dkimStatus ? 1 : 0;
|
||||||
|
return `${bimi}/${from.email.toLowerCase()}`;
|
||||||
|
},
|
||||||
|
getAvatar = msg => avatars.get(getAvatarUid(msg)),
|
||||||
|
runQueue = (() => {
|
||||||
|
let item = queue.shift();
|
||||||
|
while (item) {
|
||||||
|
let url = getAvatar(item[0]);
|
||||||
|
if (url) {
|
||||||
|
item[1](url);
|
||||||
|
item = queue.shift();
|
||||||
|
} else {
|
||||||
|
// TODO: fetch vCard from Nextcloud contacts
|
||||||
|
// let cfg = rl.settings.get('Nextcloud'),
|
||||||
|
|
||||||
|
let from = item[0].from[0];
|
||||||
|
rl.pluginRemoteRequest((iError, data) => {
|
||||||
|
if (!iError && data?.Result.type) {
|
||||||
|
url = `data:${data.Result.type};base64,${data.Result.data}`;
|
||||||
|
avatars.set(getAvatarUid(item[0]), url);
|
||||||
|
item[1](url);
|
||||||
|
}
|
||||||
|
runQueue();
|
||||||
|
}, 'Avatar', {
|
||||||
|
bimi: 'pass' == from.dkimStatus ? 1 : 0,
|
||||||
|
email: from.email
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).debounce(1000);
|
||||||
|
|
||||||
|
ko.bindingHandlers.fromPic = {
|
||||||
|
init: (element, self, dummy, msg) => {
|
||||||
|
if (msg) {
|
||||||
|
let url = getAvatar(msg),
|
||||||
|
fn = url=>{element.src = url};
|
||||||
|
if (url) {
|
||||||
|
fn(url);
|
||||||
|
} else {
|
||||||
|
queue.push([msg, fn]);
|
||||||
|
runQueue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
addEventListener('rl-view-model.create', e => {
|
||||||
|
if (templateId === e.detail.viewModelTemplateID) {
|
||||||
|
|
||||||
|
const
|
||||||
|
template = document.getElementById(templateId),
|
||||||
|
messageItemHeader = template.content.querySelector('.messageItemHeader');
|
||||||
|
|
||||||
|
if (messageItemHeader) {
|
||||||
|
messageItemHeader.prepend(Element.fromHTML(
|
||||||
|
`<img class="fromPic" data-bind="visible: viewUserPicVisible, attr: {'src': viewUserPic() }">`
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let view = e.detail;
|
||||||
|
view.viewUserPic = ko.observable('');
|
||||||
|
view.viewUserPicVisible = ko.observable(false);
|
||||||
|
|
||||||
|
view.message.subscribe(msg => {
|
||||||
|
view.viewUserPicVisible(false);
|
||||||
|
if (msg) {
|
||||||
|
let url = getAvatar(msg),
|
||||||
|
fn = url => {
|
||||||
|
view.viewUserPic(url);
|
||||||
|
view.viewUserPicVisible(true);
|
||||||
|
};
|
||||||
|
if (url) {
|
||||||
|
fn(url);
|
||||||
|
} else {
|
||||||
|
// let from = msg.from[0], bimi = 'pass' == from.dkimStatus ? 1 : 0;
|
||||||
|
// view.viewUserPic(`?Avatar/${bimi}/${encodeURIComponent(from.email)}`);
|
||||||
|
// view.viewUserPicVisible(true);
|
||||||
|
queue.push([msg, fn]);
|
||||||
|
runQueue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('MailMessageList' === e.detail.viewModelTemplateID) {
|
||||||
|
document.getElementById('MailMessageList').content.querySelector('.messageCheckbox')
|
||||||
|
.append(Element.fromHTML(`<img class="fromPic" data-bind="fromPic:$data">`));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
})(window.rl);
|
||||||
BIN
plugins/avatars/images/empty-contact.png
Normal file
|
After Width: | Height: | Size: 665 B |
BIN
plugins/avatars/images/services/amazon.com.png
Normal file
|
After Width: | Height: | Size: 3.4 KiB |
BIN
plugins/avatars/images/services/apple.com.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
plugins/avatars/images/services/asana.com.png
Normal file
|
After Width: | Height: | Size: 995 B |
BIN
plugins/avatars/images/services/battle.net.png
Normal file
|
After Width: | Height: | Size: 3.1 KiB |
BIN
plugins/avatars/images/services/blizzard.com.png
Normal file
|
After Width: | Height: | Size: 3.1 KiB |
BIN
plugins/avatars/images/services/ea.com.png
Normal file
|
After Width: | Height: | Size: 6 KiB |
BIN
plugins/avatars/images/services/ebay.com.png
Normal file
|
After Width: | Height: | Size: 4.5 KiB |
BIN
plugins/avatars/images/services/facebook.com.png
Normal file
|
After Width: | Height: | Size: 9.9 KiB |
BIN
plugins/avatars/images/services/github.com.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
plugins/avatars/images/services/google.com.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
plugins/avatars/images/services/microsoft.com.png
Normal file
|
After Width: | Height: | Size: 382 B |
BIN
plugins/avatars/images/services/onlive.com.png
Normal file
|
After Width: | Height: | Size: 4.9 KiB |
BIN
plugins/avatars/images/services/paypal.com.png
Normal file
|
After Width: | Height: | Size: 4.4 KiB |
BIN
plugins/avatars/images/services/skype.com.png
Normal file
|
After Width: | Height: | Size: 3.4 KiB |
BIN
plugins/avatars/images/services/steampowered.com.png
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
plugins/avatars/images/services/ted.com.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
plugins/avatars/images/services/twitter.com.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
plugins/avatars/images/services/youtube.com.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
196
plugins/avatars/index.php
Normal file
|
|
@ -0,0 +1,196 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
class AvatarsPlugin extends \RainLoop\Plugins\AbstractPlugin
|
||||||
|
{
|
||||||
|
const
|
||||||
|
NAME = 'Avatar',
|
||||||
|
AUTHOR = 'SnappyMail',
|
||||||
|
URL = 'https://snappymail.eu/',
|
||||||
|
VERSION = '1.0',
|
||||||
|
RELEASE = '2022-11-23',
|
||||||
|
REQUIRED = '2.22.0',
|
||||||
|
CATEGORY = 'Contacts',
|
||||||
|
LICENSE = 'MIT',
|
||||||
|
DESCRIPTION = 'Show photo of sender in message and messages list (supports BIMI and Gravatar, Contacts is still TODO)';
|
||||||
|
|
||||||
|
public function Init() : void
|
||||||
|
{
|
||||||
|
$this->addCss('style.css');
|
||||||
|
$this->addJs('avatars.js');
|
||||||
|
$this->addJsonHook('Avatar', 'DoAvatar');
|
||||||
|
$this->addPartHook('Avatar', 'ServiceAvatar');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST method handling
|
||||||
|
*/
|
||||||
|
public function DoAvatar() : array
|
||||||
|
{
|
||||||
|
$bBimi = !empty($this->jsonParam('bimi'));
|
||||||
|
$sEmail = $this->jsonParam('email');
|
||||||
|
$aResult = $this->getAvatar(\urldecode($sEmail), !empty($sEmail));
|
||||||
|
if ($aResult) {
|
||||||
|
$aResult = [
|
||||||
|
'type' => $aResult[0],
|
||||||
|
'data' => \base64_encode($aResult[1])
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return $this->jsonResponse(__FUNCTION__, $aResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /?Avatar/${bimi}/${from.email}
|
||||||
|
* Not fond of this idea because email address is exposed
|
||||||
|
* Maybe use btoa(from.email) or Crypto.subtle.encrypt({name:'AES-GCM',iv:''}, token, from.email)
|
||||||
|
*/
|
||||||
|
// public function ServiceAvatar(...$aParts)
|
||||||
|
public function ServiceAvatar(string $sServiceName, string $sBimi, string $sEmail)
|
||||||
|
{
|
||||||
|
$aResult = $this->getAvatar(\urldecode($sEmail), !empty($sEmail));
|
||||||
|
if ($aResult) {
|
||||||
|
\header('Content-Type: '.$aResult[0]);
|
||||||
|
echo $aResult[1];
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function configMapping() : array
|
||||||
|
{
|
||||||
|
return array(
|
||||||
|
\RainLoop\Plugins\Property::NewInstance('bimi')->SetLabel('Use BIMI (https://bimigroup.org/)')
|
||||||
|
->SetType(\RainLoop\Enumerations\PluginPropertyType::BOOL)
|
||||||
|
->SetDefaultValue(false),
|
||||||
|
\RainLoop\Plugins\Property::NewInstance('gravatar')->SetLabel('Use Gravatar')
|
||||||
|
->SetType(\RainLoop\Enumerations\PluginPropertyType::BOOL)
|
||||||
|
->SetDefaultValue(false)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getAvatar(string $sEmail, bool $bBimi) : ?array
|
||||||
|
{
|
||||||
|
if (!\strpos($sEmail, '@')) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$oActions = \RainLoop\Api::Actions();
|
||||||
|
$oActions->verifyCacheByKey($sEmail);
|
||||||
|
|
||||||
|
$aResult = null;
|
||||||
|
|
||||||
|
$sAsciiEmail = \MailSo\Base\Utils::IdnToAscii($sEmail, true);
|
||||||
|
$sEmailId = \sha1(\strtolower($sAsciiEmail));
|
||||||
|
|
||||||
|
$sFile = \APP_PRIVATE_DATA . 'avatars/' . $sEmailId;
|
||||||
|
$aFiles = \glob("{$sFile}.*");
|
||||||
|
if ($aFiles) {
|
||||||
|
$aResult = [
|
||||||
|
\mime_content_type($aFiles[0]),
|
||||||
|
\file_get_contents($aFiles[0])
|
||||||
|
];
|
||||||
|
$oActions->cacheByKey($sEmail);
|
||||||
|
return $aResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: lookup contacts vCard and return PHOTO value
|
||||||
|
/*
|
||||||
|
if (!$aResult) {
|
||||||
|
$oAccount = $oActions->getAccountFromToken();
|
||||||
|
if ($oAccount) {
|
||||||
|
$oAddressBookProvider = $oActions->AddressBookProvider($oAccount);
|
||||||
|
if ($oAddressBookProvider) {
|
||||||
|
$oContact = $oAddressBookProvider->GetContactByEmail($sEmail);
|
||||||
|
if ($oContact && $oContact->vCard && $oContact->vCard['PHOTO']) {
|
||||||
|
$aResult = [
|
||||||
|
'text/vcard',
|
||||||
|
$oContact->vCard
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (!$aResult) {
|
||||||
|
$sDomain = \explode('@', $sEmail);
|
||||||
|
$sDomain = \array_pop($sDomain);
|
||||||
|
|
||||||
|
$aUrls = [];
|
||||||
|
|
||||||
|
if ($this->Config()->Get('plugin', 'bimi', false)) {
|
||||||
|
$BIMI = $bBimi ? \SnappyMail\DNS::BIMI($sDomain) : null;
|
||||||
|
if ($BIMI) {
|
||||||
|
$aUrls[] = $BIMI;
|
||||||
|
// $aResult = ['text/uri-list', $BIMI];
|
||||||
|
\SnappyMail\Log::debug('Avatar', "BIMI {$sDomain} for {$sUrl}");
|
||||||
|
} else {
|
||||||
|
\SnappyMail\Log::notice('Avatar', "BIMI 404 for {$sDomain}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->Config()->Get('plugin', 'gravatar', false)) {
|
||||||
|
$aUrls[] = 'http://gravatar.com/avatar/'.\md5(\strtolower($sAsciiEmail)).'?s=80&d=404';
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($aUrls as $sUrl) {
|
||||||
|
if ($aResult = static::getUrl($sUrl)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($aResult) {
|
||||||
|
if (!\is_dir(\APP_PRIVATE_DATA . 'avatars')) {
|
||||||
|
\mkdir(\APP_PRIVATE_DATA . 'avatars', 0700);
|
||||||
|
}
|
||||||
|
\file_put_contents(
|
||||||
|
$sFile . \SnappyMail\File\MimeType::toExtension($aResult[0]),
|
||||||
|
$aResult[1]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$aResult) {
|
||||||
|
$aServices = [
|
||||||
|
"services/{$sDomain}",
|
||||||
|
'services/' . \preg_replace('/^.+\\.([^.]+\\.[^.]+)$/D', '$1', $sDomain),
|
||||||
|
'services/' . \preg_replace('/^(.+\\.)?(paypal\\.[a-z][a-z])$/D', 'paypal.com', $sDomain),
|
||||||
|
'empty-contact' // DATA_IMAGE_USER_DOT_PIC
|
||||||
|
];
|
||||||
|
foreach ($aServices as $service) {
|
||||||
|
if (\file_exists(__DIR__ . "/images/{$service}.png")) {
|
||||||
|
$aResult = [
|
||||||
|
'image/png',
|
||||||
|
\file_get_contents(__DIR__ . "/images/{$service}.png")
|
||||||
|
];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$oActions->cacheByKey($sEmail);
|
||||||
|
|
||||||
|
return $aResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function getUrl(string $sUrl) : ?array
|
||||||
|
{
|
||||||
|
$oHTTP = \SnappyMail\HTTP\Request::factory(/*'socket' or 'curl'*/);
|
||||||
|
$oHTTP->proxy = \RainLoop\Api::Config()->Get('labs', 'curl_proxy', '');
|
||||||
|
$oHTTP->proxy_auth = \RainLoop\Api::Config()->Get('labs', 'curl_proxy_auth', '');
|
||||||
|
$oHTTP->max_response_kb = 0;
|
||||||
|
$oHTTP->timeout = 15; // timeout in seconds.
|
||||||
|
$oResponse = $oHTTP->doRequest('GET', $sUrl);
|
||||||
|
if ($oResponse) {
|
||||||
|
if (200 === $oResponse->status && \str_starts_with($oResponse->getHeader('content-type'), 'image/')) {
|
||||||
|
return [
|
||||||
|
$oResponse->getHeader('content-type'),
|
||||||
|
$oResponse->body
|
||||||
|
];
|
||||||
|
}
|
||||||
|
\SnappyMail\Log::notice('Avatar', "error {$oResponse->status} for {$sUrl}");
|
||||||
|
} else {
|
||||||
|
\SnappyMail\Log::warning('Avatar', "failed for {$sUrl}");
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
25
plugins/avatars/style.css
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
.messageView .fromPic {
|
||||||
|
border: 1px solid var(--border-color, #ddd);
|
||||||
|
float: left;
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
margin: 0 4px 0 0;
|
||||||
|
padding: 1px;
|
||||||
|
border-radius: var(--border-radius, 5px);
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
.checkboxMessage {
|
||||||
|
background: #000 no-repeat center / contain;
|
||||||
|
background: #000 no-repeat right / 32px;
|
||||||
|
width:68px
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
#rl-app .messageCheckbox {
|
||||||
|
padding-right:0;
|
||||||
|
white-space:nowrap;
|
||||||
|
}
|
||||||
|
.messageCheckbox .fromPic {
|
||||||
|
margin: 0 0 -0.4em 0.4em;
|
||||||
|
height: 1.5em;
|
||||||
|
max-width: 1.5em;
|
||||||
|
}
|
||||||
|
|
@ -42,7 +42,7 @@ class DemoAccountPlugin extends \RainLoop\Plugins\AbstractPlugin
|
||||||
{
|
{
|
||||||
if (!$bAdmin && \is_array($aResult) && empty($aResult['Auth'])) {
|
if (!$bAdmin && \is_array($aResult) && empty($aResult['Auth'])) {
|
||||||
$aResult['DevEmail'] = $this->Config()->Get('plugin', 'email', $aResult['DevEmail']);
|
$aResult['DevEmail'] = $this->Config()->Get('plugin', 'email', $aResult['DevEmail']);
|
||||||
$aResult['DevPassword'] = APP_DUMMY;
|
$aResult['DevPassword'] = '********';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -35,8 +35,13 @@ class DemoStorage extends \RainLoop\Providers\Storage\FileStorage
|
||||||
if (!\is_dir($sDataPath) && \mkdir($sDataPath, 0700, true)) {
|
if (!\is_dir($sDataPath) && \mkdir($sDataPath, 0700, true)) {
|
||||||
\file_put_contents("{$sDataPath}/settings",'{"RemoveColors":true,"ListInlineAttachments":true}');
|
\file_put_contents("{$sDataPath}/settings",'{"RemoveColors":true,"ListInlineAttachments":true}');
|
||||||
if (\mkdir($sDataPath.'/.gnupg/private-keys-v1.d', 0700, true)) {
|
if (\mkdir($sDataPath.'/.gnupg/private-keys-v1.d', 0700, true)) {
|
||||||
|
// AES
|
||||||
|
\file_put_contents("{$sDataPath}/.gnupg/private-keys-v1.d/3106F4281F98D820114228FEF16B5BA0D78AA005.key",file_get_contents("{$this->sDataPath}/demo.pgp/.gnupg/private-keys-v1.d/3106F4281F98D820114228FEF16B5BA0D78AA005.key"));
|
||||||
|
\file_put_contents("{$sDataPath}/.gnupg/private-keys-v1.d/82CA239C482423D364BFD6DFC3E400B3B98AD66F.key",file_get_contents("{$this->sDataPath}/demo.pgp/.gnupg/private-keys-v1.d/82CA239C482423D364BFD6DFC3E400B3B98AD66F.key"));
|
||||||
|
// ECC
|
||||||
\file_put_contents("{$sDataPath}/.gnupg/private-keys-v1.d/5A1A6C7310D0508C68E8E74F15068301E83FD1AE.key",file_get_contents("{$this->sDataPath}/demo.pgp/.gnupg/private-keys-v1.d/5A1A6C7310D0508C68E8E74F15068301E83FD1AE.key"));
|
\file_put_contents("{$sDataPath}/.gnupg/private-keys-v1.d/5A1A6C7310D0508C68E8E74F15068301E83FD1AE.key",file_get_contents("{$this->sDataPath}/demo.pgp/.gnupg/private-keys-v1.d/5A1A6C7310D0508C68E8E74F15068301E83FD1AE.key"));
|
||||||
\file_put_contents("{$sDataPath}/.gnupg/private-keys-v1.d/886921A7E06BE56F8E8C51797BB476BB26DF21BF.key",file_get_contents("{$this->sDataPath}/demo.pgp/.gnupg/private-keys-v1.d/886921A7E06BE56F8E8C51797BB476BB26DF21BF.key"));
|
\file_put_contents("{$sDataPath}/.gnupg/private-keys-v1.d/886921A7E06BE56F8E8C51797BB476BB26DF21BF.key",file_get_contents("{$this->sDataPath}/demo.pgp/.gnupg/private-keys-v1.d/886921A7E06BE56F8E8C51797BB476BB26DF21BF.key"));
|
||||||
|
|
||||||
\file_put_contents("{$sDataPath}/.gnupg/pubring.kbx",file_get_contents("{$this->sDataPath}/demo.pgp/.gnupg/pubring.kbx"));
|
\file_put_contents("{$sDataPath}/.gnupg/pubring.kbx",file_get_contents("{$this->sDataPath}/demo.pgp/.gnupg/pubring.kbx"));
|
||||||
\file_put_contents("{$sDataPath}/.gnupg/trustdb.gpg",file_get_contents("{$this->sDataPath}/demo.pgp/.gnupg/trustdb.gpg"));
|
\file_put_contents("{$sDataPath}/.gnupg/trustdb.gpg",file_get_contents("{$this->sDataPath}/demo.pgp/.gnupg/trustdb.gpg"));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -313,6 +313,12 @@ class KolabAddressBook implements \RainLoop\Providers\AddressBook\AddressBookInt
|
||||||
return $aResult;
|
return $aResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function GetContactByEmail(string $sEmail) : ?Contact;
|
||||||
|
{
|
||||||
|
// TODO
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
public function GetContactByID($mID, bool $bIsStrID = false) : ?Contact
|
public function GetContactByID($mID, bool $bIsStrID = false) : ?Contact
|
||||||
{
|
{
|
||||||
if ($bIsStrID) {
|
if ($bIsStrID) {
|
||||||
|
|
|
||||||
|
|
@ -4,11 +4,11 @@ class KolabPlugin extends \RainLoop\Plugins\AbstractPlugin
|
||||||
{
|
{
|
||||||
const
|
const
|
||||||
NAME = 'Kolab',
|
NAME = 'Kolab',
|
||||||
VERSION = '2.0',
|
VERSION = '2.1',
|
||||||
RELEASE = '2022-09-06',
|
RELEASE = '2022-11-23',
|
||||||
CATEGORY = 'Contacts',
|
CATEGORY = 'Contacts',
|
||||||
DESCRIPTION = 'Use an Address Book of Kolab.',
|
DESCRIPTION = 'Use an Address Book of Kolab.',
|
||||||
REQUIRED = '2.18.0';
|
REQUIRED = '2.22.0';
|
||||||
|
|
||||||
public function Init() : void
|
public function Init() : void
|
||||||
{
|
{
|
||||||
|
|
|
||||||