Merge branch 'the-djmaze:master' into master

This commit is contained in:
cm-schl 2022-11-25 10:44:00 +01:00 committed by GitHub
commit fa4bb8ae18
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
275 changed files with 5872 additions and 2371 deletions

View file

@ -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"

View file

@ -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

View file

@ -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
View 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";

View file

@ -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);
}
}

View file

@ -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'));

View file

@ -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',

View file

@ -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));

View file

@ -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

View file

@ -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));

View file

@ -7,7 +7,6 @@ export class CheckboxComponent {
: ko.observable(undefined === params.enable || !!params.enable);
this.label = params.label;
this.inline = params.inline;
}
click() {

View file

@ -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();
}
}
}

View file

@ -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();
}

View file

@ -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;
}
}

View file

@ -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;

View file

@ -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
};
}

View file

@ -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())

View file

@ -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;
}

View file

@ -8,6 +8,6 @@ export class LoginAdminScreen extends AbstractScreen {
}
onShow() {
rl.setWindowTitle();
rl.setTitle();
}
}

View file

@ -37,6 +37,6 @@ export class SettingsAdminScreen extends AbstractSettingsScreen {
}
onShow() {
rl.setWindowTitle();
rl.setTitle();
}
}

View file

@ -8,6 +8,6 @@ export class LoginUserScreen extends AbstractScreen {
}
onShow() {
rl.setWindowTitle();
rl.setTitle();
}
}

View file

@ -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 + ' - '
: ''

View file

@ -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);
}
}

View file

@ -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);
}

View file

@ -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) => () => {

View file

@ -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;
});

View file

@ -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);
}
};

View file

@ -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;
}
}

View file

@ -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 = () => {

View file

@ -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'));
}
};

View file

@ -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 {

View file

@ -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 {

View file

@ -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;
}

View file

@ -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;

View file

@ -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;

View file

@ -80,7 +80,6 @@
> div > div {
position: absolute;
box-sizing: border-box;
margin-top: 1px;
animation: checkmark-to-box 200ms ease-out forwards;

View file

@ -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%;
}
}

View file

@ -112,7 +112,7 @@
}
.buttonLogin {
margin: 0;
width: 100%;
}
.alert {

View file

@ -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;

View file

@ -20,7 +20,6 @@
.settings-save-trigger {
display: inline-block;
height: 1em;
line-height: 1em;
margin-left: 0.5em !important;

View file

@ -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;
}
}

View file

@ -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%;
}
}

View file

@ -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%;

View file

@ -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;
}
}

View file

@ -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;

View file

@ -17,7 +17,6 @@ html.rl-no-preview-pane {
}
#V-MailMessageList .btn-toolbar {
height: 30px;
padding: 10px 1px;
white-space: nowrap;
}

View file

@ -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;
}
}

View file

@ -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);

View file

@ -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);
}

View file

@ -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;

View file

@ -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;

View file

@ -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() {

View file

@ -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

View file

@ -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: ''

View file

@ -69,8 +69,8 @@ export class OpenPgpImportPopupView extends AbstractViewPopup {
this.close();
}
onShow() {
this.key('');
onShow(key) {
this.key(key || '');
this.keyError(false);
this.keyErrorMessage('');
}

View file

@ -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';

View file

@ -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);

View file

@ -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);
}
}
}

View file

@ -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() {

View file

@ -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
View file

@ -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);
}

View file

@ -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'

View file

@ -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.

View file

@ -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 && \

View file

@ -0,0 +1,3 @@
url: "/3rdparty/snappymail/?RemoteAutoLogin"
displayname: "SnappyMail"
icon: "/3rdparty/snappymail/snappymail/v/2.22.1/static/logo-text.png"

View file

@ -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.**

View file

@ -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

View file

@ -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;
}
}

View file

@ -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,

View file

@ -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

View file

@ -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",

View file

@ -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

View file

@ -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
View 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
View 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);

Binary file not shown.

After

Width:  |  Height:  |  Size: 665 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 995 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 382 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

196
plugins/avatars/index.php Normal file
View 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
View 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;
}

View file

@ -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'] = '********';
}
}

View file

@ -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"));
}

View file

@ -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) {

View file

@ -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
{

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