Merge branch 'the-djmaze:master' into master
|
|
@ -273,7 +273,6 @@ curl_proxy_auth = ""
|
|||
force_https = Off
|
||||
custom_login_link = ""
|
||||
custom_logout_link = ""
|
||||
allow_external_login = Off
|
||||
http_client_ip_check_proxy = Off
|
||||
fast_cache_memcache_host = "127.0.0.1"
|
||||
fast_cache_memcache_port = 11211
|
||||
|
|
@ -290,5 +289,5 @@ dev_email = ""
|
|||
dev_password = ""
|
||||
|
||||
[version]
|
||||
current = "2.20.6"
|
||||
current = "2.22.2"
|
||||
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
|
||||
|
||||
### Fixed
|
||||
|
|
|
|||
40
README.md
|
|
@ -41,7 +41,7 @@ Copyright (c) 2013 - 2022 RainLoop
|
|||
|
||||
This fork of RainLoop has the following changes:
|
||||
|
||||
* Privacy/GDPR friendly (no: Social, Gravatar, Facebook, Google, Twitter, DropBox, OwnCloud, X-Mailer)
|
||||
* Privacy/GDPR friendly (no: Social, Gravatar, Facebook, Google, Twitter, DropBox, X-Mailer)
|
||||
* Admin uses password_hash/password_verify
|
||||
* Auth failed attempts written to syslog
|
||||
* Added Fail2ban instructions
|
||||
|
|
@ -57,10 +57,11 @@ This fork of RainLoop has the following changes:
|
|||
* Removed background video support
|
||||
* Removed Sentry (Application Monitoring and Error Tracking Software)
|
||||
* Removed Spyc yaml
|
||||
* Removed OwnCloud
|
||||
* Replaced gulp-uglify with gulp-terser
|
||||
* CRLF => LF line endings
|
||||
* Embed boot.js and boot.css into index.html
|
||||
* Ongoing removal of old JavaScript code (things are native these days)
|
||||
* Removal of old JavaScript code (things are native these days)
|
||||
* Added modified [Squire](https://github.com/neilj/Squire) HTML editor as replacement for CKEditor
|
||||
* Updated [Sabre/VObject](https://github.com/sabre-io/vobject)
|
||||
* Split Admin specific JavaScript code from User code
|
||||
|
|
@ -105,7 +106,6 @@ Supported are:
|
|||
The result is faster and smaller download code (good for mobile networks).
|
||||
|
||||
* Added dev/prototype.js for some additional features
|
||||
* boot.js without webpack overhead
|
||||
* Modified Jua.js to be without jQuery
|
||||
* Replaced Autolinker with simple https/email detection
|
||||
* Replaced momentToNode with proper HTML5 `<time>`
|
||||
|
|
@ -118,6 +118,7 @@ The result is faster and smaller download code (good for mobile networks).
|
|||
* Replaced simplestatemanager with CSS @media
|
||||
* Replaced inputosaurus with own code
|
||||
* Replaced keymaster with own shortcuts handler
|
||||
* Replaced OpenPGP.js v2 with OpenPGP.js v5
|
||||
* Removed ifvisible.js
|
||||
* Removed pikaday
|
||||
* Removed underscore
|
||||
|
|
@ -140,26 +141,26 @@ RainLoop 1.17 vs SnappyMail
|
|||
|
||||
|js/* |RainLoop |Snappy |
|
||||
|--------------- |--------: |--------: |
|
||||
|admin.js |2.170.153 | 81.239 |
|
||||
|app.js |4.207.787 | 406.270 |
|
||||
|boot.js | 868.735 | 2.055 |
|
||||
|libs.js | 658.812 | 192.965 |
|
||||
|admin.js |2.170.153 | 82.419 |
|
||||
|app.js |4.207.787 | 407.238 |
|
||||
|boot.js | 868.735 | 1.989 |
|
||||
|libs.js | 658.812 | 193.075 |
|
||||
|sieve.js | 0 | 86.121 |
|
||||
|polyfills.js | 334.608 | 0 |
|
||||
|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 |
|
||||
|--------------- |--------: |--------: |------: |------: |--------: |--------: |
|
||||
|admin.min.js | 256.831 | 40.167 | 73.606 | 13.399 | 60.877 | 12.006 |
|
||||
|app.min.js | 515.367 | 188.083 |139.456 | 62.512 |110.485 | 53.609 |
|
||||
|boot.min.js | 84.659 | 1.261 | 26.998 | 776 | 23.643 | 633 |
|
||||
|libs.min.js | 584.772 | 92.055 |180.901 | 34.113 |155.182 | 30.641 |
|
||||
|admin.min.js | 256.831 | 40.933 | 73.606 | 13.575 | 60.877 | 12.185 |
|
||||
|app.min.js | 515.367 | 188.830 |139.456 | 62.816 |110.485 | 53.944 |
|
||||
|boot.min.js | 84.659 | 1.216 | 26.998 | 761 | 23.643 | 614 |
|
||||
|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 |
|
||||
|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+sieve |1.217.635 | 323.325 |358.761 |107.885 |299.485 | 94.334 |
|
||||
|TOTAL admin | 959.099 | 133.483 |292.911 | 48.288 |249.877 | 43.280 |
|
||||
|TOTAL user |1.217.635 | 282.130 |358.761 | 97.696 |299.485 | 85.179 |
|
||||
|TOTAL user+sieve |1.217.635 | 324.056 |358.761 |108.180 |299.485 | 94.630 |
|
||||
|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.
|
||||
|
||||
|
|
@ -169,6 +170,7 @@ For a user its around 70% smaller and faster than traditional RainLoop.
|
|||
* Themes work in mobile mode
|
||||
* Bugfix invalid/conflicting css rules
|
||||
* Use flexbox
|
||||
* Use border-box
|
||||
* Split app.css to have separate admin.css
|
||||
* Remove oldschool 'float'
|
||||
* Remove unused css
|
||||
|
|
@ -187,12 +189,12 @@ For a user its around 70% smaller and faster than traditional RainLoop.
|
|||
|
||||
|css/* |RainLoop |Snappy |RL gzip |SM gzip |SM brotli |
|
||||
|------------ |-------: |------: |------: |------: |--------: |
|
||||
|app.css | 340.331 | 83.746 | 46.946 | 17.262 | 14.779 |
|
||||
|app.min.css | 274.947 | 67.483 | 39.647 | 15.311 | 13.365 |
|
||||
|app.css | 340.331 | 82.425 | 46.946 | 17.049 | 14.595 |
|
||||
|app.min.css | 274.947 | 66.502 | 39.647 | 15.049 | 13.221 |
|
||||
|boot.css | | 1.326 | | 664 | 545 |
|
||||
|boot.min.css | | 1.071 | | 590 | 474 |
|
||||
|admin.css | | 30.816 | | 6.939 | 6.028 |
|
||||
|admin.min.css | | 24.969 | | 6.300 | 5.531 |
|
||||
|admin.css | | 29.807 | | 6.744 | 5.851 |
|
||||
|admin.min.css | | 24.223 | | 6.142 | 5.397 |
|
||||
|
||||
### PGP
|
||||
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) => {
|
||||
params = params || {};
|
||||
i18nToNodes(componentInfo.element);
|
||||
if (params.inline) {
|
||||
componentInfo.element.style.display = 'inline-block';
|
||||
}
|
||||
return new ClassObject(params);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -212,7 +212,7 @@ export class AppUser extends AbstractApp {
|
|||
|
||||
start() {
|
||||
if (SettingsGet('Auth')) {
|
||||
rl.setWindowTitle(i18n('GLOBAL/LOADING'));
|
||||
rl.setTitle(i18n('GLOBAL/LOADING'));
|
||||
|
||||
SMAudio.notifications(!!SettingsGet('SoundNotification'));
|
||||
NotificationUserStore.enabled(!!SettingsGet('DesktopNotifications'));
|
||||
|
|
|
|||
|
|
@ -22,7 +22,8 @@ const
|
|||
ics: 'text/calendar',
|
||||
xml: 'text/xml',
|
||||
json: app+'json',
|
||||
asc: app+'pgp-signature',
|
||||
// asc: app+'pgp-signature',
|
||||
// asc: app+'pgp-keys',
|
||||
p10: app+'pkcs10',
|
||||
p7c: app+'pkcs7-mime',
|
||||
p7m: app+'pkcs7-mime',
|
||||
|
|
|
|||
|
|
@ -18,8 +18,8 @@ export const
|
|||
dropdowns = [],
|
||||
dropdownVisibility = ko.observable(false).extend({ rateLimit: 0 }),
|
||||
|
||||
moveAction = ko.observable(false),
|
||||
leftPanelDisabled = ko.observable(false),
|
||||
toggleLeftPanel = () => leftPanelDisabled(!leftPanelDisabled()),
|
||||
|
||||
createElement = (name, attr) => {
|
||||
let el = doc.createElement(name);
|
||||
|
|
@ -68,10 +68,4 @@ dropdownVisibility.subscribe(value => {
|
|||
}
|
||||
});
|
||||
|
||||
leftPanelDisabled.toggle = () => leftPanelDisabled(!leftPanelDisabled());
|
||||
leftPanelDisabled.subscribe(value => {
|
||||
value && moveAction() && moveAction(false);
|
||||
$htmlCL.toggle('rl-left-panel-disabled', value);
|
||||
});
|
||||
|
||||
moveAction.subscribe(value => value && leftPanelDisabled() && leftPanelDisabled(false));
|
||||
leftPanelDisabled.subscribe(value => $htmlCL.toggle('rl-left-panel-disabled', value));
|
||||
|
|
|
|||
|
|
@ -229,32 +229,28 @@ export const
|
|||
|
||||
// if ('TABLE' === name || 'TD' === name || 'TH' === name) {
|
||||
if (!oStyle.backgroundImage) {
|
||||
value = oStyle.width;
|
||||
if (hasAttribute('width')) {
|
||||
if (!value) {
|
||||
if ('TD' !== name && 'TH' !== name) {
|
||||
// Make width responsive
|
||||
if (hasAttribute('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('%')) {
|
||||
oStyle.maxWidth = value;
|
||||
if ('TD' !== name && 'TH' !== name) {
|
||||
value = oStyle.removeProperty('width');
|
||||
if (value) {
|
||||
oStyle.maxWidth = value;
|
||||
oStyle.width = '100%';
|
||||
}
|
||||
}
|
||||
if (hasAttribute('height')) {
|
||||
value = getAttribute('height');
|
||||
if (pInt(value)) {
|
||||
// Make height responsive
|
||||
if (hasAttribute('height')) {
|
||||
value = getAttribute('height');
|
||||
oStyle.height = value.includes('%') ? value : value + 'px';
|
||||
delAttribute('height');
|
||||
}
|
||||
value = oStyle.removeProperty('height');
|
||||
if (value) {
|
||||
oStyle.maxHeight = value;
|
||||
}
|
||||
delAttribute('height');
|
||||
}
|
||||
value = oStyle.removeProperty('height');
|
||||
if (value && !value.includes('%')) {
|
||||
oStyle.maxHeight = value;
|
||||
}
|
||||
}
|
||||
// } else
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { MessageFlagsCache } from 'Common/Cache';
|
||||
import { Notification } from 'Common/Enums';
|
||||
import { MessageSetAction, ComposeType/*, FolderType*/ } from 'Common/EnumsUser';
|
||||
import { doc, createElement, elementById, dropdowns, dropdownVisibility, SettingsGet } from 'Common/Globals';
|
||||
import { doc, createElement, elementById, dropdowns, dropdownVisibility, SettingsGet, leftPanelDisabled } from 'Common/Globals';
|
||||
import { plainToHtml } from 'Common/Html';
|
||||
import { getNotification } from 'Common/Translator';
|
||||
import { EmailModel } from 'Model/Email';
|
||||
|
|
@ -15,6 +15,8 @@ import Remote from 'Remote/User/Fetch';
|
|||
|
||||
export const
|
||||
|
||||
moveAction = ko.observable(false),
|
||||
|
||||
dropdownsDetectVisibility = (() =>
|
||||
dropdownVisibility(!!dropdowns.find(item => item.classList.contains('show')))
|
||||
).debounce(50),
|
||||
|
|
@ -356,3 +358,6 @@ populateMessageBody = (oMessage, popup) => {
|
|||
}, 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);
|
||||
|
||||
this.label = params.label;
|
||||
this.inline = params.inline;
|
||||
}
|
||||
|
||||
click() {
|
||||
|
|
|
|||
7
dev/External/SquireUI.js
vendored
|
|
@ -493,7 +493,12 @@ class SquireUI
|
|||
}
|
||||
|
||||
focus() {
|
||||
('plain' == this.mode ? this.plain : this.squire).focus();
|
||||
if ('plain' == this.mode) {
|
||||
this.plain.focus();
|
||||
this.plain.setSelectionRange(0, 0);
|
||||
} else {
|
||||
this.squire.focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
4
dev/External/User/ko.js
vendored
|
|
@ -2,7 +2,7 @@ import 'External/ko';
|
|||
import ko from 'ko';
|
||||
import { HtmlEditor } from 'Common/Html';
|
||||
import { timeToNode } from 'Common/Translator';
|
||||
import { doc, elementById, addEventsListeners, dropdowns } from 'Common/Globals';
|
||||
import { doc, elementById, addEventsListeners, dropdowns, leftPanelDisabled } from 'Common/Globals';
|
||||
import { dropdownsDetectVisibility } from 'Common/UtilsUser';
|
||||
import { EmailAddressesComponent } from 'Component/EmailAddresses';
|
||||
import { ThemeStore } from 'Stores/Theme';
|
||||
|
|
@ -144,6 +144,8 @@ Object.assign(ko.bindingHandlers, {
|
|||
|
||||
// Remove the Chrome visibility
|
||||
dragImage.style.cssText = '';
|
||||
|
||||
leftPanelDisabled(false);
|
||||
} else {
|
||||
e.preventDefault();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import ko from '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 { SaveSettingStatus } from 'Common/Enums';
|
||||
|
|
@ -97,6 +97,7 @@ export class AbstractViewLeft extends AbstractView
|
|||
{
|
||||
super(templateID, 'left');
|
||||
this.leftPanelDisabled = leftPanelDisabled;
|
||||
this.toggleLeftPanel = toggleLeftPanel;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,15 +1,16 @@
|
|||
import ko from 'ko';
|
||||
import { koComputable } from 'External/ko';
|
||||
import { doc, $htmlCL, elementById, fireEvent } from 'Common/Globals';
|
||||
import { forEachObjectValue, forEachObjectEntry } from 'Common/Utils';
|
||||
import { forEachObjectEntry } from 'Common/Utils';
|
||||
import { i18nToNodes } from 'Common/Translator';
|
||||
|
||||
let
|
||||
SCREENS = {},
|
||||
currentScreen = null,
|
||||
defaultScreenName = '';
|
||||
|
||||
const
|
||||
SCREENS = new Map,
|
||||
|
||||
autofocus = dom => dom.querySelector('[autofocus]')?.focus(),
|
||||
|
||||
visiblePopups = new Set,
|
||||
|
|
@ -18,7 +19,7 @@ const
|
|||
* @param {string} screenName
|
||||
* @returns {?Object}
|
||||
*/
|
||||
screen = screenName => (screenName && SCREENS[screenName]) || null,
|
||||
screen = screenName => (screenName && SCREENS.get(screenName)) || null,
|
||||
|
||||
/**
|
||||
* @param {Function} ViewModelClass
|
||||
|
|
@ -35,8 +36,6 @@ const
|
|||
dialog = ViewTypePopup === vm.viewType,
|
||||
vmPlace = doc.getElementById(position);
|
||||
|
||||
fireEvent('rl-view-model.create', vm);
|
||||
|
||||
ViewModelClass.__builded = true;
|
||||
ViewModelClass.__vm = vm;
|
||||
|
||||
|
|
@ -53,6 +52,7 @@ const
|
|||
|
||||
// Firefox < 98 / Safari < 15.4 HTMLDialogElement not defined
|
||||
if (!vmDom.showModal) {
|
||||
vmDom.className = 'polyfill';
|
||||
vmDom.showModal = () => {
|
||||
vmDom.backdrop ||
|
||||
vmDom.before(vmDom.backdrop = Element.fromHTML('<div class="dialog-backdrop"></div>'));
|
||||
|
|
@ -113,6 +113,8 @@ const
|
|||
vmDom.addEventListener('transitionend', endShowHide);
|
||||
}
|
||||
|
||||
fireEvent('rl-view-model.create', vm);
|
||||
|
||||
ko.applyBindingAccessorsToNode(
|
||||
vmDom,
|
||||
{
|
||||
|
|
@ -129,7 +131,7 @@ const
|
|||
}
|
||||
}
|
||||
|
||||
return ViewModelClass && ViewModelClass.__vm;
|
||||
return ViewModelClass?.__vm;
|
||||
},
|
||||
|
||||
forEachViewModel = (screen, fn) => {
|
||||
|
|
@ -157,11 +159,9 @@ const
|
|||
* @param {Function} ViewModelClassToHide
|
||||
* @returns {void}
|
||||
*/
|
||||
hideScreenPopup = ViewModelClassToHide => {
|
||||
if (ViewModelClassToHide?.__vm && ViewModelClassToHide?.__dom) {
|
||||
ViewModelClassToHide.__vm.modalVisible(false);
|
||||
}
|
||||
},
|
||||
hideScreenPopup = ViewModelClassToHide =>
|
||||
ViewModelClassToHide?.__vm && ViewModelClassToHide.__dom
|
||||
&& ViewModelClassToHide.__vm.modalVisible(false),
|
||||
|
||||
/**
|
||||
* @param {string} screenName
|
||||
|
|
@ -208,9 +208,7 @@ const
|
|||
|
||||
setTimeout(() => {
|
||||
// hide screen
|
||||
if (currentScreen && !isSameScreen) {
|
||||
hideScreen(currentScreen);
|
||||
}
|
||||
currentScreen && !isSameScreen && hideScreen(currentScreen);
|
||||
// --
|
||||
|
||||
currentScreen = vmScreen;
|
||||
|
|
@ -267,21 +265,19 @@ export const
|
|||
*/
|
||||
startScreens = screensClasses => {
|
||||
hasher.clear();
|
||||
forEachObjectValue(SCREENS, screen => hideScreen(screen, 1));
|
||||
SCREENS = {};
|
||||
SCREENS.forEach(screen => hideScreen(screen, 1));
|
||||
SCREENS.clear();
|
||||
currentScreen = null,
|
||||
defaultScreenName = '';
|
||||
|
||||
screensClasses.forEach(CScreen => {
|
||||
if (CScreen) {
|
||||
const vmScreen = new CScreen(),
|
||||
screenName = vmScreen.screenName;
|
||||
defaultScreenName || (defaultScreenName = screenName);
|
||||
SCREENS[screenName] = vmScreen;
|
||||
}
|
||||
const vmScreen = new CScreen(),
|
||||
screenName = vmScreen.screenName;
|
||||
defaultScreenName || (defaultScreenName = screenName);
|
||||
SCREENS.set(screenName, vmScreen);
|
||||
});
|
||||
|
||||
forEachObjectValue(SCREENS, vmScreen => {
|
||||
SCREENS.forEach(vmScreen => {
|
||||
if (!vmScreen.__started) {
|
||||
vmScreen.onStart();
|
||||
vmScreen.__started = true;
|
||||
|
|
|
|||
|
|
@ -1,8 +1,16 @@
|
|||
|
||||
const
|
||||
// RFC2045
|
||||
QPDecodeIn = /=([0-9A-F]{2})/g,
|
||||
QPDecodeOut = (...args) => String.fromCharCode(parseInt(args[1], 16));
|
||||
QPDecodeParams = [/=([0-9A-F]{2})/g, (...args) => String.fromCharCode(parseInt(args[1], 16))],
|
||||
QPDecode = data => data.replace(/=\r?\n/g, '').replace(...QPDecodeParams),
|
||||
decodeText = (charset, data) => {
|
||||
try {
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/Encoding_API/Encodings
|
||||
return new TextDecoder(charset).decode(Uint8Array.from(data, c => c.charCodeAt(0)));
|
||||
} catch (e) {
|
||||
console.error({charset:charset,error:e});
|
||||
}
|
||||
};
|
||||
|
||||
export function ParseMime(text)
|
||||
{
|
||||
|
|
@ -40,22 +48,14 @@ export function ParseMime(text)
|
|||
|
||||
get body() {
|
||||
let body = this.bodyRaw,
|
||||
// charset = this.header('content-type')?.params.charset,
|
||||
charset = this.header('content-type')?.params.charset,
|
||||
encoding = this.headerValue('content-transfer-encoding');
|
||||
if ('quoted-printable' == encoding) {
|
||||
body = body.replace(/=\r?\n/g, '').replace(QPDecodeIn, QPDecodeOut);
|
||||
body = QPDecode(body);
|
||||
} else if ('base64' == encoding) {
|
||||
body = atob(body.replace(/\r?\n/g, ''));
|
||||
}
|
||||
/*
|
||||
try {
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/Encoding_API/Encodings
|
||||
return new TextDecoder(charset).decode(Uint8Array.from(body, c => c.charCodeAt(0)));
|
||||
// return new TextDecoder(charset).decode(new TextEncoder().encode(body));
|
||||
} catch (e) {
|
||||
}
|
||||
*/
|
||||
return body;
|
||||
return decodeText(charset, body);
|
||||
}
|
||||
|
||||
get dataUrl() {
|
||||
|
|
@ -65,7 +65,7 @@ export function ParseMime(text)
|
|||
body = body.replace(/\r?\n/g, '');
|
||||
} else {
|
||||
if ('quoted-printable' == encoding) {
|
||||
body = body.replace(/=\r?\n/g, '').replace(QPDecodeIn, QPDecodeOut);
|
||||
body = QPDecode(body);
|
||||
}
|
||||
body = btoa(body);
|
||||
}
|
||||
|
|
@ -111,8 +111,12 @@ export function ParseMime(text)
|
|||
[...header.matchAll(/;\s*([^;=]+)=\s*"?([^;"]+)"?/g)].forEach(param =>
|
||||
params[param[1].trim().toLowerCase()] = param[2].trim()
|
||||
);
|
||||
// encoded-word = "=?" charset "?" encoding "?" encoded-text "?="
|
||||
match[2] = match[2].trim().replace(/=\?([^?]+)\?(B|Q)\?(.+?)\?=/g, (m, charset, encoding, text) =>
|
||||
decodeText(charset, 'B' == encoding ? atob(text) : QPDecode(text))
|
||||
);
|
||||
headers[match[1].trim().toLowerCase()] = {
|
||||
value: match[2].trim(),
|
||||
value: match[2],
|
||||
params: params
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ export class ComposeAttachmentModel extends AbstractModel {
|
|||
fileName: fileName,
|
||||
size: size,
|
||||
tempName: '',
|
||||
type: '', // application/octet-stream
|
||||
|
||||
progress: 0,
|
||||
error: '',
|
||||
|
|
@ -54,7 +55,7 @@ export class ComposeAttachmentModel extends AbstractModel {
|
|||
return null === localSize ? '' : FileInfo.friendlySize(localSize);
|
||||
},
|
||||
|
||||
mimeType: () => FileInfo.getContentType(this.fileName()),
|
||||
mimeType: () => this.type() || FileInfo.getContentType(this.fileName()),
|
||||
fileExt: () => FileInfo.getExtension(this.fileName()),
|
||||
|
||||
iconClass: () => FileInfo.getIconClass(this.fileExt(), this.mimeType())
|
||||
|
|
|
|||
|
|
@ -119,7 +119,7 @@ export class ContactModel extends AbstractModel {
|
|||
|
||||
display: () => {
|
||||
let a = this.fullName(),
|
||||
b = this.email()?.[0]?.value,
|
||||
b = this.email()?.[0]?.value(),
|
||||
c = this.nickname();
|
||||
return a || b || c;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,6 @@ export class LoginAdminScreen extends AbstractScreen {
|
|||
}
|
||||
|
||||
onShow() {
|
||||
rl.setWindowTitle();
|
||||
rl.setTitle();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,6 +37,6 @@ export class SettingsAdminScreen extends AbstractSettingsScreen {
|
|||
}
|
||||
|
||||
onShow() {
|
||||
rl.setWindowTitle();
|
||||
rl.setTitle();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,6 @@ export class LoginUserScreen extends AbstractScreen {
|
|||
}
|
||||
|
||||
onShow() {
|
||||
rl.setWindowTitle();
|
||||
rl.setTitle();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import { Scope } from 'Common/Enums';
|
||||
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 { setLayoutResizer } from 'Common/UtilsUser';
|
||||
import { setLayoutResizer, moveAction } from 'Common/UtilsUser';
|
||||
import { getFolderFromCacheList, getFolderFullName, getFolderInboxName } from 'Common/Cache';
|
||||
import { i18n, initOnStartOrLangChange } from 'Common/Translator';
|
||||
import { SettingsUserStore } from 'Stores/User/Settings';
|
||||
|
|
@ -46,7 +46,7 @@ export class MailBoxUserScreen extends AbstractScreen {
|
|||
const count = Settings.app('listPermanentFiltered') ? 0 : FolderUserStore.foldersInboxUnreadCount(),
|
||||
email = AccountUserStore.email();
|
||||
|
||||
rl.setWindowTitle(
|
||||
rl.setTitle(
|
||||
(email
|
||||
? '' + (0 < count ? '(' + count + ') ' : ' ') + email + ' - '
|
||||
: ''
|
||||
|
|
|
|||
|
|
@ -72,6 +72,6 @@ export class SettingsUserScreen extends AbstractSettingsScreen {
|
|||
|
||||
setSettingsTitle() {
|
||||
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*/ {
|
||||
constructor() {
|
||||
this.domains = DomainAdminStore;
|
||||
|
||||
this.username = ko.observable('');
|
||||
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() {
|
||||
showScreenPopup(DomainPopupView);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,8 +42,8 @@ export class UserSettingsGeneral extends AbstractViewSettings {
|
|||
|
||||
['layout', 'messageReadDelay', 'messagesPerPage',
|
||||
'editorDefaultType', 'requestReadReceipt', 'requestDsn', 'pgpSign', 'pgpEncrypt',
|
||||
'viewHTML', 'showImages', 'removeColors', 'hideDeleted', 'listInlineAttachments',
|
||||
'useCheckboxesInList', 'useThreads', 'replySameFolder', 'msgDefaultAction'
|
||||
'viewHTML', 'showImages', 'removeColors', 'hideDeleted', 'listInlineAttachments', 'simpleAttachmentsList',
|
||||
'useCheckboxesInList', 'useThreads', 'replySameFolder', 'msgDefaultAction', 'allowSpellcheck'
|
||||
].forEach(name => this[name] = SettingsUserStore[name]);
|
||||
|
||||
this.allowLanguagesOnSettings = !!SettingsGet('AllowLanguagesOnSettings');
|
||||
|
|
@ -97,9 +97,9 @@ export class UserSettingsGeneral extends AbstractViewSettings {
|
|||
this.addSetting('MessagesPerPage');
|
||||
this.addSetting('Layout');
|
||||
|
||||
this.addSettings(['ViewHTML', 'ShowImages', 'HideDeleted', 'ListInlineAttachments',
|
||||
this.addSettings(['ViewHTML', 'ShowImages', 'HideDeleted', 'ListInlineAttachments', 'simpleAttachmentsList',
|
||||
'UseCheckboxesInList', 'ReplySameFolder',
|
||||
'requestReadReceipt', 'requestDsn', 'pgpSign', 'pgpEncrypt',
|
||||
'requestReadReceipt', 'requestDsn', 'pgpSign', 'pgpEncrypt', 'allowSpellcheck',
|
||||
'DesktopNotifications', 'SoundNotification']);
|
||||
|
||||
const fReloadLanguageHelper = (saveSettingsStep) => () => {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
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 { serverRequestRaw } from 'Common/Links';
|
||||
import { SaveSettingStatus } from 'Common/Enums';
|
||||
|
|
@ -7,6 +7,8 @@ import { SaveSettingStatus } from 'Common/Enums';
|
|||
let __themeTimer = 0;
|
||||
|
||||
export const
|
||||
appEl = () => elementById('rl-app'),
|
||||
|
||||
ThemeStore = {
|
||||
theme: ko.observable(''),
|
||||
themes: ko.observableArray(),
|
||||
|
|
@ -65,7 +67,7 @@ ThemeStore.isMobile.subscribe(value => $htmlCL.toggle('rl-mobile', value));
|
|||
|
||||
ThemeStore.fontSansSerif.subscribe(value => {
|
||||
if (null != value) {
|
||||
let cl = elementById('rl-app').classList;
|
||||
let cl = appEl().classList;
|
||||
cl.forEach(name => {
|
||||
if (name.startsWith('font') && !/font(Serif|Mono)/.test(name)) {
|
||||
cl.remove(name);
|
||||
|
|
@ -76,25 +78,20 @@ ThemeStore.fontSansSerif.subscribe(value => {
|
|||
});
|
||||
ThemeStore.fontSerif.subscribe(value => {
|
||||
if (null != value) {
|
||||
let cl = elementById('rl-app').classList;
|
||||
let cl = appEl().classList;
|
||||
cl.forEach(name => name.startsWith('fontSerif') && cl.remove(name));
|
||||
value && cl.add('fontSerif'+value);
|
||||
}
|
||||
});
|
||||
ThemeStore.fontMono.subscribe(value => {
|
||||
if (null != value) {
|
||||
let cl = elementById('rl-app').classList;
|
||||
let cl = appEl().classList;
|
||||
cl.forEach(name => name.startsWith('fontMono') && cl.remove(name));
|
||||
value && cl.add('fontMono'+value);
|
||||
}
|
||||
});
|
||||
|
||||
ThemeStore.userBackgroundHash.subscribe(value => {
|
||||
if (value) {
|
||||
$htmlCL.add('UserBackground');
|
||||
doc.body.style.backgroundImage = "url("+serverRequestRaw('UserBackground', value)+")";
|
||||
} else {
|
||||
$htmlCL.remove('UserBackground');
|
||||
doc.body.removeAttribute('style');
|
||||
}
|
||||
appEl().classList.toggle('UserBackground', !!value);
|
||||
appEl().style.backgroundImage = value ? "url("+serverRequestRaw('UserBackground', value)+")" : null;
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import ko from 'ko';
|
||||
import { SettingsGet } from 'Common/Globals';
|
||||
import { pInt } from 'Common/Utils';
|
||||
import { koComputable, addObservablesTo, koArrayWithDestroy } from 'External/ko';
|
||||
import Remote from 'Remote/User/Fetch';
|
||||
|
||||
|
|
@ -51,16 +50,14 @@ ContactUserStore.sync = fResultFunc => {
|
|||
};
|
||||
|
||||
ContactUserStore.init = () => {
|
||||
let value = !!SettingsGet('ContactsSyncIsAllowed');
|
||||
ContactUserStore.allowSync(value);
|
||||
if (value) {
|
||||
ContactUserStore.syncMode(SettingsGet('ContactsSyncMode'));
|
||||
ContactUserStore.syncUrl(SettingsGet('ContactsSyncUrl'));
|
||||
ContactUserStore.syncUser(SettingsGet('ContactsSyncUser'));
|
||||
ContactUserStore.syncPass(SettingsGet('ContactsSyncPassword'));
|
||||
let config = SettingsGet('ContactsSync');
|
||||
ContactUserStore.allowSync(!!config);
|
||||
if (config) {
|
||||
ContactUserStore.syncMode(config.Mode);
|
||||
ContactUserStore.syncUrl(config.Url);
|
||||
ContactUserStore.syncUser(config.User);
|
||||
ContactUserStore.syncPass(config.Password);
|
||||
setTimeout(ContactUserStore.sync, 10000);
|
||||
value = pInt(SettingsGet('ContactsSyncInterval'));
|
||||
value = 5 <= value ? (320 >= value ? value : 320) : 20;
|
||||
setInterval(ContactUserStore.sync, value * 60000 + 5000);
|
||||
setInterval(ContactUserStore.sync, config.Interval * 60000 + 5000);
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -186,7 +186,7 @@ export const GnuPGUserStore = new class {
|
|||
}
|
||||
if (null !== params.Passphrase) {
|
||||
const result = await Remote.post('GnupgDecrypt', null, params);
|
||||
if (result?.Result) {
|
||||
if (result?.Result && false !== result.Result.data) {
|
||||
return result.Result;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -347,7 +347,7 @@ MessagelistUserStore.removeMessagesFromList = (
|
|||
fromFolderFullName, oUids, toFolderFullName = '', copy = false
|
||||
) => {
|
||||
let unseenCount = 0,
|
||||
messageList = MessagelistUserStore,
|
||||
setPage = 0,
|
||||
currentMessage = MessageUserStore.message();
|
||||
|
||||
const trashFolder = FolderUserStore.trashFolder(),
|
||||
|
|
@ -356,7 +356,7 @@ MessagelistUserStore.removeMessagesFromList = (
|
|||
toFolder = toFolderFullName ? getFolderFromCacheList(toFolderFullName) : null,
|
||||
messages =
|
||||
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);
|
||||
|
|
@ -406,47 +406,47 @@ MessagelistUserStore.removeMessagesFromList = (
|
|||
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 (
|
||||
messageList.length &&
|
||||
messageList.find(item => item?.deleted() && item.uid == MessagelistUserStore.threadUid())
|
||||
) {
|
||||
const message = messageList.find(item => item && !item.deleted());
|
||||
let setHash;
|
||||
if (!message) {
|
||||
if (1 < MessagelistUserStore.page()) {
|
||||
MessagelistUserStore.page(MessagelistUserStore.page() - 1);
|
||||
setHash = 1;
|
||||
} else {
|
||||
MessagelistUserStore.threadUid(0);
|
||||
replaceHash(
|
||||
mailBox(
|
||||
FolderUserStore.currentFolderFullNameHash(),
|
||||
MessagelistUserStore.pageBeforeThread(),
|
||||
MessagelistUserStore.listSearch()
|
||||
)
|
||||
);
|
||||
}
|
||||
} else if (MessagelistUserStore.threadUid() != message.uid) {
|
||||
MessagelistUserStore.threadUid(message.uid);
|
||||
setHash = 1;
|
||||
}
|
||||
if (setHash) {
|
||||
replaceHash(
|
||||
mailBox(
|
||||
FolderUserStore.currentFolderFullNameHash(),
|
||||
MessagelistUserStore.page(),
|
||||
MessagelistUserStore.listSearch(),
|
||||
MessagelistUserStore.threadUid()
|
||||
)
|
||||
);
|
||||
if (MessagelistUserStore.threadUid()
|
||||
&& MessagelistUserStore.length
|
||||
&& MessagelistUserStore.find(item => item?.deleted() && item.uid == MessagelistUserStore.threadUid())
|
||||
) {
|
||||
const message = MessagelistUserStore.find(item => item && !item.deleted());
|
||||
if (!message) {
|
||||
if (1 < MessagelistUserStore.page()) {
|
||||
setPage = MessagelistUserStore.page() - 1;
|
||||
} else {
|
||||
MessagelistUserStore.threadUid(0);
|
||||
setPage = MessagelistUserStore.pageBeforeThread();
|
||||
}
|
||||
} else if (MessagelistUserStore.threadUid() != message.uid) {
|
||||
MessagelistUserStore.threadUid(message.uid);
|
||||
setPage = MessagelistUserStore.page();
|
||||
}
|
||||
}
|
||||
|
||||
if (setPage) {
|
||||
MessagelistUserStore.page(setPage);
|
||||
replaceHash(
|
||||
mailBox(
|
||||
FolderUserStore.currentFolderFullNameHash(),
|
||||
setPage,
|
||||
MessagelistUserStore.listSearch(),
|
||||
MessagelistUserStore.threadUid()
|
||||
)
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
MessagelistUserStore.reloadFlagsAndCachedMessage = () => {
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ export const SettingsUserStore = new class {
|
|||
showImages: 0,
|
||||
removeColors: 0,
|
||||
listInlineAttachments: 0,
|
||||
simpleAttachmentsList: 0,
|
||||
useCheckboxesInList: 1,
|
||||
allowDraftAutosave: 1,
|
||||
useThreads: 0,
|
||||
|
|
@ -32,6 +33,7 @@ export const SettingsUserStore = new class {
|
|||
requestDsn: 0,
|
||||
pgpSign: 0,
|
||||
pgpEncrypt: 0,
|
||||
allowSpellcheck: 0,
|
||||
|
||||
layout: 1,
|
||||
editorDefaultType: 'Html',
|
||||
|
|
@ -79,6 +81,7 @@ export const SettingsUserStore = new class {
|
|||
self.showImages(SettingsGet('ShowImages'));
|
||||
self.removeColors(SettingsGet('RemoveColors'));
|
||||
self.listInlineAttachments(SettingsGet('ListInlineAttachments'));
|
||||
self.simpleAttachmentsList(SettingsGet('simpleAttachmentsList'));
|
||||
self.useCheckboxesInList(SettingsGet('UseCheckboxesInList'));
|
||||
self.allowDraftAutosave(SettingsGet('AllowDraftAutosave'));
|
||||
self.useThreads(SettingsGet('UseThreads'));
|
||||
|
|
@ -92,5 +95,6 @@ export const SettingsUserStore = new class {
|
|||
self.requestDsn(SettingsGet('requestDsn'));
|
||||
self.pgpSign(SettingsGet('pgpSign'));
|
||||
self.pgpEncrypt(SettingsGet('pgpEncrypt'));
|
||||
self.allowSpellcheck(SettingsGet('allowSpellcheck'));
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -38,12 +38,26 @@
|
|||
html, body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
-webkit-text-size-adjust: 100%; /* 2 */
|
||||
}
|
||||
|
||||
#rl-app *, #rl-app *::before, #rl-app *::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
#rl-app {
|
||||
background-color: var(--main-bg-color, #aaa);
|
||||
color: var(--main-color, #333);
|
||||
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 {
|
||||
|
|
|
|||
|
|
@ -57,11 +57,7 @@
|
|||
@import "_End.less";
|
||||
|
||||
@media print {
|
||||
body, html {
|
||||
height: auto;
|
||||
overflow: auto;
|
||||
}
|
||||
body, #rl-right {
|
||||
#rl-app, #rl-right {
|
||||
position: initial;
|
||||
}
|
||||
#rl-left {
|
||||
|
|
|
|||
|
|
@ -7,12 +7,10 @@
|
|||
.domain-name {
|
||||
display: inline-block;
|
||||
word-break: break-all;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.domain-alias {
|
||||
display: inline-block;
|
||||
box-sizing: border-box;
|
||||
color: #bbb;
|
||||
padding-left: 5px;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -45,17 +45,15 @@
|
|||
height: 100%;
|
||||
|
||||
> .b-toolbar {
|
||||
color: #fff;
|
||||
display: flex;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
left: 0;
|
||||
height: 34px;
|
||||
padding: 8px @rlLowMargin 8px 0;
|
||||
color: #fff;
|
||||
text-shadow: 0 1px 1px #000;
|
||||
|
||||
display: flex;
|
||||
|
||||
h4 {
|
||||
flex-grow: 1;
|
||||
margin: 8px;
|
||||
|
|
@ -73,7 +71,6 @@
|
|||
border: 1px solid var(--border-color, #aaa);
|
||||
border-radius: var(--border-radius, 5px);
|
||||
box-shadow: @rlMainShadow;
|
||||
box-sizing: border-box;
|
||||
height: calc(100% - 50px - @rlLowMargin - @rlLowMargin);
|
||||
margin: (50px + @rlLowMargin) @rlLowMargin @rlLowMargin 0;
|
||||
overflow-y: auto;
|
||||
|
|
|
|||
|
|
@ -2,17 +2,6 @@
|
|||
|
||||
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 {
|
||||
width: 400px;
|
||||
height: 70px;
|
||||
|
|
|
|||
|
|
@ -80,7 +80,6 @@
|
|||
|
||||
> div > div {
|
||||
position: absolute;
|
||||
box-sizing: border-box;
|
||||
margin-top: 1px;
|
||||
|
||||
animation: checkmark-to-box 200ms ease-out forwards;
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
display: inline-block;
|
||||
padding: 5px 15px;
|
||||
margin: 2px 5px;
|
||||
width: 180px;
|
||||
width: 216px;
|
||||
background-color: rgba(128, 128, 128, 0.1);
|
||||
border: 1px solid transparent;
|
||||
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 {
|
||||
width: calc(100% - 40px);
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -112,7 +112,7 @@
|
|||
}
|
||||
|
||||
.buttonLogin {
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.alert {
|
||||
|
|
|
|||
|
|
@ -3,23 +3,6 @@
|
|||
* 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 {
|
||||
/* cursor: wait;*/
|
||||
cursor: progress;
|
||||
|
|
|
|||
|
|
@ -20,7 +20,6 @@
|
|||
.settings-save-trigger {
|
||||
|
||||
display: inline-block;
|
||||
height: 1em;
|
||||
line-height: 1em;
|
||||
margin-left: 0.5em !important;
|
||||
|
||||
|
|
|
|||
|
|
@ -83,7 +83,6 @@
|
|||
}
|
||||
|
||||
.iconMain, .iconPreview {
|
||||
box-sizing: border-box;
|
||||
padding: 6px 0 0;
|
||||
font-size: 36px;
|
||||
color: #aaa;
|
||||
|
|
@ -120,3 +119,22 @@
|
|||
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 {
|
||||
box-sizing: border-box;
|
||||
height: 100%;
|
||||
min-height: 200px;
|
||||
position: relative;
|
||||
|
|
@ -150,6 +149,8 @@
|
|||
@media screen and (max-width: 999px) {
|
||||
#V-PopupsCompose {
|
||||
border: 0;
|
||||
height: calc(100vh - 100px);
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@
|
|||
border: 1px solid var(--input-border-clr, #ccc);
|
||||
border-radius: var(--input-border-radius, 3px);
|
||||
box-shadow: inset 0 1px 1px rgba(0,0,0,.075);
|
||||
box-sizing: border-box;
|
||||
color: var(--input-clr, #555);
|
||||
cursor: text;
|
||||
font-size: 90%;
|
||||
|
|
|
|||
|
|
@ -31,7 +31,6 @@
|
|||
|
||||
.b-toolbar {
|
||||
top: 0;
|
||||
height: 30px;
|
||||
padding: 10px 0 0 @rlLowMargin;
|
||||
color: #fff;
|
||||
z-index: 101;
|
||||
|
|
@ -39,7 +38,6 @@
|
|||
|
||||
.b-footer {
|
||||
bottom: 10px;
|
||||
height: 30px;
|
||||
padding: 0 10px 0 5px;
|
||||
z-index: 101;
|
||||
|
||||
|
|
@ -69,7 +67,6 @@
|
|||
display: block;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
height: 34px;
|
||||
line-height: 34px;
|
||||
background-color: transparent;
|
||||
vertical-align: middle;
|
||||
|
|
@ -222,8 +219,7 @@
|
|||
max-width: -webkit-fit-content;
|
||||
max-width: fit-content;
|
||||
overflow: hidden;
|
||||
height: 19px;
|
||||
padding: 4px 8px;
|
||||
max-height: 2em;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
|
||||
.UserBackground body {
|
||||
#rl-app.UserBackground {
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
|
|
@ -105,7 +105,7 @@ html:not(.rl-left-panel-disabled) #rl-left > .resizer,
|
|||
|
||||
#V-MailMessageView {
|
||||
position: absolute;
|
||||
top: 50px + @rlLowMargin;
|
||||
top: 50px;
|
||||
bottom: 13px;
|
||||
right: 0;
|
||||
left: 0;
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@ html.rl-no-preview-pane {
|
|||
}
|
||||
|
||||
#V-MailMessageList .btn-toolbar {
|
||||
height: 30px;
|
||||
padding: 10px 1px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ html.rl-no-preview-pane {
|
|||
|
||||
.toolbar {
|
||||
position: absolute;
|
||||
top: -50px - @rlLowMargin;
|
||||
top: -50px;
|
||||
right: 0;
|
||||
left: 0;
|
||||
height: 30px;
|
||||
|
|
@ -44,9 +44,8 @@ html.rl-no-preview-pane {
|
|||
.b-message-view-checked-helper {
|
||||
text-align: center;
|
||||
font-size: 70px;
|
||||
line-height: 70px;
|
||||
padding-top: 100px;
|
||||
color: #999;
|
||||
padding-top: 25vh;
|
||||
opacity: 0.6;
|
||||
}
|
||||
.b-message-view-checked-helper::after {
|
||||
content: ' ✉';
|
||||
|
|
@ -58,7 +57,7 @@ html.rl-no-preview-pane {
|
|||
font-size: 24px;
|
||||
line-height: 30px;
|
||||
color: #999;
|
||||
padding: 120px 10px 0 10px;
|
||||
padding: 25vh 10px 0;
|
||||
}
|
||||
|
||||
.b-message-view-desc.error {
|
||||
|
|
@ -95,7 +94,6 @@ html.rl-no-preview-pane {
|
|||
}
|
||||
|
||||
.messageTags {
|
||||
margin-top: 6px;
|
||||
align-items: baseline;
|
||||
display: flex;
|
||||
}
|
||||
|
|
@ -128,7 +126,6 @@ html.rl-no-preview-pane {
|
|||
display: flex;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.subject {
|
||||
|
|
@ -148,7 +145,7 @@ html.rl-no-preview-pane {
|
|||
}
|
||||
|
||||
.informationShort {
|
||||
margin: 4px 40px 0 0;
|
||||
margin: 0 40px 0 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
|
|
@ -253,33 +250,6 @@ html.rl-no-preview-pane {
|
|||
overflow: auto;
|
||||
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 {
|
||||
text-align: center;
|
||||
font-size: 24px;
|
||||
|
|
@ -345,6 +315,7 @@ html.rl-no-preview-pane {
|
|||
border-bottom: 1px solid var(--border-color, #bbb);
|
||||
}
|
||||
.attachmentsControls > * {
|
||||
display: inline-block;
|
||||
margin-right: 0.5em;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
|
@ -539,9 +510,3 @@ html.rl-fullscreen {
|
|||
border: 0;
|
||||
}
|
||||
}
|
||||
|
||||
html:not(.rl-mobile) #messageItem {
|
||||
.buttonFull {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,11 +27,13 @@
|
|||
cursor: pointer;
|
||||
display: block;
|
||||
font-size: 18px;
|
||||
height: 30px;
|
||||
line-height: 29px;
|
||||
line-height: 2em;
|
||||
max-height: 2em;
|
||||
overflow: hidden;
|
||||
padding: 4px 10px;
|
||||
padding: 0 10px;
|
||||
text-decoration: none;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
a:focus, a:hover {
|
||||
|
|
@ -54,7 +56,6 @@
|
|||
top: 0;
|
||||
right: 0;
|
||||
left: 0;
|
||||
height: 34px;
|
||||
padding: 8px 0;
|
||||
color: #fff;
|
||||
}
|
||||
|
|
@ -71,7 +72,6 @@
|
|||
scroll-behavior: smooth;
|
||||
padding:20px;
|
||||
|
||||
box-sizing: border-box;
|
||||
height: calc(100% - 50px - @rlLowMargin - @rlLowMargin);
|
||||
|
||||
background-color: var(--settings-bg-clr, #fff);
|
||||
|
|
|
|||
|
|
@ -37,7 +37,6 @@
|
|||
*/
|
||||
|
||||
.squire-wysiwyg, .squire-plain {
|
||||
box-sizing: border-box;
|
||||
font-size: 13px;
|
||||
height: 100%;
|
||||
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;
|
||||
top: 0;
|
||||
right: 0;
|
||||
height: 30px;
|
||||
padding: 10px @rlLowMargin;
|
||||
z-index: 103;
|
||||
|
||||
|
|
@ -33,7 +32,7 @@
|
|||
cursor: pointer;
|
||||
display: inline-block;
|
||||
font-size: 30px;
|
||||
height: 30px;
|
||||
height: 1em;
|
||||
margin-right: 10px;
|
||||
width: 1em;
|
||||
|
||||
|
|
@ -61,8 +60,7 @@
|
|||
display: inline-block;
|
||||
font-weight: bold;
|
||||
font-size: 16px;
|
||||
height: 29px;
|
||||
line-height: 30px;
|
||||
line-height: 29px;
|
||||
max-width: 25vw;
|
||||
overflow: hidden;
|
||||
padding: 1px 8px;
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
[class^="icon-"],
|
||||
[class*=" icon-"] {
|
||||
display: inline-block;
|
||||
width: 1em;
|
||||
min-width: 1em;
|
||||
|
||||
.disabled &,
|
||||
.btn[disabled] & {
|
||||
|
|
@ -35,8 +35,8 @@
|
|||
|
||||
.icon-spinner {
|
||||
|
||||
height: 0.75em;
|
||||
width: 0.75em;
|
||||
height: 1.2em;
|
||||
width: 1.2em;
|
||||
|
||||
margin: 0 -1px;
|
||||
|
||||
|
|
|
|||
|
|
@ -2,12 +2,13 @@ import Remote from 'Remote/Admin/Fetch';
|
|||
|
||||
import { AbstractViewRight } from 'Knoin/AbstractViews';
|
||||
|
||||
import { leftPanelDisabled } from 'Common/Globals';
|
||||
import { leftPanelDisabled, toggleLeftPanel } from 'Common/Globals';
|
||||
|
||||
export class PaneSettingsAdminView extends AbstractViewRight {
|
||||
constructor() {
|
||||
super('AdminPane');
|
||||
this.leftPanelDisabled = leftPanelDisabled;
|
||||
this.toggleLeftPanel = toggleLeftPanel;
|
||||
}
|
||||
|
||||
logoutClick() {
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import {
|
|||
SetSystemFoldersNotification
|
||||
} 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 { koArrayWithDestroy, addObservablesTo, addComputablesTo, addSubscribablesTo } from 'External/ko';
|
||||
|
||||
|
|
@ -214,6 +214,7 @@ export class ComposePopupView extends AbstractViewPopup {
|
|||
|
||||
this.allowContacts = AppUserStore.allowContacts();
|
||||
this.allowIdentities = SettingsCapa('Identities');
|
||||
this.allowSpellcheck = SettingsUserStore.allowSpellcheck;
|
||||
|
||||
addObservablesTo(this, {
|
||||
identitiesDropdownTrigger: false,
|
||||
|
|
@ -271,15 +272,11 @@ export class ComposePopupView extends AbstractViewPopup {
|
|||
currentIdentity: IdentityUserStore()[0]
|
||||
});
|
||||
|
||||
// this.to.subscribe((v) => console.log(v));
|
||||
|
||||
// Used by ko.bindingHandlers.emailsTags
|
||||
this.to.focused = ko.observable(false);
|
||||
this.to.focused.subscribe(value => value && (this.sLastFocusedField = 'to'));
|
||||
this.cc.focused = ko.observable(false);
|
||||
this.cc.focused.subscribe(value => value && (this.sLastFocusedField = 'cc'));
|
||||
this.bcc.focused = ko.observable(false);
|
||||
this.bcc.focused.subscribe(value => value && (this.sLastFocusedField = 'bcc'));
|
||||
['to','cc','bcc'].forEach(name => {
|
||||
this[name].focused = ko.observable(false);
|
||||
this[name].focused.subscribe(value => value && (this.sLastFocusedField = name));
|
||||
});
|
||||
|
||||
this.attachments = koArrayWithDestroy();
|
||||
|
||||
|
|
@ -976,28 +973,22 @@ export class ComposePopupView extends AbstractViewPopup {
|
|||
if (arrayLength(downloads)) {
|
||||
Remote.request('MessageUploadAttachments',
|
||||
(iError, oData) => {
|
||||
if (!iError) {
|
||||
forEachObjectEntry(oData.Result, (tempName, id) => {
|
||||
const attachment = this.getAttachmentById(id);
|
||||
if (attachment) {
|
||||
attachment.tempName(tempName);
|
||||
attachment
|
||||
.waiting(false)
|
||||
.uploading(false)
|
||||
.complete(true);
|
||||
const result = oData?.Result;
|
||||
downloads.forEach((id, index) => {
|
||||
const attachment = this.getAttachmentById(id);
|
||||
if (attachment) {
|
||||
attachment
|
||||
.waiting(false)
|
||||
.uploading(false)
|
||||
.complete(true);
|
||||
if (iError || !result?.[index]) {
|
||||
attachment.error(getUploadErrorDescByCode(UploadErrorCode.NoFileUploaded));
|
||||
} else {
|
||||
attachment.tempName(result[index].TempName);
|
||||
attachment.type(result[index].MimeType);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.attachments.forEach(attachment => {
|
||||
if (attachment?.fromMessage) {
|
||||
attachment
|
||||
.waiting(false)
|
||||
.uploading(false)
|
||||
.complete(true)
|
||||
.error(getUploadErrorDescByCode(UploadErrorCode.NoFileUploaded));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
{
|
||||
Attachments: downloads
|
||||
|
|
@ -1013,9 +1004,8 @@ export class ComposePopupView extends AbstractViewPopup {
|
|||
setTimeout(() => {
|
||||
if (!this.to()) {
|
||||
this.to.focused(true);
|
||||
// TODO https://github.com/the-djmaze/snappymail/issues/501#issuecomment-1255906243
|
||||
// } else if (!this.subject()) {
|
||||
// this.subject.focus();
|
||||
} else if (!this.subject()) {
|
||||
this.viewModelDom.querySelector('input[name="subject"]').focus();
|
||||
} else {
|
||||
this.oEditor?.focus();
|
||||
}
|
||||
|
|
@ -1127,6 +1117,7 @@ export class ComposePopupView extends AbstractViewPopup {
|
|||
attachment.size(attachmentJson.Size ? pInt(attachmentJson.Size) : 0);
|
||||
attachment.tempName(attachmentJson.TempName ? attachmentJson.TempName : '');
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
|
|
@ -1241,6 +1218,7 @@ export class ComposePopupView extends AbstractViewPopup {
|
|||
if (message) {
|
||||
let reply = [ComposeType.Reply, ComposeType.ReplyAll].includes(type);
|
||||
if (reply || [ComposeType.Forward, ComposeType.Draft, ComposeType.EditAsNew].includes(type)) {
|
||||
// item instanceof AttachmentModel
|
||||
message.attachments.forEach(item => {
|
||||
if (!reply || item.isLinked()) {
|
||||
const attachment = new ComposeAttachmentModel(
|
||||
|
|
@ -1253,6 +1231,7 @@ export class ComposePopupView extends AbstractViewPopup {
|
|||
item.contentLocation
|
||||
);
|
||||
attachment.fromMessage = true;
|
||||
attachment.type(item.mimeType);
|
||||
this.addAttachment(attachment);
|
||||
}
|
||||
});
|
||||
|
|
@ -1409,6 +1388,20 @@ export class ComposePopupView extends AbstractViewPopup {
|
|||
|
||||
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
|
||||
identity = this.currentIdentity(),
|
||||
params = {
|
||||
|
|
@ -1426,7 +1419,7 @@ export class ComposePopupView extends AbstractViewPopup {
|
|||
InReplyTo: this.sInReplyTo,
|
||||
References: this.sReferences,
|
||||
MarkAsImportant: this.markAsImportant() ? 1 : 0,
|
||||
Attachments: this.prepareAttachmentsForSendOrSave(),
|
||||
Attachments: attachments,
|
||||
// Only used at send, not at save:
|
||||
Dsn: this.requestDsn() ? 1 : 0,
|
||||
ReadReceiptRequest: this.requestReadReceipt() ? 1 : 0
|
||||
|
|
|
|||
|
|
@ -12,24 +12,49 @@ import { DomainAdminStore } from 'Stores/Admin/Domain';
|
|||
|
||||
import { AskPopupView } from 'View/Popup/Ask';
|
||||
|
||||
const domainToParams = oDomain => ({
|
||||
Name: oDomain.name(),
|
||||
|
||||
IncHost: oDomain.imapHost(),
|
||||
IncPort: oDomain.imapPort(),
|
||||
IncSecure: oDomain.imapSecure(),
|
||||
|
||||
UseSieve: oDomain.useSieve() ? 1 : 0,
|
||||
SieveHost: oDomain.sieveHost(),
|
||||
SievePort: oDomain.sievePort(),
|
||||
SieveSecure: oDomain.sieveSecure(),
|
||||
|
||||
OutHost: oDomain.smtpHost(),
|
||||
OutPort: oDomain.smtpPort(),
|
||||
OutSecure: oDomain.smtpSecure(),
|
||||
OutAuth: oDomain.smtpAuth() ? 1 : 0,
|
||||
OutUsePhpMail: oDomain.smtpPhpMail() ? 1 : 0
|
||||
});
|
||||
const
|
||||
capitalize = string => string.charAt(0).toUpperCase() + string.slice(1),
|
||||
domainToParams = oDomain => ({
|
||||
Name: oDomain.name(),
|
||||
IMAP: {
|
||||
host: oDomain.imapHost(),
|
||||
port: oDomain.imapPort(),
|
||||
secure: pInt(oDomain.imapType()),
|
||||
shortLogin: !!oDomain.imapShortLogin(),
|
||||
ssl: {
|
||||
verify_peer: !!oDomain.imapSslVerify_peer(),
|
||||
verify_peer_name: !!oDomain.imapSslVerify_peer(),
|
||||
allow_self_signed: !!oDomain.imapSslAllow_self_signed()
|
||||
}
|
||||
},
|
||||
SMTP: {
|
||||
host: oDomain.smtpHost(),
|
||||
port: oDomain.smtpPort(),
|
||||
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 {
|
||||
constructor() {
|
||||
|
|
@ -82,14 +107,14 @@ export class DomainPopupView extends AbstractViewPopup {
|
|||
},
|
||||
|
||||
domainIsComputed: () => {
|
||||
const usePhpMail = this.smtpPhpMail(),
|
||||
useSieve = this.useSieve();
|
||||
const usePhpMail = this.smtpUsePhpMail(),
|
||||
sieveEnabled = this.sieveEnabled();
|
||||
|
||||
return (
|
||||
this.name() &&
|
||||
this.imapHost() &&
|
||||
this.imapPort() &&
|
||||
(useSieve ? this.sieveHost() && this.sievePort() : true) &&
|
||||
(sieveEnabled ? this.sieveHost() && this.sievePort() : true) &&
|
||||
((this.smtpHost() && this.smtpPort()) || usePhpMail)
|
||||
);
|
||||
},
|
||||
|
|
@ -113,7 +138,7 @@ export class DomainPopupView extends AbstractViewPopup {
|
|||
smtpHostFocus: value => value && this.imapHost() && !this.smtpHost()
|
||||
&& this.smtpHost(this.imapHost().replace(/imap/gi, 'smtp')),
|
||||
|
||||
imapSecure: value => {
|
||||
imapType: value => {
|
||||
if (this.enableSmartPorts()) {
|
||||
const port = pInt(this.imapPort());
|
||||
switch (pInt(value)) {
|
||||
|
|
@ -133,7 +158,7 @@ export class DomainPopupView extends AbstractViewPopup {
|
|||
}
|
||||
},
|
||||
|
||||
smtpSecure: value => {
|
||||
smtpType: value => {
|
||||
if (this.enableSmartPorts()) {
|
||||
const port = pInt(this.smtpPort());
|
||||
switch (pInt(value)) {
|
||||
|
|
@ -169,14 +194,7 @@ export class DomainPopupView extends AbstractViewPopup {
|
|||
Remote.request('AdminDomainSave',
|
||||
this.onDomainCreateOrSaveResponse.bind(this),
|
||||
Object.assign(domainToParams(this), {
|
||||
Create: this.edit() ? 0 : 1,
|
||||
|
||||
IncShortLogin: this.imapShortLogin() ? 1 : 0,
|
||||
|
||||
OutShortLogin: this.smtpShortLogin() ? 1 : 0,
|
||||
OutSetSender: this.smtpSetSender() ? 1 : 0,
|
||||
|
||||
WhiteList: this.whiteList()
|
||||
Create: this.edit() ? 0 : 1
|
||||
})
|
||||
);
|
||||
}
|
||||
|
|
@ -188,8 +206,10 @@ export class DomainPopupView extends AbstractViewPopup {
|
|||
if (credentials) {
|
||||
this.testing(true);
|
||||
const params = domainToParams(this);
|
||||
params.username = credentials.username;
|
||||
params.password = credentials.password;
|
||||
params.auth = {
|
||||
user: credentials.username,
|
||||
pass: credentials.password
|
||||
};
|
||||
Remote.request('AdminDomainTest',
|
||||
(iError, oData) => {
|
||||
this.testing(false);
|
||||
|
|
@ -250,7 +270,23 @@ export class DomainPopupView extends AbstractViewPopup {
|
|||
if (oDomain) {
|
||||
this.enableSmartPorts(false);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
@ -265,21 +301,27 @@ export class DomainPopupView extends AbstractViewPopup {
|
|||
|
||||
imapHost: '',
|
||||
imapPort: 143,
|
||||
imapSecure: 0,
|
||||
imapType: 0,
|
||||
imapShortLogin: false,
|
||||
// SSL
|
||||
imapSslVerify_peer: false,
|
||||
imapSslAllow_self_signed: false,
|
||||
|
||||
useSieve: false,
|
||||
sieveEnabled: false,
|
||||
sieveHost: '',
|
||||
sievePort: 4190,
|
||||
sieveSecure: 0,
|
||||
sieveType: 0,
|
||||
|
||||
smtpHost: '',
|
||||
smtpPort: 25,
|
||||
smtpSecure: 0,
|
||||
smtpType: 0,
|
||||
smtpShortLogin: false,
|
||||
smtpAuth: true,
|
||||
smtpUseAuth: true,
|
||||
smtpSetSender: false,
|
||||
smtpPhpMail: false,
|
||||
smtpUsePhpMail: false,
|
||||
// SSL
|
||||
smtpSslVerify_peer: false,
|
||||
smtpSslAllow_self_signed: false,
|
||||
|
||||
whiteList: '',
|
||||
aliasName: ''
|
||||
|
|
|
|||
|
|
@ -69,8 +69,8 @@ export class OpenPgpImportPopupView extends AbstractViewPopup {
|
|||
this.close();
|
||||
}
|
||||
|
||||
onShow() {
|
||||
this.key('');
|
||||
onShow(key) {
|
||||
this.key(key || '');
|
||||
this.keyError(false);
|
||||
this.keyErrorMessage('');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import ko from 'ko';
|
||||
|
||||
import { Scope } from 'Common/Enums';
|
||||
import { moveAction, addShortcut } from 'Common/Globals';
|
||||
import { addShortcut } from 'Common/Globals';
|
||||
import { mailBox, settings } from 'Common/Links';
|
||||
//import { setFolderHash } from 'Common/Cache';
|
||||
import { addComputablesTo } from 'External/ko';
|
||||
|
|
@ -15,7 +15,7 @@ import { MessagelistUserStore } from 'Stores/User/Messagelist';
|
|||
import { showScreenPopup } from 'Knoin/Knoin';
|
||||
import { AbstractViewLeft } from 'Knoin/AbstractViews';
|
||||
|
||||
import { showMessageComposer } from 'Common/UtilsUser';
|
||||
import { showMessageComposer, moveAction } from 'Common/UtilsUser';
|
||||
import { FolderCreatePopupView } from 'View/Popup/FolderCreate';
|
||||
import { ContactsPopupView } from 'View/Popup/Contacts';
|
||||
import { ComposePopupView } from 'View/Popup/Compose';
|
||||
|
|
|
|||
|
|
@ -5,17 +5,17 @@ import { Scope } from 'Common/Enums';
|
|||
|
||||
import { ComposeType, FolderType, MessageSetAction } from 'Common/EnumsUser';
|
||||
|
||||
import { leftPanelDisabled, moveAction,
|
||||
import { leftPanelDisabled, toggleLeftPanel,
|
||||
Settings, SettingsCapa,
|
||||
addEventsListeners,
|
||||
addShortcut, registerShortcut, formFieldFocused
|
||||
} 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 { isFullscreen, toggleFullscreen } from 'Common/Fullscreen';
|
||||
|
||||
import { mailBox } from 'Common/Links';
|
||||
import { mailBox, attachmentDownload } from 'Common/Links';
|
||||
import { Selector } from 'Common/Selector';
|
||||
|
||||
import { i18n } from 'Common/Translator';
|
||||
|
|
@ -83,6 +83,7 @@ export class MailMessageList extends AbstractViewRight {
|
|||
|
||||
this.isMobile = ThemeStore.isMobile;
|
||||
this.leftPanelDisabled = leftPanelDisabled;
|
||||
this.toggleLeftPanel = toggleLeftPanel;
|
||||
|
||||
this.popupVisibility = arePopupsVisible;
|
||||
|
||||
|
|
@ -258,6 +259,7 @@ export class MailMessageList extends AbstractViewRight {
|
|||
).throttle(50));
|
||||
|
||||
decorateKoCommands(this, {
|
||||
downloadCommand: canBeMovedHelper,
|
||||
forwardCommand: canBeMovedHelper,
|
||||
deleteWithoutMoveCommand: 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() {
|
||||
SettingsCapa('DangerousActions')
|
||||
&& moveMessagesToFolderType(FolderType.Trash, true);
|
||||
|
|
|
|||
|
|
@ -15,7 +15,6 @@ import {
|
|||
elementById,
|
||||
leftPanelDisabled,
|
||||
keyScopeReal,
|
||||
moveAction,
|
||||
Settings,
|
||||
SettingsCapa,
|
||||
fireEvent,
|
||||
|
|
@ -24,7 +23,7 @@ import {
|
|||
} from 'Common/Globals';
|
||||
|
||||
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 { SMAudio } from 'Common/Audio';
|
||||
|
|
@ -55,10 +54,17 @@ import { MimeToMessage } from 'Mime/Utils';
|
|||
|
||||
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
|
||||
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 {
|
||||
constructor() {
|
||||
|
|
@ -92,6 +98,7 @@ export class MailMessageView extends AbstractViewRight {
|
|||
}, this.messageVisibility);
|
||||
|
||||
this.msgDefaultAction = SettingsUserStore.msgDefaultAction;
|
||||
this.simpleAttachmentsList = SettingsUserStore.simpleAttachmentsList;
|
||||
|
||||
addObservablesTo(this, {
|
||||
showAttachmentControls: !!Local.get(ClientSideKeyNameMessageAttachmentControls),
|
||||
|
|
@ -154,9 +161,9 @@ export class MailMessageView extends AbstractViewRight {
|
|||
case 'none':
|
||||
return '';
|
||||
case 'pass':
|
||||
return 'icon-ok iconcolor-green';
|
||||
return 'icon-ok iconcolor-green'; // ✔️
|
||||
default:
|
||||
return 'icon-warning-alt iconcolor-red';
|
||||
return 'icon-cross iconcolor-red'; // ✖ ❌
|
||||
}
|
||||
},
|
||||
|
||||
|
|
@ -182,7 +189,8 @@ export class MailMessageView extends AbstractViewRight {
|
|||
this.scrollMessageToTop();
|
||||
}
|
||||
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.viewToShort(message.toToLine(true, true));
|
||||
} else {
|
||||
|
|
@ -257,8 +265,9 @@ export class MailMessageView extends AbstractViewRight {
|
|||
return;
|
||||
}
|
||||
|
||||
if (eqs(event, '.attachmentsPlace .attachmentIconParent')) {
|
||||
if (eqs(event, '.attachmentsPlace .showPreview')) {
|
||||
event.stopPropagation();
|
||||
return;
|
||||
}
|
||||
|
||||
el = eqs(event, '.attachmentsPlace .showPreplay');
|
||||
|
|
@ -279,26 +288,28 @@ export class MailMessageView extends AbstractViewRight {
|
|||
// no default
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
el = eqs(event, '.attachmentsPlace .attachmentItem .attachmentNameParent');
|
||||
el = eqs(event, '.attachmentItem');
|
||||
if (el) {
|
||||
const attachment = ko.dataFor(el);
|
||||
if (attachment?.linkDownload()) {
|
||||
if ('message/rfc822' == attachment.mimeType) {
|
||||
const attachment = ko.dataFor(el), url = attachment?.linkDownload();
|
||||
if (url) {
|
||||
if ('application/pgp-keys' == attachment.mimeType
|
||||
&& (OpenPGPUserStore.isSupported() || GnuPGUserStore.isSupported())) {
|
||||
fetchRaw(url).then(text =>
|
||||
showScreenPopup(OpenPgpImportPopupView, [text])
|
||||
);
|
||||
} else if ('message/rfc822' == attachment.mimeType) {
|
||||
// TODO
|
||||
rl.fetch(attachment.linkDownload()).then(response => {
|
||||
if (response.ok) {
|
||||
response.text().then(text => {
|
||||
const oMessage = new MessageModel();
|
||||
MimeToMessage(text, oMessage);
|
||||
// cleanHTML
|
||||
oMessage.viewPopupMessage();
|
||||
});
|
||||
}
|
||||
fetchRaw(url).then(text => {
|
||||
const oMessage = new MessageModel();
|
||||
MimeToMessage(text, oMessage);
|
||||
// cleanHTML
|
||||
oMessage.viewPopupMessage();
|
||||
});
|
||||
} else {
|
||||
download(attachment.linkDownload(), attachment.fileName);
|
||||
download(url, attachment.fileName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { mailbox } from 'Common/Links';
|
||||
import { getFolderInboxName } from 'Common/Cache';
|
||||
import { leftPanelDisabled } from 'Common/Globals';
|
||||
import { leftPanelDisabled, toggleLeftPanel } from 'Common/Globals';
|
||||
|
||||
import { MessageUserStore } from 'Stores/User/Message';
|
||||
import { ThemeStore } from 'Stores/Theme';
|
||||
|
|
@ -13,6 +13,7 @@ export class SettingsPaneUserView extends AbstractViewRight {
|
|||
|
||||
this.isMobile = ThemeStore.isMobile;
|
||||
this.leftPanelDisabled = leftPanelDisabled;
|
||||
this.toggleLeftPanel = toggleLeftPanel;
|
||||
}
|
||||
|
||||
onShow() {
|
||||
|
|
|
|||
15
dev/boot.js
|
|
@ -1,13 +1,11 @@
|
|||
(doc => {
|
||||
|
||||
navigator.cookieEnabled || doc.location.replace('./?/NoCookie');
|
||||
|
||||
const
|
||||
eId = id => doc.getElementById('rl-'+id),
|
||||
app = eId('app'),
|
||||
admin = app && '1' == app.dataset.admin,
|
||||
layout = doc.cookie.match(/(^|;) ?rllayout=([^;]+)/) || '',
|
||||
badBrowser = () => doc.location.replace('./?/BadBrowser'),
|
||||
redirect = path => doc.location.replace('./?/'+path),
|
||||
|
||||
showError = msg => {
|
||||
let div = eId('loading-error');
|
||||
|
|
@ -23,14 +21,15 @@ const
|
|||
return new Promise((resolve, reject) => {
|
||||
const script = doc.createElement('script');
|
||||
script.onload = () => resolve();
|
||||
script.onerror = () => reject(new Error('Failed loading ' + src));
|
||||
script.onerror = () => reject('Failed loading ' + src);
|
||||
script.src = src;
|
||||
// script.async = true;
|
||||
doc.head.append(script);
|
||||
});
|
||||
};
|
||||
|
||||
[].flat || badBrowser();
|
||||
navigator.cookieEnabled || redirect('NoCookie');
|
||||
[].flat || redirect('BadBrowser');
|
||||
|
||||
let RL_APP_DATA = {};
|
||||
|
||||
|
|
@ -45,13 +44,13 @@ window.rl = {
|
|||
app: name => RL_APP_DATA.System[name]
|
||||
},
|
||||
|
||||
setWindowTitle: title =>
|
||||
doc.title = RL_APP_DATA.Title ? (title ? title + ' - ' : '') + RL_APP_DATA.Title : (title ? '' + title : ''),
|
||||
setTitle: title =>
|
||||
doc.title = (title || '') + (RL_APP_DATA.Title ? (title ? ' - ' : '') + RL_APP_DATA.Title : ''),
|
||||
|
||||
initData: appData => {
|
||||
RL_APP_DATA = appData;
|
||||
const url = appData.StaticLibsJs,
|
||||
cb = () => rl.app ? rl.app.bootstart() : badBrowser();
|
||||
cb = () => rl.app.bootstart();
|
||||
loadScript(url)
|
||||
.then(() => loadScript(url.replace('/libs.', `/${admin?'admin':'app'}.`)))
|
||||
.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 { 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 => {
|
||||
|
||||
|
|
@ -57,7 +45,18 @@ export default App => {
|
|||
if (postData) {
|
||||
init.method = 'POST';
|
||||
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');
|
||||
init.body = JSON.stringify(postData);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@ const
|
|||
setScope: value => {
|
||||
_scope = value || 'all';
|
||||
scope = _scopes[_scope] || {};
|
||||
console.log('Shortcuts scope set to: ' + _scope);
|
||||
},
|
||||
getScope: () => _scope,
|
||||
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.
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ RUN mkdir -p /app/code
|
|||
WORKDIR /app/code
|
||||
|
||||
# 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 && \
|
||||
unzip /tmp/snappymail.zip -d /app/code && \
|
||||
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>
|
||||
<name>SnappyMail</name>
|
||||
<summary>SnappyMail Webmail</summary>
|
||||
<version>2.20.6</version>
|
||||
<version>2.22.2</version>
|
||||
<licence>agpl</licence>
|
||||
<author>SnappyMail, RainLoop Team, Nextgen-Networks, Tab Fitts, Nathan Kinkade, Pierre-Alain Bandinelli</author>
|
||||
<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%;
|
||||
}
|
||||
|
||||
#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 {
|
||||
min-width: 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 {
|
||||
box-sizing: border-box;
|
||||
font-size: 13px;
|
||||
height: 100%;
|
||||
line-height: 16px;
|
||||
|
|
@ -105,6 +78,15 @@ body > header ul {
|
|||
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
|
||||
* 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 */
|
||||
protected $evalScriptAllowed = true;
|
||||
/** @var bool Whether strict-dynamic should be set */
|
||||
protected $strictDynamicAllowed = true; // NC24+
|
||||
// protected $strictDynamicAllowed = true; // NC24+
|
||||
/** @var bool Whether inline CSS is allowed */
|
||||
protected $inlineStyleAllowed = true;
|
||||
|
||||
|
|
@ -21,9 +21,11 @@ class ContentSecurityPolicy extends \OCP\AppFramework\Http\ContentSecurityPolicy
|
|||
$CSP->script
|
||||
));
|
||||
$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,
|
||||
|
|
@ -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';
|
||||
$sLanguage = $oActions->GetLanguage(false);
|
||||
|
||||
$cspManager = \OC::$server->getContentSecurityPolicyNonceManager();
|
||||
$sNonce = $cspManager->getNonce() ?: \SnappyMail\UUID::generate();
|
||||
$csp = new ContentSecurityPolicy();
|
||||
if (\method_exists($cspManager, 'browserSupportsCspV3') && !$cspManager->browserSupportsCspV3()) {
|
||||
$csp->addAllowedScriptDomain("'nonce-{$sNonce}'");
|
||||
}
|
||||
$sNonce = $csp->getSnappyMailNonce();
|
||||
|
||||
$params = [
|
||||
'Admin' => $bAdmin ? 1 : 0,
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ return "SnappyMail Webmail is a browser-based multilingual IMAP client with an a
|
|||
# script_snappymail_versions()
|
||||
sub script_snappymail_versions
|
||||
{
|
||||
return ( "2.20.6" );
|
||||
return ( "2.22.2" );
|
||||
}
|
||||
|
||||
sub script_snappymail_version_desc
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
"title": "SnappyMail",
|
||||
"description": "Simple, modern & fast web-based email client",
|
||||
"private": true,
|
||||
"version": "2.20.6",
|
||||
"version": "2.22.2",
|
||||
"homepage": "https://snappymail.eu",
|
||||
"author": {
|
||||
"name": "DJ Maze",
|
||||
|
|
|
|||
|
|
@ -3,13 +3,84 @@ PHP
|
|||
class Plugin extends \RainLoop\Plugins\AbstractPlugin
|
||||
{
|
||||
public function __construct();
|
||||
public function Name() : string;
|
||||
public function Description() : string;
|
||||
public function UseLangs(?bool $bLangs = null) : bool;
|
||||
public function Supported() : string;
|
||||
public function Init() : void;
|
||||
public function FilterAppDataPluginSection(bool $bAdmin, bool $bAuth, array &$aConfig) : void;
|
||||
protected function configMapping() : array;
|
||||
|
||||
/** Returns static::NAME */
|
||||
public function Name(): string;
|
||||
|
||||
/** Returns /README file contents or static::DESCRIPTION */
|
||||
public function Description(): string;
|
||||
|
||||
/** When $bLangs is boolean it sets the value, else returns current value */
|
||||
public function UseLangs(?bool $bLangs = null): bool;
|
||||
|
||||
/** When true the result is empty string, else the error message */
|
||||
public function Supported(): string;
|
||||
|
||||
/** Initialize settings */
|
||||
public function Init(): 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;
|
||||
|
||||
/** 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:
|
||||
\RainLoop\Model\Account $oAccount
|
||||
\MailSo\Imap\ImapClient $oImapClient
|
||||
array &$aCredentials
|
||||
\MailSo\Imap\Settings $oSettings
|
||||
|
||||
### imap.after-connect
|
||||
params:
|
||||
\RainLoop\Model\Account $oAccount
|
||||
\MailSo\Imap\ImapClient $oImapClient
|
||||
array $aCredentials
|
||||
\MailSo\Imap\Settings $oSettings
|
||||
|
||||
### imap.before-login
|
||||
params:
|
||||
\RainLoop\Model\Account $oAccount
|
||||
\MailSo\Imap\ImapClient $oImapClient
|
||||
array &$aCredentials
|
||||
\MailSo\Imap\Settings $oSettings
|
||||
|
||||
### imap.after-login
|
||||
params:
|
||||
\RainLoop\Model\Account $oAccount
|
||||
\MailSo\Imap\ImapClient $oImapClient
|
||||
bool $bSuccess
|
||||
array $aCredentials
|
||||
\MailSo\Imap\Settings $oSettings
|
||||
|
||||
## Sieve
|
||||
|
||||
### sieve.before-connect
|
||||
params:
|
||||
\RainLoop\Model\Account $oAccount
|
||||
\MailSo\Sieve\ManageSieveClient $oSieveClient
|
||||
array &$aCredentials
|
||||
\MailSo\Sieve\SieveClient $oSieveClient
|
||||
\MailSo\Sieve\Settings $oSettings
|
||||
|
||||
### sieve.after-connect
|
||||
params:
|
||||
\RainLoop\Model\Account $oAccount
|
||||
\MailSo\Sieve\ManageSieveClient $oSieveClient
|
||||
array $aCredentials
|
||||
\MailSo\Sieve\SieveClient $oSieveClient
|
||||
\MailSo\Sieve\Settings $oSettings
|
||||
|
||||
### sieve.before-login
|
||||
params:
|
||||
\RainLoop\Model\Account $oAccount
|
||||
\MailSo\Sieve\ManageSieveClient $oSieveClient
|
||||
array &$aCredentials
|
||||
\MailSo\Sieve\SieveClient $oSieveClient
|
||||
\MailSo\Sieve\Settings $oSettings
|
||||
|
||||
### sieve.after-login
|
||||
params:
|
||||
\RainLoop\Model\Account $oAccount
|
||||
\MailSo\Sieve\ManageSieveClient $oSieveClient
|
||||
\MailSo\Sieve\SieveClient $oSieveClient
|
||||
bool $bSuccess
|
||||
array $aCredentials
|
||||
\MailSo\Sieve\Settings $oSettings
|
||||
|
||||
## SMTP
|
||||
|
||||
|
|
@ -126,26 +197,26 @@ $Plugin->addHook('hook.name', 'functionName');
|
|||
params:
|
||||
\RainLoop\Model\Account $oAccount
|
||||
\MailSo\Smtp\SmtpClient $oSmtpClient
|
||||
array &$aCredentials
|
||||
\MailSo\Smtp\Settings $oSettings
|
||||
|
||||
### smtp.after-connect
|
||||
params:
|
||||
\RainLoop\Model\Account $oAccount
|
||||
\MailSo\Smtp\SmtpClient $oSmtpClient
|
||||
array $aCredentials
|
||||
\MailSo\Smtp\Settings $oSettings
|
||||
|
||||
### smtp.before-login
|
||||
params:
|
||||
\RainLoop\Model\Account $oAccount
|
||||
\MailSo\Smtp\SmtpClient $oSmtpClient
|
||||
array &$aCredentials
|
||||
\MailSo\Smtp\Settings $oSettings
|
||||
|
||||
### smtp.after-login
|
||||
params:
|
||||
\RainLoop\Model\Account $oAccount
|
||||
\MailSo\Smtp\SmtpClient $oSmtpClient
|
||||
bool $bSuccess
|
||||
array $aCredentials
|
||||
\MailSo\Smtp\Settings $oSettings
|
||||
|
||||
## Folders
|
||||
|
||||
|
|
|
|||
|
|
@ -15,8 +15,9 @@ class AutoDomainGrabPlugin extends \RainLoop\Plugins\AbstractPlugin
|
|||
{
|
||||
const
|
||||
NAME = 'Auto Domain Selection',
|
||||
VERSION = '2.8',
|
||||
REQUIRED = '2.8.0',
|
||||
VERSION = '2.9',
|
||||
RELEASE = '2022-11-11',
|
||||
REQUIRED = '2.21.0',
|
||||
CATEGORY = 'General',
|
||||
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.
|
||||
*/
|
||||
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
|
||||
if (!empty($aImapCredentials['Host']) && 'auto' === $aImapCredentials['Host'])
|
||||
if ('auto' === $oSettings->host)
|
||||
{
|
||||
$domain = \substr(\strrchr($oAccount->Email(), '@'), 1);
|
||||
$mxhosts = array();
|
||||
if (\getmxrr($domain, $mxhosts) && $mxhosts)
|
||||
{
|
||||
$aImapCredentials['Host'] = $mxhosts[0];
|
||||
$oSettings->host = $mxhosts[0];
|
||||
}
|
||||
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.
|
||||
*/
|
||||
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
|
||||
if (!empty($aSmtpCredentials['Host']) && 'auto' === $aSmtpCredentials['Host'])
|
||||
if ('auto' === $oSettings->host)
|
||||
{
|
||||
$domain = \substr(\strrchr($oAccount->Email(), '@'), 1);
|
||||
$mxhosts = array();
|
||||
if (\getmxrr($domain, $mxhosts) && $mxhosts)
|
||||
{
|
||||
$aSmtpCredentials['Host'] = $mxhosts[0];
|
||||
$oSettings->host = $mxhosts[0];
|
||||
}
|
||||
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'])) {
|
||||
$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)) {
|
||||
\file_put_contents("{$sDataPath}/settings",'{"RemoveColors":true,"ListInlineAttachments":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/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/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;
|
||||
}
|
||||
|
||||
public function GetContactByEmail(string $sEmail) : ?Contact;
|
||||
{
|
||||
// TODO
|
||||
return null;
|
||||
}
|
||||
|
||||
public function GetContactByID($mID, bool $bIsStrID = false) : ?Contact
|
||||
{
|
||||
if ($bIsStrID) {
|
||||
|
|
|
|||
|
|
@ -4,11 +4,11 @@ class KolabPlugin extends \RainLoop\Plugins\AbstractPlugin
|
|||
{
|
||||
const
|
||||
NAME = 'Kolab',
|
||||
VERSION = '2.0',
|
||||
RELEASE = '2022-09-06',
|
||||
VERSION = '2.1',
|
||||
RELEASE = '2022-11-23',
|
||||
CATEGORY = 'Contacts',
|
||||
DESCRIPTION = 'Use an Address Book of Kolab.',
|
||||
REQUIRED = '2.18.0';
|
||||
REQUIRED = '2.22.0';
|
||||
|
||||
public function Init() : void
|
||||
{
|
||||
|
|
|
|||