mirror of
https://github.com/the-djmaze/snappymail.git
synced 2026-09-08 17:07:03 +03:00
Merge branch 'master' into php81
# Conflicts: # snappymail/v/0.0.0/app/libraries/RainLoop/Actions.php # snappymail/v/0.0.0/app/libraries/RainLoop/Config/Application.php # snappymail/v/0.0.0/app/libraries/RainLoop/Enumerations/SignMeType.php
This commit is contained in:
commit
0661076905
319 changed files with 19562 additions and 7439 deletions
|
|
@ -35,7 +35,9 @@ module.exports = {
|
||||||
// vendors/bootstrap/bootstrap.native.js
|
// vendors/bootstrap/bootstrap.native.js
|
||||||
'BSN': "readonly",
|
'BSN': "readonly",
|
||||||
// Mailvelope
|
// Mailvelope
|
||||||
'mailvelope': "readonly"
|
'mailvelope': "readonly",
|
||||||
|
// Punycode
|
||||||
|
'IDN': "readonly"
|
||||||
},
|
},
|
||||||
// http://eslint.org/docs/rules/
|
// http://eslint.org/docs/rules/
|
||||||
rules: {
|
rules: {
|
||||||
|
|
|
||||||
76
CHANGELOG.md
76
CHANGELOG.md
|
|
@ -1,4 +1,78 @@
|
||||||
## 2.35.2 – 2024-02-26
|
## 2.35.4 – 2024-03-16
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- \SnappyMail\IDN::toAscii()
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- OpenPGP.js to v5.11.1
|
||||||
|
- punycode.js lowercase domain names
|
||||||
|
- Nextcloud changed stored password handling
|
||||||
|
- application.ini `login_lowercase` removed and now configurable per domain JSON `lowerLogin`
|
||||||
|
- Update Portuguese by @ner00
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Raise JS TypeEroor "toLowerCase" after update
|
||||||
|
[#1491](https://github.com/the-djmaze/snappymail/issues/1491)
|
||||||
|
- Call to undefined function shell_exec
|
||||||
|
[#1496](https://github.com/the-djmaze/snappymail/issues/1496)
|
||||||
|
- Download attachments as ZIP doesn't work for PGP encrypted mail
|
||||||
|
[#1499](https://github.com/the-djmaze/snappymail/issues/1499)
|
||||||
|
- Importing or downloading a PGP public key attachment from a PGP encrypted message doesn't work
|
||||||
|
[#1500](https://github.com/the-djmaze/snappymail/issues/1500)
|
||||||
|
- VCard PHP Notice: Undefined index: ENCODING
|
||||||
|
|
||||||
|
|
||||||
|
## 2.35.3 – 2024-03-12
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- GnuPG can be disabled
|
||||||
|
- Missing strings for localization inside identity popup (Cryptography > S/MIME)
|
||||||
|
[#1458](https://github.com/the-djmaze/snappymail/issues/1458)
|
||||||
|
- Automatically verify PGP and S/MIME signed messages
|
||||||
|
- TNEFDecoder for
|
||||||
|
[#1012](https://github.com/the-djmaze/snappymail/discussions/1012)
|
||||||
|
- RTF to HTML converter for
|
||||||
|
[#1012](https://github.com/the-djmaze/snappymail/discussions/1012)
|
||||||
|
- Polyfill for PHP ctype
|
||||||
|
[#1250](https://github.com/the-djmaze/snappymail/issues/1250)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- `new Error()` to `Error()`
|
||||||
|
- Reduce KnockoutJS footprint by removing unused code
|
||||||
|
- CSS reposition rainloopErrorTip location
|
||||||
|
- Improved error handling on PGP and S/MIME decrypt
|
||||||
|
- Improved OpenPGP.js import keys
|
||||||
|
- Use Identity S/MIME key and certificate from server instead of POST
|
||||||
|
- application.ini `[webmail]language_admin` to `[admin_panel]language`
|
||||||
|
- application.ini `[security]admin_panel_host` to `[admin_panel]host`
|
||||||
|
- application.ini `[security]admin_panel_key` to `[admin_panel]key`
|
||||||
|
- Drop deprecated Domain::SetConfig()
|
||||||
|
- Internationalized domain names are now handled as punycode
|
||||||
|
- Cacher->Get() can now return NULL
|
||||||
|
- Update French by @hguilbert
|
||||||
|
- Update Polish by @tinola
|
||||||
|
- Update Portuguese by @ner00
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Handling of Internationalized Domain Names in several areas
|
||||||
|
- Decrypt error message
|
||||||
|
- Stalwart ManageSieve Error 352 when getting Filters
|
||||||
|
[#1455](https://github.com/the-djmaze/snappymail/issues/1455)
|
||||||
|
- Nextcloud V25+ theme slightly broken
|
||||||
|
[#1463](https://github.com/the-djmaze/snappymail/issues/1463)
|
||||||
|
- PGP decryption fails with "Not armored text"
|
||||||
|
[#1462](https://github.com/the-djmaze/snappymail/issues/1462)
|
||||||
|
- AUTH_BASIC falling through as AUTH_BEARER; change AUTH_BEARER to a different value
|
||||||
|
[#1461](https://github.com/the-djmaze/snappymail/issues/1461)
|
||||||
|
- SetPassword expects \SnappyMail\SensitiveString
|
||||||
|
- Crash on importing corrupt OpenPGP keys
|
||||||
|
- Crash on old browsers instead of showing error
|
||||||
|
- Ignore popups on logoutReload()
|
||||||
|
- Custom SASLMechanisms fail in IMAP when the connection is secure
|
||||||
|
[#1484](https://github.com/the-djmaze/snappymail/pull/1484)
|
||||||
|
|
||||||
|
|
||||||
|
## 2.35.2 – 2024-02-27
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
- GnuPG error handling
|
- GnuPG error handling
|
||||||
|
|
|
||||||
36
README.md
36
README.md
|
|
@ -140,26 +140,26 @@ RainLoop 1.17 vs SnappyMail
|
||||||
|
|
||||||
|js/* |RainLoop |Snappy |
|
|js/* |RainLoop |Snappy |
|
||||||
|--------------- |--------: |--------: |
|
|--------------- |--------: |--------: |
|
||||||
|admin.js |2.170.153 | 82.445 |
|
|admin.js |2.170.153 | 82.845 |
|
||||||
|app.js |4.207.787 | 429.565 |
|
|app.js |4.207.787 | 430.201 |
|
||||||
|boot.js | 868.735 | 4.142 |
|
|boot.js | 868.735 | 4.147 |
|
||||||
|libs.js | 658.812 | 192.786 |
|
|libs.js | 658.812 | 201.911 |
|
||||||
|sieve.js | 0 | 84.707 |
|
|sieve.js | 0 | 84.703 |
|
||||||
|polyfills.js | 334.608 | 0 |
|
|polyfills.js | 334.608 | 0 |
|
||||||
|serviceworker.js | 0 | 285 |
|
|serviceworker.js | 0 | 285 |
|
||||||
|TOTAL |8.240.095 | 793.930 |
|
|TOTAL |8.240.095 | 804.092 |
|
||||||
|
|
||||||
|js/min/* |RainLoop |Snappy |RL gzip |SM gzip |RL brotli |SM brotli |
|
|js/min/* |RainLoop |Snappy |RL gzip |SM gzip |RL brotli |SM brotli |
|
||||||
|--------------- |--------: |--------: |------: |------: |--------: |--------: |
|
|--------------- |--------: |--------: |------: |------: |--------: |--------: |
|
||||||
|admin.min.js | 256.831 | 40.573 | 73.606 | 13.585 | 60.877 | 12.188 |
|
|admin.min.js | 256.831 | 40.733 | 73.606 | 13.649 | 60.877 | 12.228 |
|
||||||
|app.min.js | 515.367 | 195.013 |139.456 | 66.135 |110.485 | 56.623 |
|
|app.min.js | 515.367 | 195.260 |139.456 | 66.275 |110.485 | 56.682 |
|
||||||
|boot.min.js | 84.659 | 2.084 | 26.998 | 1.202 | 23.643 | 1.003 |
|
|boot.min.js | 84.659 | 2.087 | 26.998 | 1.204 | 23.643 | 1.002 |
|
||||||
|libs.min.js | 584.772 | 92.746 |180.901 | 34.452 |155.182 | 30.890 |
|
|libs.min.js | 584.772 | 92.482 |180.901 | 34.729 |155.182 | 31.017 |
|
||||||
|sieve.min.js | 0 | 41.164 | 0 | 10.365 | 0 | 9.359 |
|
|sieve.min.js | 0 | 41.164 | 0 | 10.365 | 0 | 9.359 |
|
||||||
|polyfills.min.js | 32.837 | 0 | 11.406 | 0 | 10.175 | 0 |
|
|polyfills.min.js | 32.837 | 0 | 11.406 | 0 | 10.175 | 0 |
|
||||||
|TOTAL user |1.217.635 | 289.843 |358.761 |101.789 |299.485 | 88.516 |
|
|TOTAL user |1.217.635 | 289.829 |358.761 |102.208 |299.485 | 88.701 |
|
||||||
|TOTAL user+sieve |1.217.635 | 331.007 |358.761 |112.154 |299.485 | 97.875 |
|
|TOTAL user+sieve |1.217.635 | 330.993 |358.761 |112.573 |299.485 | 98.060 |
|
||||||
|TOTAL admin | 959.099 | 135.403 |292.911 | 49.239 |249.877 | 44.081 |
|
|TOTAL admin | 959.099 | 135.302 |292.911 | 49.582 |249.877 | 44.247 |
|
||||||
|
|
||||||
For a user it is around 69% smaller and faster than traditional RainLoop.
|
For a user it is around 69% smaller and faster than traditional RainLoop.
|
||||||
|
|
||||||
|
|
@ -188,12 +188,12 @@ For a user it is around 69% smaller and faster than traditional RainLoop.
|
||||||
|
|
||||||
|css/* |RainLoop |Snappy |RL gzip |SM gzip |SM brotli |
|
|css/* |RainLoop |Snappy |RL gzip |SM gzip |SM brotli |
|
||||||
|------------ |-------: |------: |------: |------: |--------: |
|
|------------ |-------: |------: |------: |------: |--------: |
|
||||||
|app.css | 340.331 | 84.472 | 46.946 | 17.622 | 15.112 |
|
|app.css | 340.331 | 84.531 | 46.946 | 17.627 | 15.111 |
|
||||||
|app.min.css | 274.947 | 67.857 | 39.647 | 15.537 | 13.549 |
|
|app.min.css | 274.947 | 67.906 | 39.647 | 15.544 | 13.567 |
|
||||||
|boot.css | | 1.326 | | 664 | 545 |
|
|boot.css | | 1.326 | | 664 | 545 |
|
||||||
|boot.min.css | | 1.071 | | 590 | 474 |
|
|boot.min.css | | 1.071 | | 590 | 474 |
|
||||||
|admin.css | | 30.576 | | 7.013 | 6.096 |
|
|admin.css | | 30.573 | | 7.011 | 6.101 |
|
||||||
|admin.min.css | | 24.692 | | 6.336 | 5.579 |
|
|admin.min.css | | 24.689 | | 6.334 | 5.575 |
|
||||||
|
|
||||||
### PGP
|
### PGP
|
||||||
RainLoop uses the old OpenPGP.js v2
|
RainLoop uses the old OpenPGP.js v2
|
||||||
|
|
@ -207,7 +207,7 @@ See https://github.com/the-djmaze/openpgpjs for development
|
||||||
|
|
||||||
|OpenPGP |RainLoop |Snappy |RL gzip |SM gzip |RL brotli |SM brotli |
|
|OpenPGP |RainLoop |Snappy |RL gzip |SM gzip |RL brotli |SM brotli |
|
||||||
|--------------- |--------: |--------: |------: |-------: |--------: |--------: |
|
|--------------- |--------: |--------: |------: |-------: |--------: |--------: |
|
||||||
|openpgp.min.js | 330.742 | 540.792 |102.388 | 167.971 | 84.241 | 138.010 |
|
|openpgp.min.js | 330.742 | 546.309 |102.388 | 169.249 | 84.241 | 138.751 |
|
||||||
|openpgp.worker | 1.499 | | 824 | | 695 | |
|
|openpgp.worker | 1.499 | | 824 | | 695 | |
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,8 @@ import ko from 'ko';
|
||||||
import { logoutLink } from 'Common/Links';
|
import { logoutLink } from 'Common/Links';
|
||||||
import { i18nToNodes, initOnStartOrLangChange } from 'Common/Translator';
|
import { i18nToNodes, initOnStartOrLangChange } from 'Common/Translator';
|
||||||
|
|
||||||
|
import { arePopupsVisible } from 'Knoin/Knoin';
|
||||||
|
|
||||||
import { LanguageStore } from 'Stores/Language';
|
import { LanguageStore } from 'Stores/Language';
|
||||||
import { initThemes } from 'Stores/Theme';
|
import { initThemes } from 'Stores/Theme';
|
||||||
|
|
||||||
|
|
@ -18,6 +20,7 @@ export class AbstractApp {
|
||||||
}
|
}
|
||||||
|
|
||||||
logoutReload(url) {
|
logoutReload(url) {
|
||||||
|
arePopupsVisible(false);
|
||||||
url = url || logoutLink();
|
url = url || logoutLink();
|
||||||
if (location.href !== url) {
|
if (location.href !== url) {
|
||||||
setTimeout(() => location.href = url, 100);
|
setTimeout(() => location.href = url, 100);
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import 'External/ko';
|
import 'External/ko';
|
||||||
|
|
||||||
import { Settings, SettingsGet } from 'Common/Globals';
|
import { SettingsGet, SettingsAdmin } from 'Common/Globals';
|
||||||
import { initThemes } from 'Stores/Theme';
|
import { initThemes } from 'Stores/Theme';
|
||||||
|
|
||||||
import Remote from 'Remote/Admin/Fetch';
|
import Remote from 'Remote/Admin/Fetch';
|
||||||
|
|
@ -11,6 +11,8 @@ import { LoginAdminScreen } from 'Screen/Admin/Login';
|
||||||
import { startScreens } from 'Knoin/Knoin';
|
import { startScreens } from 'Knoin/Knoin';
|
||||||
import { AbstractApp } from 'App/Abstract';
|
import { AbstractApp } from 'App/Abstract';
|
||||||
|
|
||||||
|
import { AskPopupView } from 'View/Popup/Ask';
|
||||||
|
|
||||||
export class AdminApp extends AbstractApp {
|
export class AdminApp extends AbstractApp {
|
||||||
constructor() {
|
constructor() {
|
||||||
super(Remote);
|
super(Remote);
|
||||||
|
|
@ -23,7 +25,8 @@ export class AdminApp extends AbstractApp {
|
||||||
}
|
}
|
||||||
|
|
||||||
start() {
|
start() {
|
||||||
if (!Settings.app('adminAllowed')) {
|
// if (!Settings.app('adminAllowed')) {
|
||||||
|
if (!SettingsAdmin('allowed')) {
|
||||||
rl.route.root();
|
rl.route.root();
|
||||||
setTimeout(() => location.href = '/', 1);
|
setTimeout(() => location.href = '/', 1);
|
||||||
} else if (SettingsGet('Auth')) {
|
} else if (SettingsGet('Auth')) {
|
||||||
|
|
@ -34,3 +37,16 @@ export class AdminApp extends AbstractApp {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
AskPopupView.credentials = function(sAskDesc, btnText) {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
this.showModal([
|
||||||
|
sAskDesc,
|
||||||
|
view => resolve({username:view.username(), password:view.passphrase()}),
|
||||||
|
() => resolve(null),
|
||||||
|
true,
|
||||||
|
3,
|
||||||
|
btnText
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -86,6 +86,8 @@ export class AppUser extends AbstractApp {
|
||||||
|
|
||||||
this.folderList = FolderUserStore.folderList;
|
this.folderList = FolderUserStore.folderList;
|
||||||
this.messageList = MessagelistUserStore;
|
this.messageList = MessagelistUserStore;
|
||||||
|
|
||||||
|
this.ask = AskPopupView;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -248,3 +250,46 @@ export class AppUser extends AbstractApp {
|
||||||
showScreenPopup(ComposePopupView, params);
|
showScreenPopup(ComposePopupView, params);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
AskPopupView.password = function(sAskDesc, btnText) {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
this.showModal([
|
||||||
|
sAskDesc,
|
||||||
|
view => resolve({password:view.passphrase(), remember:view.remember()}),
|
||||||
|
() => resolve(null),
|
||||||
|
true,
|
||||||
|
5,
|
||||||
|
btnText
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
AskPopupView.cryptkey = () => new Promise(resolve => {
|
||||||
|
const fn = () => AskPopupView.showModal([
|
||||||
|
i18n('CRYPTO/ASK_CRYPTKEY_PASS'),
|
||||||
|
view => {
|
||||||
|
let pass = view.passphrase();
|
||||||
|
if (pass) {
|
||||||
|
Remote.post('ResealCryptKey', null, {
|
||||||
|
passphrase: pass
|
||||||
|
}).then(response => {
|
||||||
|
resolve(response?.Result);
|
||||||
|
}).catch(e => {
|
||||||
|
if (111 === e.code) {
|
||||||
|
fn();
|
||||||
|
} else {
|
||||||
|
console.error(e);
|
||||||
|
resolve(null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
resolve(null);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
() => resolve(null),
|
||||||
|
true,
|
||||||
|
1,
|
||||||
|
i18n('CRYPTO/DECRYPT')
|
||||||
|
]);
|
||||||
|
fn();
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,7 @@ Notifications = {
|
||||||
ConnectionError: 104,
|
ConnectionError: 104,
|
||||||
DomainNotAllowed: 109,
|
DomainNotAllowed: 109,
|
||||||
AccountNotAllowed: 110,
|
AccountNotAllowed: 110,
|
||||||
|
CryptKeyError: 111,
|
||||||
|
|
||||||
ContactsSyncError: 140,
|
ContactsSyncError: 140,
|
||||||
|
|
||||||
|
|
@ -95,7 +96,6 @@ Notifications = {
|
||||||
JsonParse: 952,
|
JsonParse: 952,
|
||||||
// JsonTimeout: 953,
|
// JsonTimeout: 953,
|
||||||
|
|
||||||
UnknownNotification: 998,
|
|
||||||
UnknownError: 999,
|
UnknownError: 999,
|
||||||
|
|
||||||
// Admin
|
// Admin
|
||||||
|
|
|
||||||
|
|
@ -138,7 +138,7 @@ export const FileInfo = {
|
||||||
getContentType: fileName => {
|
getContentType: fileName => {
|
||||||
fileName = lowerCase(fileName);
|
fileName = lowerCase(fileName);
|
||||||
if ('winmail.dat' === fileName) {
|
if ('winmail.dat' === fileName) {
|
||||||
return app + 'ms-tnef';
|
return app + 'vnd.ms-tnef';
|
||||||
}
|
}
|
||||||
let ext = fileName.split('.').pop();
|
let ext = fileName.split('.').pop();
|
||||||
if (/^(txt|text|def|list|in|ini|log|sql|cfg|conf)$/.test(ext))
|
if (/^(txt|text|def|list|in|ini|log|sql|cfg|conf)$/.test(ext))
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ export const
|
||||||
|
|
||||||
Settings = rl.settings,
|
Settings = rl.settings,
|
||||||
SettingsGet = Settings.get,
|
SettingsGet = Settings.get,
|
||||||
|
SettingsAdmin = name => (SettingsGet('Admin') || {})[name],
|
||||||
SettingsCapa = name => name && !!(SettingsGet('Capa') || {})[name],
|
SettingsCapa = name => name && !!(SettingsGet('Capa') || {})[name],
|
||||||
|
|
||||||
dropdowns = [],
|
dropdowns = [],
|
||||||
|
|
|
||||||
|
|
@ -26,10 +26,8 @@ export class HtmlEditor {
|
||||||
if (element) {
|
if (element) {
|
||||||
onReady = onReady ? [onReady] : [];
|
onReady = onReady ? [onReady] : [];
|
||||||
this.onReady = fn => onReady.push(fn);
|
this.onReady = fn => onReady.push(fn);
|
||||||
// TODO: make 'which' user configurable
|
|
||||||
const which = SettingsUserStore.editorWysiwyg(),
|
const which = SettingsUserStore.editorWysiwyg(),
|
||||||
wysiwyg = WYSIWYGS.find(item => which == item.name) || WYSIWYGS.find(item => 'Squire' == item.name);
|
wysiwyg = WYSIWYGS.find(item => which == item.name) || WYSIWYGS.find(item => 'Squire' == item.name);
|
||||||
// const wysiwyg = WYSIWYGS.find(item => 'Squire' == item.name);
|
|
||||||
wysiwyg.construct(this, element, editor => setTimeout(()=>{
|
wysiwyg.construct(this, element, editor => setTimeout(()=>{
|
||||||
this.editor = editor;
|
this.editor = editor;
|
||||||
editor.on('blur', () => this.blurTrigger());
|
editor.on('blur', () => this.blurTrigger());
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
import { pInt } from 'Common/Utils';
|
import { pInt } from 'Common/Utils';
|
||||||
import { doc, Settings } from 'Common/Globals';
|
import { doc, Settings, SettingsAdmin } from 'Common/Globals';
|
||||||
|
|
||||||
const
|
const
|
||||||
BASE = doc.location.pathname.replace(/\/+$/,'') + '/',
|
BASE = doc.location.pathname.replace(/\/+$/,'') + '/',
|
||||||
HASH_PREFIX = '#/',
|
HASH_PREFIX = '#/',
|
||||||
|
|
||||||
adminPath = () => rl.adminArea() && !Settings.app('adminHost'),
|
adminPath = () => rl.adminArea() && !SettingsAdmin('host'),
|
||||||
|
|
||||||
prefix = () => BASE + '?' + (adminPath() ? Settings.app('adminPath') : '');
|
prefix = () => BASE + '?' + (adminPath() ? SettingsAdmin('path') : '');
|
||||||
|
|
||||||
export const
|
export const
|
||||||
SUB_QUERY_PREFIX = '&q[]=',
|
SUB_QUERY_PREFIX = '&q[]=',
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ const
|
||||||
|
|
||||||
getNotificationMessage = code => {
|
getNotificationMessage = code => {
|
||||||
let key = getKeyByValue(Notifications, code);
|
let key = getKeyByValue(Notifications, code);
|
||||||
return key ? I18N_DATA.NOTIFICATIONS[i18nKey(key).replace('_NOTIFICATION', '_ERROR')] : '';
|
return key ? I18N_DATA.NOTIFICATIONS[key] : '';
|
||||||
},
|
},
|
||||||
|
|
||||||
fromNow = date => relativeTime(Math.round((date.getTime() - Date.now()) / 1000));
|
fromNow = date => relativeTime(Math.round((date.getTime() - Date.now()) / 1000));
|
||||||
|
|
@ -211,7 +211,7 @@ export const
|
||||||
script.remove();
|
script.remove();
|
||||||
resolve();
|
resolve();
|
||||||
};
|
};
|
||||||
script.onerror = () => reject(new Error('Language '+language+' failed'));
|
script.onerror = () => reject(Error('Language '+language+' failed'));
|
||||||
script.src = langLink(language, admin);
|
script.src = langLink(language, admin);
|
||||||
// script.async = true;
|
// script.async = true;
|
||||||
doc.head.append(script);
|
doc.head.append(script);
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ export class JCard {
|
||||||
if (input) {
|
if (input) {
|
||||||
// read from jCard
|
// read from jCard
|
||||||
if (typeof input !== 'object') {
|
if (typeof input !== 'object') {
|
||||||
throw new Error('error reading vcard')
|
throw Error('error reading vcard')
|
||||||
}
|
}
|
||||||
this.parseFromJCard(input)
|
this.parseFromJCard(input)
|
||||||
}
|
}
|
||||||
|
|
@ -87,7 +87,7 @@ export class JCard {
|
||||||
arg = new VCardProperty(String(arg), value, params, type);
|
arg = new VCardProperty(String(arg), value, params, type);
|
||||||
}
|
}
|
||||||
if (!(arg instanceof VCardProperty)) {
|
if (!(arg instanceof VCardProperty)) {
|
||||||
throw new Error('invalid argument of VCard.set(), expects string arguments or a VCardProperty');
|
throw Error('invalid argument of VCard.set(), expects string arguments or a VCardProperty');
|
||||||
}
|
}
|
||||||
let field = arg.getField();
|
let field = arg.getField();
|
||||||
this.props.set(field, [arg]);
|
this.props.set(field, [arg]);
|
||||||
|
|
@ -101,7 +101,7 @@ export class JCard {
|
||||||
arg = new VCardProperty(String(arg), value, params, type);
|
arg = new VCardProperty(String(arg), value, params, type);
|
||||||
}
|
}
|
||||||
if (!(arg instanceof VCardProperty)) {
|
if (!(arg instanceof VCardProperty)) {
|
||||||
throw new Error('invalid argument of VCard.add(), expects string arguments or a VCardProperty');
|
throw Error('invalid argument of VCard.add(), expects string arguments or a VCardProperty');
|
||||||
}
|
}
|
||||||
// VCardProperty arguments
|
// VCardProperty arguments
|
||||||
let field = arg.getField();
|
let field = arg.getField();
|
||||||
|
|
@ -125,14 +125,14 @@ export class JCard {
|
||||||
else if (arg instanceof VCardProperty) {
|
else if (arg instanceof VCardProperty) {
|
||||||
let propArray = this.props.get(arg.getField());
|
let propArray = this.props.get(arg.getField());
|
||||||
if (!(propArray === null || propArray === void 0 ? void 0 : propArray.includes(arg)))
|
if (!(propArray === null || propArray === void 0 ? void 0 : propArray.includes(arg)))
|
||||||
throw new Error("Attempted to remove VCardProperty VCard does not have: ".concat(arg));
|
throw Error("Attempted to remove VCardProperty VCard does not have: ".concat(arg));
|
||||||
propArray.splice(propArray.indexOf(arg), 1);
|
propArray.splice(propArray.indexOf(arg), 1);
|
||||||
if (propArray.length === 0)
|
if (propArray.length === 0)
|
||||||
this.props.delete(arg.getField());
|
this.props.delete(arg.getField());
|
||||||
}
|
}
|
||||||
// incorrect arguments
|
// incorrect arguments
|
||||||
else
|
else
|
||||||
throw new Error('invalid argument of VCard.remove(), expects ' +
|
throw Error('invalid argument of VCard.remove(), expects ' +
|
||||||
'string and optional param filter or a VCardProperty');
|
'string and optional param filter or a VCardProperty');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -202,7 +202,7 @@ export class JCard {
|
||||||
parseFullName(options) {
|
parseFullName(options) {
|
||||||
let n = this.getOne('n');
|
let n = this.getOne('n');
|
||||||
if (n === undefined) {
|
if (n === undefined) {
|
||||||
throw new Error('\'fn\' VCardProperty not present in card, cannot parse full name');
|
throw Error('\'fn\' VCardProperty not present in card, cannot parse full name');
|
||||||
}
|
}
|
||||||
let fnString = '';
|
let fnString = '';
|
||||||
// Position in n -> position in fn
|
// Position in n -> position in fn
|
||||||
|
|
|
||||||
|
|
@ -55,7 +55,7 @@ export class VCardProperty {
|
||||||
}
|
}
|
||||||
// invalid property
|
// invalid property
|
||||||
else {
|
else {
|
||||||
throw new Error('invalid Property constructor');
|
throw Error('invalid Property constructor');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
5
dev/External/ko.js
vendored
5
dev/External/ko.js
vendored
|
|
@ -66,8 +66,7 @@ Object.assign(ko.bindingHandlers, {
|
||||||
},
|
},
|
||||||
update: (element, fValueAccessor) => {
|
update: (element, fValueAccessor) => {
|
||||||
let value = ko.unwrap(fValueAccessor());
|
let value = ko.unwrap(fValueAccessor());
|
||||||
value = isFunction(value) ? value() : value;
|
errorTip(element, isFunction(value) ? value() : value);
|
||||||
errorTip(element, value);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -107,7 +106,7 @@ Object.assign(ko.bindingHandlers, {
|
||||||
const command = fValueAccessor();
|
const command = fValueAccessor();
|
||||||
|
|
||||||
if (!command || !command.canExecute) {
|
if (!command || !command.canExecute) {
|
||||||
throw new Error('Value should be a command');
|
throw Error('Value should be a command');
|
||||||
}
|
}
|
||||||
|
|
||||||
ko.bindingHandlers['FORM'==element.nodeName ? 'submit' : 'click'].init(
|
ko.bindingHandlers['FORM'==element.nodeName ? 'submit' : 'click'].init(
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ export class AbstractModel {
|
||||||
constructor() {
|
constructor() {
|
||||||
/*
|
/*
|
||||||
if (new.target === AbstractModel) {
|
if (new.target === AbstractModel) {
|
||||||
throw new Error("Can't instantiate AbstractModel!");
|
throw Error("Can't instantiate AbstractModel!");
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
Object.defineProperty(this, 'disposables', {value: []});
|
Object.defineProperty(this, 'disposables', {value: []});
|
||||||
|
|
|
||||||
|
|
@ -80,6 +80,12 @@ export function MimeToMessage(data, message)
|
||||||
detached: true
|
detached: true
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
} else if ('application/pkcs7-mime' === type.value /*&& 'signed-data' === type.params['smime-type']=*/) {
|
||||||
|
message.smimeSigned({
|
||||||
|
micAlg: type.micalg,
|
||||||
|
bodyPart: part,
|
||||||
|
detached: false
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ export class AbstractCollectionModel extends Array
|
||||||
constructor() {
|
constructor() {
|
||||||
/*
|
/*
|
||||||
if (new.target === AbstractCollectionModel) {
|
if (new.target === AbstractCollectionModel) {
|
||||||
throw new Error("Can't instantiate AbstractCollectionModel!");
|
throw Error("Can't instantiate AbstractCollectionModel!");
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
super();
|
super();
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,10 @@ export class AccountModel extends AbstractModel {
|
||||||
&& setTimeout(()=>this.fetchUnread(), (Math.ceil(Math.random() * 10)) * 3000);
|
&& setTimeout(()=>this.fetchUnread(), (Math.ceil(Math.random() * 10)) * 3000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
label() {
|
||||||
|
return this.name || IDN.toUnicode(this.email);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get INBOX unread messages
|
* Get INBOX unread messages
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -126,7 +126,10 @@ export class AttachmentModel extends AbstractModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
get download() {
|
get download() {
|
||||||
return b64EncodeJSONSafe({
|
return b64EncodeJSONSafe(this.url ? {
|
||||||
|
fileName: this.fileName,
|
||||||
|
data: this.url.replace(/^.+,/, '')
|
||||||
|
} : {
|
||||||
folder: this.folder,
|
folder: this.folder,
|
||||||
uid: this.uid,
|
uid: this.uid,
|
||||||
mimeIndex: this.mimeIndex,
|
mimeIndex: this.mimeIndex,
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,8 @@ import { AbstractCollectionModel } from 'Model/AbstractCollection';
|
||||||
import { UNUSED_OPTION_VALUE } from 'Common/Consts';
|
import { UNUSED_OPTION_VALUE } from 'Common/Consts';
|
||||||
import { isArray, getKeyByValue, forEachObjectEntry, b64EncodeJSONSafe } from 'Common/Utils';
|
import { isArray, getKeyByValue, forEachObjectEntry, b64EncodeJSONSafe } from 'Common/Utils';
|
||||||
import { ClientSideKeyNameExpandedFolders, FolderType, FolderMetadataKeys } from 'Common/EnumsUser';
|
import { ClientSideKeyNameExpandedFolders, FolderType, FolderMetadataKeys } from 'Common/EnumsUser';
|
||||||
import { clearCache, getFolderFromCacheList, setFolder, setFolderInboxName, removeFolderFromCacheList } from 'Common/Cache';
|
import { clearCache, getFolderFromCacheList, setFolder, setFolderInboxName } from 'Common/Cache';
|
||||||
import { Settings, SettingsGet, fireEvent } from 'Common/Globals';
|
import { Settings, SettingsGet, fireEvent } from 'Common/Globals';
|
||||||
import { Notifications } from 'Common/Enums';
|
|
||||||
|
|
||||||
import * as Local from 'Storage/Client';
|
import * as Local from 'Storage/Client';
|
||||||
|
|
||||||
|
|
@ -15,7 +14,7 @@ import { MessagelistUserStore } from 'Stores/User/Messagelist';
|
||||||
import { SettingsUserStore } from 'Stores/User/Settings';
|
import { SettingsUserStore } from 'Stores/User/Settings';
|
||||||
|
|
||||||
import { sortFolders } from 'Common/Folders';
|
import { sortFolders } from 'Common/Folders';
|
||||||
import { i18n, translateTrigger, getNotification } from 'Common/Translator';
|
import { i18n, translateTrigger } from 'Common/Translator';
|
||||||
|
|
||||||
import { AbstractModel } from 'Knoin/AbstractModel';
|
import { AbstractModel } from 'Knoin/AbstractModel';
|
||||||
|
|
||||||
|
|
@ -485,41 +484,6 @@ export class FolderModel extends AbstractModel {
|
||||||
showScreenPopup(FolderPopupView, [this]);
|
showScreenPopup(FolderPopupView, [this]);
|
||||||
}
|
}
|
||||||
|
|
||||||
rename(nameToEdit, parentName) {
|
|
||||||
nameToEdit = nameToEdit.trim();
|
|
||||||
const folder = this,
|
|
||||||
parentFolder = getFolderFromCacheList(parentName),
|
|
||||||
oldFullname = folder.fullName,
|
|
||||||
newFullname = (parentFolder ? (parentName + parentFolder.delimiter) : '') + nameToEdit;
|
|
||||||
if (nameToEdit && newFullname != oldFullname) {
|
|
||||||
Remote.abort('Folders').post('FolderRename', FolderUserStore.foldersRenaming, {
|
|
||||||
oldName: oldFullname,
|
|
||||||
newName: newFullname,
|
|
||||||
subscribe: folder.isSubscribed() ? 1 : 0
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
folder.fullName = newFullname;
|
|
||||||
folder.name(nameToEdit);
|
|
||||||
if (folder.subFolders.length || folder.parentName != parentName) {
|
|
||||||
Remote.setTrigger(FolderUserStore.foldersLoading, true);
|
|
||||||
// clearTimeout(Remote.foldersTimeout);
|
|
||||||
// Remote.foldersTimeout = setTimeout(loadFolders, 500);
|
|
||||||
setTimeout(loadFolders, 500);
|
|
||||||
// TODO: rename all subfolders with folder.delimiter to prevent reload?
|
|
||||||
} else {
|
|
||||||
removeFolderFromCacheList(folder.fullName);
|
|
||||||
setFolder(folder);
|
|
||||||
sortFolders(parentFolder ? parentFolder.subFolders : FolderUserStore.folderList);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
FolderUserStore.error(
|
|
||||||
getNotification(error.code, '', Notifications.CantRenameFolder)
|
|
||||||
+ '.\n' + error.message);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* For url safe '/#/mailbox/...' path
|
* For url safe '/#/mailbox/...' path
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -32,9 +32,9 @@ export class IdentityModel extends AbstractModel {
|
||||||
});
|
});
|
||||||
|
|
||||||
addComputablesTo(this, {
|
addComputablesTo(this, {
|
||||||
smimeKeyEncrypted: () => this.smimeKey().includes('-----BEGIN ENCRYPTED PRIVATE KEY-----')
|
smimeKeyEncrypted: () => this.smimeKey().includes('-----BEGIN ENCRYPTED PRIVATE KEY-----'),
|
||||||
// smimeKeyValid: () => this.smimeKeyEncrypted() | this.smimeKey().includes('-----BEGIN PRIVATE KEY-----')
|
smimeKeyValid: () => /^-----BEGIN (ENCRYPTED )?PRIVATE KEY-----/.test(this.smimeKey()),
|
||||||
// smimeCertificateValid: () => this.smimeKey().includes('-----BEGIN CERTIFICATE-----')
|
smimeCertificateValid: () => /^-----BEGIN CERTIFICATE-----/.test(this.smimeCertificate())
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -120,12 +120,10 @@ export class MessageModel extends AbstractModel {
|
||||||
encrypted: false,
|
encrypted: false,
|
||||||
|
|
||||||
pgpSigned: null,
|
pgpSigned: null,
|
||||||
pgpVerified: null,
|
|
||||||
pgpEncrypted: null,
|
pgpEncrypted: null,
|
||||||
pgpDecrypted: false,
|
pgpDecrypted: false,
|
||||||
|
|
||||||
smimeSigned: null,
|
smimeSigned: null,
|
||||||
smimeVerified: null,
|
|
||||||
smimeEncrypted: null,
|
smimeEncrypted: null,
|
||||||
smimeDecrypted: false,
|
smimeDecrypted: false,
|
||||||
|
|
||||||
|
|
@ -199,10 +197,9 @@ export class MessageModel extends AbstractModel {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.smimeSigned.subscribe(value => {
|
this.smimeSigned.subscribe(value =>
|
||||||
value?.body && MimeToMessage(value.body, this);
|
value?.body && MimeToMessage(value.body, this)
|
||||||
value?.body && this.smimeVerified(value.verified);
|
);
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get requestHash() {
|
get requestHash() {
|
||||||
|
|
@ -423,6 +420,11 @@ export class MessageModel extends AbstractModel {
|
||||||
return this.viewBody(false);
|
return this.viewBody(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
swapColors() {
|
||||||
|
const cl = this.body?.classList;
|
||||||
|
cl && cl.toggle('swapColors');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {boolean=} print = false
|
* @param {boolean=} print = false
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,6 @@ checkResponseError = data => {
|
||||||
Notifications.DomainNotAllowed,
|
Notifications.DomainNotAllowed,
|
||||||
Notifications.AccountNotAllowed,
|
Notifications.AccountNotAllowed,
|
||||||
Notifications.MailServerError,
|
Notifications.MailServerError,
|
||||||
Notifications.UnknownNotification,
|
|
||||||
Notifications.UnknownError
|
Notifications.UnknownError
|
||||||
].includes(err)
|
].includes(err)
|
||||||
) {
|
) {
|
||||||
|
|
@ -133,7 +132,7 @@ export class AbstractFetchRemote
|
||||||
fetchJSON(sAction, getURL(sGetAdd),
|
fetchJSON(sAction, getURL(sGetAdd),
|
||||||
sGetAdd ? null : (params || {}),
|
sGetAdd ? null : (params || {}),
|
||||||
undefined === iTimeout ? 30000 : pInt(iTimeout),
|
undefined === iTimeout ? 30000 : pInt(iTimeout),
|
||||||
data => {
|
async data => {
|
||||||
let iError = 0;
|
let iError = 0;
|
||||||
if (data) {
|
if (data) {
|
||||||
/*
|
/*
|
||||||
|
|
@ -149,6 +148,10 @@ export class AbstractFetchRemote
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (111 === iError && rl.app.ask && await rl.app.ask.cryptkey()) {
|
||||||
|
return this.request(sAction, fCallback, params, iTimeout, sGetAdd);
|
||||||
|
}
|
||||||
|
|
||||||
fCallback && fCallback(
|
fCallback && fCallback(
|
||||||
iError,
|
iError,
|
||||||
data,
|
data,
|
||||||
|
|
@ -186,12 +189,16 @@ export class AbstractFetchRemote
|
||||||
post(action, fTrigger, params, timeOut) {
|
post(action, fTrigger, params, timeOut) {
|
||||||
this.setTrigger(fTrigger, true);
|
this.setTrigger(fTrigger, true);
|
||||||
return fetchJSON(action, getURL(), params || {}, pInt(timeOut, 30000),
|
return fetchJSON(action, getURL(), params || {}, pInt(timeOut, 30000),
|
||||||
data => {
|
async data => {
|
||||||
abort(action, 0, 1);
|
abort(action, 0, 1);
|
||||||
|
|
||||||
if (!data) {
|
if (!data) {
|
||||||
return Promise.reject(new FetchError(Notifications.JsonParse));
|
return Promise.reject(new FetchError(Notifications.JsonParse));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (111 === data?.ErrorCode && rl.app.ask && await rl.app.ask.cryptkey()) {
|
||||||
|
return this.post(action, fTrigger, params, timeOut);
|
||||||
|
}
|
||||||
/*
|
/*
|
||||||
let isCached = false, type = '';
|
let isCached = false, type = '';
|
||||||
if (data?.epoch) {
|
if (data?.epoch) {
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,9 @@ import ko from 'ko';
|
||||||
|
|
||||||
import Remote from 'Remote/Admin/Fetch';
|
import Remote from 'Remote/Admin/Fetch';
|
||||||
import { forEachObjectEntry } from 'Common/Utils';
|
import { forEachObjectEntry } from 'Common/Utils';
|
||||||
|
import { SettingsAdmin } from 'Common/Globals';
|
||||||
|
import { LanguageStore } from 'Stores/Language';
|
||||||
|
import { ThemeStore } from 'Stores/Theme';
|
||||||
|
|
||||||
export class AdminSettingsConfig /*extends AbstractViewSettings*/ {
|
export class AdminSettingsConfig /*extends AbstractViewSettings*/ {
|
||||||
|
|
||||||
|
|
@ -53,6 +56,11 @@ export class AdminSettingsConfig /*extends AbstractViewSettings*/ {
|
||||||
items: []
|
items: []
|
||||||
};
|
};
|
||||||
forEachObjectEntry(items, (skey, item) => {
|
forEachObjectEntry(items, (skey, item) => {
|
||||||
|
if ('language' === skey) {
|
||||||
|
item[2] = ('webmail' === key) ? LanguageStore.languages : SettingsAdmin('languages');
|
||||||
|
} else if ('theme' === skey) {
|
||||||
|
item[2] = ThemeStore.themes;
|
||||||
|
}
|
||||||
'admin_password' === skey ||
|
'admin_password' === skey ||
|
||||||
section.items.push({
|
section.items.push({
|
||||||
key: `config[${key}][${skey}]`,
|
key: `config[${key}][${skey}]`,
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,9 @@
|
||||||
import ko from 'ko';
|
import ko from 'ko';
|
||||||
|
|
||||||
import {
|
|
||||||
isArray
|
|
||||||
} from 'Common/Utils';
|
|
||||||
|
|
||||||
import { addObservablesTo, addSubscribablesTo, addComputablesTo } from 'External/ko';
|
import { addObservablesTo, addSubscribablesTo, addComputablesTo } from 'External/ko';
|
||||||
|
|
||||||
import { SaveSettingStatus } from 'Common/Enums';
|
import { SaveSettingStatus } from 'Common/Enums';
|
||||||
import { Settings, SettingsGet, SettingsCapa } from 'Common/Globals';
|
import { SettingsAdmin, SettingsGet, SettingsCapa } from 'Common/Globals';
|
||||||
import { translatorReload, convertLangName } from 'Common/Translator';
|
import { translatorReload, convertLangName } from 'Common/Translator';
|
||||||
|
|
||||||
import { AbstractViewSettings } from 'Knoin/AbstractViews';
|
import { AbstractViewSettings } from 'Knoin/AbstractViews';
|
||||||
|
|
@ -24,11 +20,7 @@ export class AdminSettingsGeneral extends AbstractViewSettings {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
this.language = LanguageStore.language;
|
this.language = LanguageStore.language;
|
||||||
this.languages = LanguageStore.languages;
|
this.languageAdmin = ko.observable(SettingsAdmin('language'));
|
||||||
|
|
||||||
const aLanguagesAdmin = Settings.app('languagesAdmin');
|
|
||||||
this.languagesAdmin = ko.observableArray(isArray(aLanguagesAdmin) ? aLanguagesAdmin : []);
|
|
||||||
this.languageAdmin = ko.observable(SettingsGet('languageAdmin'));
|
|
||||||
|
|
||||||
this.theme = ThemeStore.theme;
|
this.theme = ThemeStore.theme;
|
||||||
this.themes = ThemeStore.themes;
|
this.themes = ThemeStore.themes;
|
||||||
|
|
@ -107,14 +99,18 @@ export class AdminSettingsGeneral extends AbstractViewSettings {
|
||||||
}
|
}
|
||||||
|
|
||||||
selectLanguage() {
|
selectLanguage() {
|
||||||
showScreenPopup(LanguagesPopupView, [this.language, this.languages(), LanguageStore.userLanguage()]);
|
showScreenPopup(LanguagesPopupView, [
|
||||||
|
this.language,
|
||||||
|
LanguageStore.languages,
|
||||||
|
LanguageStore.userLanguage()
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
selectLanguageAdmin() {
|
selectLanguageAdmin() {
|
||||||
showScreenPopup(LanguagesPopupView, [
|
showScreenPopup(LanguagesPopupView, [
|
||||||
this.languageAdmin,
|
this.languageAdmin,
|
||||||
this.languagesAdmin(),
|
SettingsAdmin('languages'),
|
||||||
SettingsGet('languageUsers')
|
SettingsAdmin('clientLanguage')
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ export class AdminSettingsSecurity extends AbstractViewSettings {
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
this.addSettings(['useLocalProxyForExternalImages']);
|
this.addSettings(['useLocalProxyForExternalImages', 'autoVerifySignatures']);
|
||||||
|
|
||||||
this.weakPassword = rl.app.weakPassword;
|
this.weakPassword = rl.app.weakPassword;
|
||||||
|
|
||||||
|
|
@ -28,6 +28,7 @@ export class AdminSettingsSecurity extends AbstractViewSettings {
|
||||||
|
|
||||||
viewQRCode: '',
|
viewQRCode: '',
|
||||||
|
|
||||||
|
capaGnuPG: SettingsCapa('GnuPG'),
|
||||||
capaOpenPGP: SettingsCapa('OpenPGP')
|
capaOpenPGP: SettingsCapa('OpenPGP')
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -65,7 +66,8 @@ export class AdminSettingsSecurity extends AbstractViewSettings {
|
||||||
|
|
||||||
adminPasswordNew2: reset,
|
adminPasswordNew2: reset,
|
||||||
|
|
||||||
capaOpenPGP: value => Remote.saveSetting('CapaOpenPGP', value)
|
capaGnuPG: value => Remote.saveSetting('capaGnuPG', value),
|
||||||
|
capaOpenPGP: value => Remote.saveSetting('capaOpenPGP', value)
|
||||||
});
|
});
|
||||||
|
|
||||||
this.adminTOTP(SettingsGet('adminTOTP'));
|
this.adminTOTP(SettingsGet('adminTOTP'));
|
||||||
|
|
@ -75,6 +77,16 @@ export class AdminSettingsSecurity extends AbstractViewSettings {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
generateTOTP() {
|
||||||
|
let CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567',
|
||||||
|
length = 16,
|
||||||
|
secret = '';
|
||||||
|
while (0 < length--) {
|
||||||
|
secret += CHARS[Math.floor(Math.random() * 32)];
|
||||||
|
}
|
||||||
|
this.adminTOTP(secret);
|
||||||
|
}
|
||||||
|
|
||||||
saveAdminUserCommand() {
|
saveAdminUserCommand() {
|
||||||
if (!this.adminLogin().trim()) {
|
if (!this.adminLogin().trim()) {
|
||||||
this.adminLoginError(true);
|
this.adminLoginError(true);
|
||||||
|
|
|
||||||
|
|
@ -63,11 +63,7 @@ export class UserSettingsSecurity extends AbstractViewSettings {
|
||||||
|
|
||||||
importToOpenPGP() {
|
importToOpenPGP() {
|
||||||
OpenPGPUserStore.isSupported() && Remote.request('GetPGPKeys',
|
OpenPGPUserStore.isSupported() && Remote.request('GetPGPKeys',
|
||||||
(iError, oData) => {
|
(iError, oData) => !iError && oData.Result && OpenPGPUserStore.importKeys(oData.Result)
|
||||||
if (!iError && oData.Result) {
|
|
||||||
oData.Result.forEach(key => OpenPGPUserStore.importKey(key));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ export class AbstractModel {
|
||||||
constructor() {
|
constructor() {
|
||||||
/*
|
/*
|
||||||
if (new.target === AbstractModel) {
|
if (new.target === AbstractModel) {
|
||||||
throw new Error("Can't instantiate AbstractModel!");
|
throw Error("Can't instantiate AbstractModel!");
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
Object.defineProperty(this, 'disposables', {value: []});
|
Object.defineProperty(this, 'disposables', {value: []});
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ DomainAdminStore.fetch = () => {
|
||||||
if (!iError) {
|
if (!iError) {
|
||||||
DomainAdminStore(
|
DomainAdminStore(
|
||||||
data.Result.map(item => {
|
data.Result.map(item => {
|
||||||
|
item.name = IDN.toUnicode(item.name);
|
||||||
item.disabled = ko.observable(item.disabled);
|
item.disabled = ko.observable(item.disabled);
|
||||||
item.askDelete = ko.observable(false);
|
item.askDelete = ko.observable(false);
|
||||||
return item;
|
return item;
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ export const LanguageStore = {
|
||||||
const aLanguages = Settings.app('languages');
|
const aLanguages = Settings.app('languages');
|
||||||
this.languages(isArray(aLanguages) ? aLanguages : []);
|
this.languages(isArray(aLanguages) ? aLanguages : []);
|
||||||
this.language(SettingsGet('language'));
|
this.language(SettingsGet('language'));
|
||||||
this.userLanguage(SettingsGet('userLanguage'));
|
this.userLanguage(SettingsGet('clientLanguage'));
|
||||||
this.hourCycle(SettingsGet('hourCycle'));
|
this.hourCycle(SettingsGet('hourCycle'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ const
|
||||||
keys.find(key =>
|
keys.find(key =>
|
||||||
// key[sign ? 'can_sign' : 'can_decrypt']
|
// key[sign ? 'can_sign' : 'can_decrypt']
|
||||||
(key.can_sign || key.can_decrypt)
|
(key.can_sign || key.can_decrypt)
|
||||||
&& (key.emails.includes(query) || key.subkeys.find(key => query == key.keyid || query == key.fingerprint))
|
&& (key.for(query) || key.subkeys.find(key => query == key.keyid || query == key.fingerprint))
|
||||||
);
|
);
|
||||||
|
|
||||||
export const GnuPGUserStore = new class {
|
export const GnuPGUserStore = new class {
|
||||||
|
|
@ -46,6 +46,7 @@ export const GnuPGUserStore = new class {
|
||||||
key.fingerprint = key.subkeys[0].fingerprint;
|
key.fingerprint = key.subkeys[0].fingerprint;
|
||||||
key.uids.forEach(uid => uid.email && aEmails.push(uid.email));
|
key.uids.forEach(uid => uid.email && aEmails.push(uid.email));
|
||||||
key.emails = aEmails;
|
key.emails = aEmails;
|
||||||
|
key.for = email => aEmails.includes(IDN.toASCII(email));
|
||||||
key.askDelete = ko.observable(false);
|
key.askDelete = ko.observable(false);
|
||||||
key.openForDeletion = ko.observable(null).askDeleteHelper();
|
key.openForDeletion = ko.observable(null).askDeleteHelper();
|
||||||
key.remove = () => {
|
key.remove = () => {
|
||||||
|
|
@ -69,7 +70,7 @@ export const GnuPGUserStore = new class {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
if (isPrivate) {
|
if (isPrivate) {
|
||||||
key.password = async (btnTxt = 'CRYPTO/SIGN') => {
|
key.password = async btnTxt => {
|
||||||
const pass = await Passphrases.ask(key,
|
const pass = await Passphrases.ask(key,
|
||||||
'GnuPG key<br>' + key.id + ' ' + key.emails[0],
|
'GnuPG key<br>' + key.id + ' ' + key.emails[0],
|
||||||
btnTxt
|
btnTxt
|
||||||
|
|
@ -97,6 +98,7 @@ export const GnuPGUserStore = new class {
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Passphrases.delete(key);
|
Passphrases.delete(key);
|
||||||
|
alert(e.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return key.armor;
|
return key.armor;
|
||||||
|
|
@ -148,7 +150,7 @@ export const GnuPGUserStore = new class {
|
||||||
const count = recipients.length,
|
const count = recipients.length,
|
||||||
length = count ? recipients.filter(email =>
|
length = count ? recipients.filter(email =>
|
||||||
// (key.can_verify || key.can_encrypt) &&
|
// (key.can_verify || key.can_encrypt) &&
|
||||||
this.publicKeys.find(key => key.emails.includes(email))
|
this.publicKeys.find(key => key.for(email))
|
||||||
).length : 0;
|
).length : 0;
|
||||||
return length && length === count;
|
return length && length === count;
|
||||||
}
|
}
|
||||||
|
|
@ -156,7 +158,7 @@ export const GnuPGUserStore = new class {
|
||||||
getPublicKeyFingerprints(recipients) {
|
getPublicKeyFingerprints(recipients) {
|
||||||
const fingerprints = [];
|
const fingerprints = [];
|
||||||
recipients.forEach(email => {
|
recipients.forEach(email => {
|
||||||
fingerprints.push(this.publicKeys.find(key => key.emails.includes(email)).fingerprint);
|
fingerprints.push(this.publicKeys.find(key => key.for(email)).fingerprint);
|
||||||
});
|
});
|
||||||
return fingerprints;
|
return fingerprints;
|
||||||
}
|
}
|
||||||
|
|
@ -204,7 +206,7 @@ export const GnuPGUserStore = new class {
|
||||||
}
|
}
|
||||||
|
|
||||||
async verify(message) {
|
async verify(message) {
|
||||||
let data = message.pgpSigned(); // { bodyPartId: "1", sigPartId: "2", micAlg: "pgp-sha256" }
|
let data = message.pgpSigned(); // { partId: "1", sigPartId: "2", micAlg: "pgp-sha256" }
|
||||||
if (data) {
|
if (data) {
|
||||||
data = { ...data }; // clone
|
data = { ...data }; // clone
|
||||||
// const sender = message.from[0].email;
|
// const sender = message.from[0].email;
|
||||||
|
|
@ -227,7 +229,7 @@ export const GnuPGUserStore = new class {
|
||||||
}
|
}
|
||||||
|
|
||||||
async sign(privateKey) {
|
async sign(privateKey) {
|
||||||
return await privateKey.password();
|
return await privateKey.password('CRYPTO/SIGN');
|
||||||
}
|
}
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ import { Passphrases } from 'Storage/Passphrases';
|
||||||
const
|
const
|
||||||
findOpenPGPKey = (keys, query/*, sign*/) =>
|
findOpenPGPKey = (keys, query/*, sign*/) =>
|
||||||
keys.find(key =>
|
keys.find(key =>
|
||||||
key.emails.includes(query) || query == key.id || query == key.fingerprint
|
key.for(query) || query == key.id || query == key.fingerprint
|
||||||
),
|
),
|
||||||
|
|
||||||
decryptKey = async (privateKey, btnTxt = 'SIGN') => {
|
decryptKey = async (privateKey, btnTxt = 'SIGN') => {
|
||||||
|
|
@ -59,9 +59,11 @@ const
|
||||||
let keys = [], key,
|
let keys = [], key,
|
||||||
armoredKeys = JSON.parse(storage.getItem(itemname)),
|
armoredKeys = JSON.parse(storage.getItem(itemname)),
|
||||||
i = arrayLength(armoredKeys);
|
i = arrayLength(armoredKeys);
|
||||||
while (i--) {
|
while (i--) try {
|
||||||
key = await openpgp.readKey({armoredKey:armoredKeys[i]});
|
key = await openpgp.readKey({armoredKey:armoredKeys[i]});
|
||||||
key.err || keys.push(new OpenPgpKeyModel(armoredKeys[i], key));
|
key.err || keys.push(new OpenPgpKeyModel(armoredKeys[i], key));
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
}
|
}
|
||||||
return keys;
|
return keys;
|
||||||
},
|
},
|
||||||
|
|
@ -77,15 +79,11 @@ const
|
||||||
class OpenPgpKeyModel {
|
class OpenPgpKeyModel {
|
||||||
constructor(armor, key) {
|
constructor(armor, key) {
|
||||||
this.key = key;
|
this.key = key;
|
||||||
const aEmails = [];
|
|
||||||
if (key.users) {
|
|
||||||
key.users.forEach(user => user.userID.email && aEmails.push(user.userID.email));
|
|
||||||
}
|
|
||||||
this.id = key.getKeyID().toHex().toUpperCase();
|
this.id = key.getKeyID().toHex().toUpperCase();
|
||||||
this.fingerprint = key.getFingerprint();
|
this.fingerprint = key.getFingerprint();
|
||||||
this.can_encrypt = !!key.getEncryptionKey();
|
this.can_encrypt = !!key.getEncryptionKey();
|
||||||
this.can_sign = !!key.getSigningKey();
|
this.can_sign = !!key.getSigningKey();
|
||||||
this.emails = aEmails;
|
this.emails = key.users.map(user => IDN.toASCII(user.userID.email)).filter(email => email);
|
||||||
this.armor = armor;
|
this.armor = armor;
|
||||||
this.askDelete = ko.observable(false);
|
this.askDelete = ko.observable(false);
|
||||||
this.openForDeletion = ko.observable(null).askDeleteHelper();
|
this.openForDeletion = ko.observable(null).askDeleteHelper();
|
||||||
|
|
@ -93,6 +91,18 @@ class OpenPgpKeyModel {
|
||||||
// key.getPrimaryUser()
|
// key.getPrimaryUser()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
get id() { return this.key.getKeyID().toHex().toUpperCase(); }
|
||||||
|
get fingerprint() { return this.key.getFingerprint(); }
|
||||||
|
get can_encrypt() { return !!this.key.getEncryptionKey(); }
|
||||||
|
get can_sign() { return !!this.key.getSigningKey(); }
|
||||||
|
get emails() { return this.key.users.map(user => IDN.toASCII(user.userID.email)).filter(email => email); }
|
||||||
|
get armor() { return this.key.armor(); }
|
||||||
|
*/
|
||||||
|
for(email) {
|
||||||
|
return this.emails.includes(IDN.toASCII(email));
|
||||||
|
}
|
||||||
|
|
||||||
view() {
|
view() {
|
||||||
showScreenPopup(OpenPgpKeyPopupView, [this]);
|
showScreenPopup(OpenPgpKeyPopupView, [this]);
|
||||||
}
|
}
|
||||||
|
|
@ -142,18 +152,29 @@ export const OpenPGPUserStore = new class {
|
||||||
}
|
}
|
||||||
|
|
||||||
importKey(armoredKey) {
|
importKey(armoredKey) {
|
||||||
window.openpgp && openpgp.readKey({armoredKey:armoredKey}).then(key => {
|
this.importKeys([armoredKey]);
|
||||||
if (!key.err) {
|
}
|
||||||
key = new OpenPgpKeyModel(armoredKey, key);
|
|
||||||
const isPrivate = key.key.isPrivate(),
|
async importKeys(keys) {
|
||||||
keys = isPrivate ? this.privateKeys : this.publicKeys;
|
if (window.openpgp) {
|
||||||
if (!keys.find(entry => entry.fingerprint == key.fingerprint)) {
|
const privateKeys = this.privateKeys(),
|
||||||
keys.push(key);
|
publicKeys = this.publicKeys();
|
||||||
keys(sort(keys));
|
for (const armoredKey of keys) try {
|
||||||
storeOpenPgpKeys(keys, isPrivate ? privateKeysItem : publicKeysItem);
|
let key = await openpgp.readKey({armoredKey:armoredKey});
|
||||||
|
if (!key.err) {
|
||||||
|
key = new OpenPgpKeyModel(armoredKey, key);
|
||||||
|
const keys = key.key.isPrivate() ? privateKeys : publicKeys;
|
||||||
|
keys.find(entry => entry.fingerprint == key.fingerprint)
|
||||||
|
|| keys.push(key);
|
||||||
}
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e, armoredKey);
|
||||||
}
|
}
|
||||||
});
|
this.privateKeys(sort(privateKeys));
|
||||||
|
this.publicKeys(sort(publicKeys));
|
||||||
|
storeOpenPgpKeys(privateKeys, privateKeysItem);
|
||||||
|
storeOpenPgpKeys(publicKeys, publicKeysItem);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -180,7 +201,7 @@ export const OpenPGPUserStore = new class {
|
||||||
hasPublicKeyForEmails(recipients) {
|
hasPublicKeyForEmails(recipients) {
|
||||||
const count = recipients.length,
|
const count = recipients.length,
|
||||||
length = count ? recipients.filter(email =>
|
length = count ? recipients.filter(email =>
|
||||||
this.publicKeys().find(key => key.emails.includes(email))
|
this.publicKeys().find(key => key.for(email))
|
||||||
).length : 0;
|
).length : 0;
|
||||||
return length && length === count;
|
return length && length === count;
|
||||||
}
|
}
|
||||||
|
|
@ -229,8 +250,8 @@ export const OpenPGPUserStore = new class {
|
||||||
* https://docs.openpgpjs.org/#sign-and-verify-cleartext-messages
|
* https://docs.openpgpjs.org/#sign-and-verify-cleartext-messages
|
||||||
*/
|
*/
|
||||||
async verify(message) {
|
async verify(message) {
|
||||||
const data = message.pgpSigned(), // { bodyPartId: "1", sigPartId: "2", micAlg: "pgp-sha256" }
|
const data = message.pgpSigned(), // { partId: "1", sigPartId: "2", micAlg: "pgp-sha256" }
|
||||||
publicKey = this.publicKeys().find(key => key.emails.includes(message.from[0].email));
|
publicKey = this.publicKeys().find(key => key.for(message.from[0].email));
|
||||||
if (data && publicKey) {
|
if (data && publicKey) {
|
||||||
data.folder = message.folder;
|
data.folder = message.folder;
|
||||||
data.uid = message.uid;
|
data.uid = message.uid;
|
||||||
|
|
@ -289,7 +310,7 @@ export const OpenPGPUserStore = new class {
|
||||||
*/
|
*/
|
||||||
async encrypt(text, recipients, signPrivateKey) {
|
async encrypt(text, recipients, signPrivateKey) {
|
||||||
const count = recipients.length;
|
const count = recipients.length;
|
||||||
recipients = recipients.map(email => this.publicKeys().find(key => key.emails.includes(email))).filter(key => key);
|
recipients = recipients.map(email => this.publicKeys().find(key => key.for(email))).filter(key => key);
|
||||||
if (count === recipients.length) {
|
if (count === recipients.length) {
|
||||||
if (signPrivateKey) {
|
if (signPrivateKey) {
|
||||||
signPrivateKey = await decryptKey(signPrivateKey);
|
signPrivateKey = await decryptKey(signPrivateKey);
|
||||||
|
|
|
||||||
|
|
@ -108,19 +108,14 @@ export const
|
||||||
}
|
}
|
||||||
|
|
||||||
async verify(message) {
|
async verify(message) {
|
||||||
const signed = message.pgpSigned();
|
const signed = message.pgpSigned(),
|
||||||
|
sender = message.from[0].email;
|
||||||
if (signed) {
|
if (signed) {
|
||||||
const sender = message.from[0].email,
|
// OpenPGP only when inline, else we must download the whole message
|
||||||
gnupg = GnuPGUserStore.hasPublicKeyForEmails([sender]),
|
if (!signed.sigPartId && OpenPGPUserStore.hasPublicKeyForEmails([sender])) {
|
||||||
openpgp = OpenPGPUserStore.hasPublicKeyForEmails([sender]);
|
|
||||||
// Detached signature use GnuPG first, else we must download whole message
|
|
||||||
if (gnupg && signed.sigPartId) {
|
|
||||||
return GnuPGUserStore.verify(message);
|
|
||||||
}
|
|
||||||
if (openpgp) {
|
|
||||||
return OpenPGPUserStore.verify(message);
|
return OpenPGPUserStore.verify(message);
|
||||||
}
|
}
|
||||||
if (gnupg) {
|
if (GnuPGUserStore.hasPublicKeyForEmails([sender])) {
|
||||||
return GnuPGUserStore.verify(message);
|
return GnuPGUserStore.verify(message);
|
||||||
}
|
}
|
||||||
// Mailvelope can't
|
// Mailvelope can't
|
||||||
|
|
@ -133,12 +128,12 @@ export const
|
||||||
let result = {};
|
let result = {};
|
||||||
recipients.forEach(email => {
|
recipients.forEach(email => {
|
||||||
OpenPGPUserStore.publicKeys().forEach(key => {
|
OpenPGPUserStore.publicKeys().forEach(key => {
|
||||||
if (key.emails.includes(email)) {
|
if (key.for(email)) {
|
||||||
result[email] = key.armor;
|
result[email] = key.armor;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
GnuPGUserStore.publicKeys.map(async key => {
|
GnuPGUserStore.publicKeys.map(async key => {
|
||||||
if (!result[email] && key.emails.includes(email)) {
|
if (!result[email] && key.for(email)) {
|
||||||
result[email] = await key.fetch();
|
result[email] = await key.fetch();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -331,6 +331,11 @@
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
|
&.swapColors {
|
||||||
|
background-color: var(--main-color, currentcolor);
|
||||||
|
color: var(--main-bg-color);
|
||||||
|
}
|
||||||
|
|
||||||
* {
|
* {
|
||||||
box-sizing: unset;
|
box-sizing: unset;
|
||||||
/* unicode-bidi: plaintext;*/
|
/* unicode-bidi: plaintext;*/
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@
|
||||||
color: red;
|
color: red;
|
||||||
content: attr(data-rainloopErrorTip);
|
content: attr(data-rainloopErrorTip);
|
||||||
font-size: 90%;
|
font-size: 90%;
|
||||||
left: 100%;
|
left: 0;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
padding: 0.25em;
|
padding: 0.25em;
|
||||||
position : absolute;
|
position : absolute;
|
||||||
|
|
|
||||||
|
|
@ -123,4 +123,8 @@ html[dir="rtl"] {
|
||||||
border-right: 6px solid #eee;
|
border-right: 6px solid #eee;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[data-rainloopErrorTip]::before {
|
||||||
|
left: 100%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -85,29 +85,3 @@ export class AskPopupView extends AbstractViewPopup {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
AskPopupView.password = function(sAskDesc, btnText) {
|
|
||||||
return new Promise(resolve => {
|
|
||||||
this.showModal([
|
|
||||||
sAskDesc,
|
|
||||||
view => resolve({password:view.passphrase(), remember:view.remember()}),
|
|
||||||
() => resolve(null),
|
|
||||||
true,
|
|
||||||
5,
|
|
||||||
btnText
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
AskPopupView.credentials = function(sAskDesc, btnText) {
|
|
||||||
return new Promise(resolve => {
|
|
||||||
this.showModal([
|
|
||||||
sAskDesc,
|
|
||||||
view => resolve({username:view.username(), password:view.passphrase(), remember:view.remember()}),
|
|
||||||
() => resolve(null),
|
|
||||||
true,
|
|
||||||
3,
|
|
||||||
btnText
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1377,7 +1377,7 @@ export class ComposePopupView extends AbstractViewPopup {
|
||||||
key && options.push(['OpenPGP', key]);
|
key && options.push(['OpenPGP', key]);
|
||||||
key = GnuPGUserStore.getPrivateKeyFor(email, 1);
|
key = GnuPGUserStore.getPrivateKeyFor(email, 1);
|
||||||
key && options.push(['GnuPG', key]);
|
key && options.push(['GnuPG', key]);
|
||||||
identity.smimeKey() && identity.smimeCertificate() && identity.email() === email
|
identity.smimeKeyValid() && identity.smimeCertificateValid() && identity.email() === email
|
||||||
&& options.push(['S/MIME']);
|
&& options.push(['S/MIME']);
|
||||||
console.dir({signOptions: options});
|
console.dir({signOptions: options});
|
||||||
this.signOptions(options);
|
this.signOptions(options);
|
||||||
|
|
@ -1561,12 +1561,13 @@ export class ComposePopupView extends AbstractViewPopup {
|
||||||
}
|
}
|
||||||
if ('S/MIME' == signOptions[i][0]) {
|
if ('S/MIME' == signOptions[i][0]) {
|
||||||
// TODO: sign in PHP fails
|
// TODO: sign in PHP fails
|
||||||
params.signCertificate = identity.smimeCertificate();
|
params.sign = 'S/MIME';
|
||||||
params.signPrivateKey = identity.smimeKey();
|
// params.signCertificate = identity.smimeCertificate();
|
||||||
|
// params.signPrivateKey = identity.smimeKey();
|
||||||
if (identity.smimeKeyEncrypted()) {
|
if (identity.smimeKeyEncrypted()) {
|
||||||
const pass = await Passphrases.ask(identity,
|
const pass = await Passphrases.ask(identity,
|
||||||
i18n('SMIME/PRIVATE_KEY_OF', {EMAIL: identity.email()}),
|
i18n('SMIME/PRIVATE_KEY_OF', {EMAIL: identity.email()}),
|
||||||
'CRYPTO/DECRYPT'
|
'CRYPTO/SIGN'
|
||||||
);
|
);
|
||||||
if (null != pass) {
|
if (null != pass) {
|
||||||
params.signPassphrase = pass.password;
|
params.signPassphrase = pass.password;
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ const
|
||||||
imapType: 0,
|
imapType: 0,
|
||||||
imapTimeout: 300,
|
imapTimeout: 300,
|
||||||
imapShortLogin: false,
|
imapShortLogin: false,
|
||||||
|
imapLowerLogin: true,
|
||||||
// SSL
|
// SSL
|
||||||
imapSslVerify_peer: false,
|
imapSslVerify_peer: false,
|
||||||
imapSslAllow_self_signed: false,
|
imapSslAllow_self_signed: false,
|
||||||
|
|
@ -50,6 +51,7 @@ const
|
||||||
smtpType: 0,
|
smtpType: 0,
|
||||||
smtpTimeout: 60,
|
smtpTimeout: 60,
|
||||||
smtpShortLogin: false,
|
smtpShortLogin: false,
|
||||||
|
smtpLowerLogin: true,
|
||||||
smtpUseAuth: true,
|
smtpUseAuth: true,
|
||||||
smtpSetSender: false,
|
smtpSetSender: false,
|
||||||
smtpAuthPlainLine: false,
|
smtpAuthPlainLine: false,
|
||||||
|
|
@ -69,6 +71,7 @@ const
|
||||||
secure: pInt(oDomain.imapType()),
|
secure: pInt(oDomain.imapType()),
|
||||||
timeout: oDomain.imapTimeout,
|
timeout: oDomain.imapTimeout,
|
||||||
shortLogin: !!oDomain.imapShortLogin(),
|
shortLogin: !!oDomain.imapShortLogin(),
|
||||||
|
lowerLogin: !!oDomain.imapLowerLogin(),
|
||||||
ssl: {
|
ssl: {
|
||||||
verify_peer: !!oDomain.imapSslVerify_peer(),
|
verify_peer: !!oDomain.imapSslVerify_peer(),
|
||||||
verify_peer_name: !!oDomain.imapSslVerify_peer(),
|
verify_peer_name: !!oDomain.imapSslVerify_peer(),
|
||||||
|
|
@ -92,6 +95,7 @@ const
|
||||||
secure: pInt(oDomain.smtpType()),
|
secure: pInt(oDomain.smtpType()),
|
||||||
timeout: oDomain.smtpTimeout,
|
timeout: oDomain.smtpTimeout,
|
||||||
shortLogin: !!oDomain.smtpShortLogin(),
|
shortLogin: !!oDomain.smtpShortLogin(),
|
||||||
|
lowerLogin: !!oDomain.smtpLowerLogin(),
|
||||||
ssl: {
|
ssl: {
|
||||||
verify_peer: !!oDomain.smtpSslVerify_peer(),
|
verify_peer: !!oDomain.smtpSslVerify_peer(),
|
||||||
verify_peer_name: !!oDomain.smtpSslVerify_peer(),
|
verify_peer_name: !!oDomain.smtpSslVerify_peer(),
|
||||||
|
|
@ -109,6 +113,7 @@ const
|
||||||
secure: pInt(oDomain.sieveType()),
|
secure: pInt(oDomain.sieveType()),
|
||||||
timeout: oDomain.sieveTimeout,
|
timeout: oDomain.sieveTimeout,
|
||||||
shortLogin: !!oDomain.imapShortLogin(),
|
shortLogin: !!oDomain.imapShortLogin(),
|
||||||
|
lowerLogin: !!oDomain.imapLowerLogin(),
|
||||||
ssl: {
|
ssl: {
|
||||||
verify_peer: !!oDomain.imapSslVerify_peer(),
|
verify_peer: !!oDomain.imapSslVerify_peer(),
|
||||||
verify_peer_name: !!oDomain.imapSslVerify_peer(),
|
verify_peer_name: !!oDomain.imapSslVerify_peer(),
|
||||||
|
|
@ -365,6 +370,8 @@ export class DomainPopupView extends AbstractViewPopup {
|
||||||
this[key]?.(value);
|
this[key]?.(value);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
this.name(IDN.toUnicode(this.name()));
|
||||||
|
this.aliasName(IDN.toUnicode(this.aliasName()));
|
||||||
this.imapCapabilities(this.imapCapabilities.concat(this.imapDisabled_capabilities()).unique());
|
this.imapCapabilities(this.imapCapabilities.concat(this.imapDisabled_capabilities()).unique());
|
||||||
this.enableSmartPorts(true);
|
this.enableSmartPorts(true);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,12 @@ import { addObservablesTo, koComputable } from 'External/ko';
|
||||||
import Remote from 'Remote/User/Fetch';
|
import Remote from 'Remote/User/Fetch';
|
||||||
import { FolderUserStore } from 'Stores/User/Folder';
|
import { FolderUserStore } from 'Stores/User/Folder';
|
||||||
|
|
||||||
|
import { getFolderFromCacheList, setFolder, removeFolderFromCacheList } from 'Common/Cache';
|
||||||
|
import { Notifications } from 'Common/Enums';
|
||||||
|
import { FolderMetadataKeys } from 'Common/EnumsUser';
|
||||||
|
import { folderListOptionsBuilder, sortFolders } from 'Common/Folders';
|
||||||
|
import { initOnStartOrLangChange, i18n, getNotification } from 'Common/Translator';
|
||||||
import { defaultOptionsAfterRender } from 'Common/Utils';
|
import { defaultOptionsAfterRender } from 'Common/Utils';
|
||||||
import { folderListOptionsBuilder } from 'Common/Folders';
|
|
||||||
|
|
||||||
export class FolderPopupView extends AbstractViewPopup {
|
export class FolderPopupView extends AbstractViewPopup {
|
||||||
constructor() {
|
constructor() {
|
||||||
|
|
@ -28,6 +32,24 @@ export class FolderPopupView extends AbstractViewPopup {
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this.displaySpecSetting = FolderUserStore.displaySpecSetting;
|
||||||
|
|
||||||
|
this.showKolab = FolderUserStore.allowKolab();
|
||||||
|
this.kolabTypeOptions = ko.observableArray();
|
||||||
|
let i18nFilter = key => i18n('SETTINGS_FOLDERS/TYPE_' + key);
|
||||||
|
initOnStartOrLangChange(()=>{
|
||||||
|
this.kolabTypeOptions([
|
||||||
|
{ id: '', name: '' },
|
||||||
|
{ id: 'event', name: i18nFilter('CALENDAR') },
|
||||||
|
{ id: 'contact', name: i18nFilter('CONTACTS') },
|
||||||
|
{ id: 'task', name: i18nFilter('TASKS') },
|
||||||
|
{ id: 'note', name: i18nFilter('NOTES') },
|
||||||
|
{ id: 'file', name: i18nFilter('FILES') },
|
||||||
|
{ id: 'journal', name: i18nFilter('JOURNAL') },
|
||||||
|
{ id: 'configuration', name: i18nFilter('CONFIGURATION') }
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
this.defaultOptionsAfterRender = defaultOptionsAfterRender;
|
this.defaultOptionsAfterRender = defaultOptionsAfterRender;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -35,9 +57,81 @@ export class FolderPopupView extends AbstractViewPopup {
|
||||||
this.editing(false);
|
this.editing(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
submitForm(form) {
|
submitForm(/*form*/) {
|
||||||
this.folder().rename(this.name(), this.parentFolder());
|
const
|
||||||
console.dir({form});
|
folder = this.folder(),
|
||||||
|
nameToEdit = this.name().trim(),
|
||||||
|
newParentName = this.parentFolder(),
|
||||||
|
oldParent = getFolderFromCacheList(folder.parentName),
|
||||||
|
newParent = getFolderFromCacheList(newParentName),
|
||||||
|
folderList = FolderUserStore.folderList,
|
||||||
|
newFolderList = newParent ? newParent.subFolders : folderList,
|
||||||
|
delimiter = (newParent || folder).delimiter,
|
||||||
|
oldFullname = folder.fullName,
|
||||||
|
newFullname = (newParent ? newParentName + delimiter : '') + nameToEdit;
|
||||||
|
if (nameToEdit && newFullname != oldFullname) {
|
||||||
|
Remote.abort('Folders').post('FolderRename', FolderUserStore.foldersRenaming, {
|
||||||
|
oldName: oldFullname,
|
||||||
|
newName: newFullname,
|
||||||
|
// toggleFolderSubscription / FolderSubscribe
|
||||||
|
subscribe: folder.isSubscribed() ? 1 : 0,
|
||||||
|
// toggleFolderCheckable / FolderCheckable
|
||||||
|
checkable: folder.checkable() ? 1 : 0,
|
||||||
|
// toggleFolderKolabType / FolderSetMetadata
|
||||||
|
kolab: {
|
||||||
|
// TODO: append '.default' ?
|
||||||
|
type: FolderMetadataKeys.KolabFolderType,
|
||||||
|
value: folder.kolabType()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
const
|
||||||
|
renameFolder = (folder, parent) => {
|
||||||
|
removeFolderFromCacheList(folder.fullName);
|
||||||
|
folder.parentName = (parent ? parent.fullName : '');
|
||||||
|
folder.fullName = (parent ? parent.fullName + delimiter : '') + folder.name();
|
||||||
|
folder.delimiter = delimiter;
|
||||||
|
folder.deep = (parent ? parent.deep : -1) + 1;
|
||||||
|
setFolder(folder);
|
||||||
|
},
|
||||||
|
renameChildren = folder => {
|
||||||
|
folder.subFolders.forEach(child => {
|
||||||
|
renameFolder(child, folder);
|
||||||
|
renameChildren(child);
|
||||||
|
})
|
||||||
|
};
|
||||||
|
folder.name(nameToEdit);
|
||||||
|
renameFolder(folder, newParent);
|
||||||
|
if (folder.subFolders.length || newParent != oldParent) {
|
||||||
|
// Rename all subfolders to prevent reload
|
||||||
|
renameChildren(folder);
|
||||||
|
}
|
||||||
|
(oldParent ? oldParent.subFolders : folderList).remove(folder);
|
||||||
|
newFolderList.push(folder);
|
||||||
|
sortFolders(newFolderList);
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error(error);
|
||||||
|
FolderUserStore.error(
|
||||||
|
getNotification(error.code, '', Notifications.CantRenameFolder) + '.\n' + error.message
|
||||||
|
);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
Remote.request('FolderSettings', null, {
|
||||||
|
folder: folder.fullName,
|
||||||
|
// toggleFolderSubscription / FolderSubscribe
|
||||||
|
subscribe: folder.isSubscribed() ? 1 : 0,
|
||||||
|
// toggleFolderCheckable / FolderCheckable
|
||||||
|
checkable: folder.checkable() ? 1 : 0,
|
||||||
|
// toggleFolderKolabType / FolderSetMetadata
|
||||||
|
kolab: {
|
||||||
|
// TODO: append '.default' ?
|
||||||
|
type: FolderMetadataKeys.KolabFolderType,
|
||||||
|
value: folder.kolabType()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
this.close();
|
this.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,7 @@ export class OpenPgpGeneratePopupView extends AbstractViewPopup {
|
||||||
const type = this.keyType().toLowerCase(),
|
const type = this.keyType().toLowerCase(),
|
||||||
userId = {
|
userId = {
|
||||||
name: this.name(),
|
name: this.name(),
|
||||||
email: this.email()
|
email: IDN.toASCII(this.email())
|
||||||
},
|
},
|
||||||
cfg = {
|
cfg = {
|
||||||
type: type,
|
type: type,
|
||||||
|
|
|
||||||
|
|
@ -120,7 +120,7 @@ export class LoginUserView extends AbstractViewLogin {
|
||||||
iError = Notifications.AuthError;
|
iError = Notifications.AuthError;
|
||||||
}
|
}
|
||||||
this.submitError(getNotification(iError, oData?.ErrorMessage,
|
this.submitError(getNotification(iError, oData?.ErrorMessage,
|
||||||
Notifications.UnknownNotification));
|
Notifications.UnknownError));
|
||||||
this.submitErrorAdditional(oData?.ErrorMessageAdditional);
|
this.submitErrorAdditional(oData?.ErrorMessageAdditional);
|
||||||
} else {
|
} else {
|
||||||
rl.setData(oData.Result);
|
rl.setData(oData.Result);
|
||||||
|
|
@ -138,25 +138,20 @@ export class LoginUserView extends AbstractViewLogin {
|
||||||
onBuild(dom) {
|
onBuild(dom) {
|
||||||
super.onBuild(dom);
|
super.onBuild(dom);
|
||||||
|
|
||||||
const signMe = (SettingsGet('signMe') || '').toLowerCase();
|
let signMe = SettingsGet('signMe');
|
||||||
|
|
||||||
switch (signMe) {
|
switch (signMe) {
|
||||||
case 'defaultoff':
|
case SignMeOff:
|
||||||
case 'defaulton':
|
case SignMeOn:
|
||||||
this.signMeType(
|
|
||||||
'defaulton' === signMe ? SignMeOn : SignMeOff
|
|
||||||
);
|
|
||||||
|
|
||||||
switch (Local.get(ClientSideKeyNameLastSignMe)) {
|
switch (Local.get(ClientSideKeyNameLastSignMe)) {
|
||||||
case '-1-':
|
case '-1-':
|
||||||
this.signMeType(SignMeOn);
|
signMe = SignMeOn;
|
||||||
break;
|
break;
|
||||||
case '-0-':
|
case '-0-':
|
||||||
this.signMeType(SignMeOff);
|
signMe = SignMeOff;
|
||||||
break;
|
break;
|
||||||
// no default
|
// no default
|
||||||
}
|
}
|
||||||
|
this.signMeType(signMe);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
this.signMeType(SignMeUnused);
|
this.signMeType(SignMeUnused);
|
||||||
|
|
|
||||||
|
|
@ -146,7 +146,7 @@ export class MailMessageView extends AbstractViewRight {
|
||||||
|
|
||||||
downloadAsZipAllowed: () => this.attachmentsActions.includes('zip')
|
downloadAsZipAllowed: () => this.attachmentsActions.includes('zip')
|
||||||
&& (currentMessage()?.attachments || [])
|
&& (currentMessage()?.attachments || [])
|
||||||
.filter(item => item?.download /*&& !item?.isLinked()*/ && item?.checked())
|
.filter(item => item?.checked() && item?.download /*&& !item?.isLinked()*/)
|
||||||
.length,
|
.length,
|
||||||
|
|
||||||
tagsAllowed: () => FolderUserStore.currentFolder()?.tagsAllowed(),
|
tagsAllowed: () => FolderUserStore.currentFolder()?.tagsAllowed(),
|
||||||
|
|
@ -570,29 +570,31 @@ export class MailMessageView extends AbstractViewRight {
|
||||||
}
|
}
|
||||||
|
|
||||||
pgpDecrypt() {
|
pgpDecrypt() {
|
||||||
const oMessage = currentMessage();
|
const oMessage = currentMessage(),
|
||||||
|
data = oMessage.pgpEncrypted();
|
||||||
|
delete data.error;
|
||||||
PgpUserStore.decrypt(oMessage).then(result => {
|
PgpUserStore.decrypt(oMessage).then(result => {
|
||||||
if (result) {
|
if (!result) {
|
||||||
oMessage.pgpDecrypted(true);
|
|
||||||
if (result.data) {
|
|
||||||
MimeToMessage(result.data, oMessage);
|
|
||||||
oMessage.html() ? oMessage.viewHtml() : oMessage.viewPlain();
|
|
||||||
if (result.signatures?.length) {
|
|
||||||
oMessage.pgpSigned(true);
|
|
||||||
oMessage.pgpVerified({
|
|
||||||
signatures: result.signatures,
|
|
||||||
success: !!result.signatures.length
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// TODO: translate
|
// TODO: translate
|
||||||
throw 'Decryption failed, canceled or not possible';
|
throw Error('Decryption failed, canceled or not possible');
|
||||||
|
}
|
||||||
|
oMessage.pgpDecrypted(true);
|
||||||
|
if (result.data) {
|
||||||
|
MimeToMessage(result.data, oMessage);
|
||||||
|
oMessage.html() ? oMessage.viewHtml() : oMessage.viewPlain();
|
||||||
|
if (result.signatures?.length) {
|
||||||
|
oMessage.pgpSigned({
|
||||||
|
signatures: result.signatures,
|
||||||
|
success: !!result.signatures.length
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(e => {
|
.catch(e => {
|
||||||
console.error(e)
|
data.error = e.message;
|
||||||
alert(e.message);
|
})
|
||||||
|
.finally(() => {
|
||||||
|
oMessage.pgpEncrypted(data);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -600,7 +602,7 @@ export class MailMessageView extends AbstractViewRight {
|
||||||
const oMessage = currentMessage()/*, ctrl = event.target.closest('.openpgp-control')*/;
|
const oMessage = currentMessage()/*, ctrl = event.target.closest('.openpgp-control')*/;
|
||||||
PgpUserStore.verify(oMessage).then(result => {
|
PgpUserStore.verify(oMessage).then(result => {
|
||||||
if (result) {
|
if (result) {
|
||||||
oMessage.pgpVerified(result);
|
oMessage.pgpSigned(result);
|
||||||
} else {
|
} else {
|
||||||
alert('Verification failed or no valid public key found');
|
alert('Verification failed or no valid public key found');
|
||||||
}
|
}
|
||||||
|
|
@ -627,16 +629,17 @@ export class MailMessageView extends AbstractViewRight {
|
||||||
|
|
||||||
async smimeDecrypt() {
|
async smimeDecrypt() {
|
||||||
const message = currentMessage();
|
const message = currentMessage();
|
||||||
let pass, data = message.smimeEncrypted(); // { partId: "1" }
|
|
||||||
const addresses = message.from.concat(message.to, message.cc, message.bcc).map(item => item.email),
|
const addresses = message.from.concat(message.to, message.cc, message.bcc).map(item => item.email),
|
||||||
identity = IdentityUserStore.find(item => addresses.includes(item.email()));
|
identity = IdentityUserStore.find(item => addresses.includes(item.email())),
|
||||||
|
data = message.smimeEncrypted(); // { partId: "1" }
|
||||||
if (data && identity) {
|
if (data && identity) {
|
||||||
data = { ...data }; // clone
|
delete data.error;
|
||||||
data.folder = message.folder;
|
let pass, params = { ...data }; // clone
|
||||||
data.uid = message.uid;
|
params.folder = message.folder;
|
||||||
// data.bodyPart = data.bodyPart?.raw;
|
params.uid = message.uid;
|
||||||
data.certificate = identity.smimeCertificate();
|
// params.bodyPart = params.bodyPart?.raw;
|
||||||
data.privateKey = identity.smimeKey();
|
params.certificate = identity.smimeCertificate();
|
||||||
|
params.privateKey = identity.smimeKey();
|
||||||
if (identity.smimeKeyEncrypted()) {
|
if (identity.smimeKeyEncrypted()) {
|
||||||
pass = await Passphrases.ask(identity,
|
pass = await Passphrases.ask(identity,
|
||||||
i18n('SMIME/PRIVATE_KEY_OF', {EMAIL: identity.email()}),
|
i18n('SMIME/PRIVATE_KEY_OF', {EMAIL: identity.email()}),
|
||||||
|
|
@ -645,36 +648,44 @@ export class MailMessageView extends AbstractViewRight {
|
||||||
if (!pass) {
|
if (!pass) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
data.passphrase = pass?.password;
|
params.passphrase = pass?.password;
|
||||||
}
|
}
|
||||||
Remote.post('SMimeDecryptMessage', null, data).then(response => {
|
Remote.post('SMimeDecryptMessage', null, params).then(response => {
|
||||||
if (response?.Result) {
|
if (response?.Result?.data) {
|
||||||
message.smimeDecrypted(true);
|
message.smimeDecrypted(true);
|
||||||
MimeToMessage(response.Result, message);
|
MimeToMessage(response.Result.data, message);
|
||||||
message.html() ? message.viewHtml() : message.viewPlain();
|
message.html() ? message.viewHtml() : message.viewPlain();
|
||||||
pass && pass.remember && Passphrases.set(identity, pass.password);
|
pass && pass.remember && Passphrases.set(identity, pass.password);
|
||||||
|
if ('signed' in response.Result) {
|
||||||
|
message.smimeSigned(response.Result.signed);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}).catch(e => {
|
||||||
|
data.error = e.message
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
message.smimeEncrypted(data);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
smimeVerify(/*self, event*/) {
|
smimeVerify(/*self, event*/) {
|
||||||
const message = currentMessage();
|
const message = currentMessage(),
|
||||||
let data = message.smimeSigned(); // { partId: "1", micAlg: "pgp-sha256" }
|
data = message.smimeSigned(); // { partId: "1", micAlg: "pgp-sha256" }
|
||||||
if (data) {
|
if (data) {
|
||||||
data = { ...data }; // clone
|
const params = { ...data }; // clone
|
||||||
data.folder = message.folder;
|
params.folder = message.folder;
|
||||||
data.uid = message.uid;
|
params.uid = message.uid;
|
||||||
data.bodyPart = data.bodyPart?.raw;
|
params.bodyPart = data.bodyPart?.raw;
|
||||||
data.sigPart = data.sigPart?.bodyRaw;
|
params.sigPart = data.sigPart?.bodyRaw;
|
||||||
Remote.post('SMimeVerifyMessage', null, data).then(response => {
|
Remote.post('SMimeVerifyMessage', null, params).then(response => {
|
||||||
if (response?.Result) {
|
if (response?.Result) {
|
||||||
if (response.Result.body) {
|
if (response.Result.body) {
|
||||||
MimeToMessage(response.Result.body, message);
|
MimeToMessage(response.Result.body, message);
|
||||||
message.html() ? message.viewHtml() : message.viewPlain();
|
message.html() ? message.viewHtml() : message.viewPlain();
|
||||||
response.Result.body = null;
|
|
||||||
}
|
}
|
||||||
message.smimeVerified(response.Result.success);
|
data.success = response.Result.success;
|
||||||
|
message.smimeSigned(data);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -87,9 +87,8 @@ export class SystemDropDownUserView extends AbstractViewRight {
|
||||||
}
|
}
|
||||||
|
|
||||||
accountName() {
|
accountName() {
|
||||||
let email = AccountUserStore.email(),
|
const email = AccountUserStore.email();
|
||||||
account = AccountUserStore.find(account => account.email == email);
|
return AccountUserStore.find(account => account.email == email)?.label() || IDN.toUnicode(email);
|
||||||
return account?.name || email;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
settingsClick() {
|
settingsClick() {
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
const
|
const
|
||||||
qUri = path => doc.location.pathname.replace(/\/+$/,'') + '/?/' + path,
|
qUri = path => doc.location.pathname.replace(/\/+$/,'') + '/?/' + path,
|
||||||
eId = id => doc.getElementById('rl-'+id),
|
eId = id => doc.getElementById('rl-'+id),
|
||||||
admin = '1' == eId('app')?.dataset?.admin,
|
admin = '1' == eId('app').dataset.admin,
|
||||||
|
|
||||||
toggle = div => {
|
toggle = div => {
|
||||||
eId('loading').hidden = true;
|
eId('loading').hidden = true;
|
||||||
|
|
@ -69,7 +69,7 @@ window.rl = {
|
||||||
headers: {}
|
headers: {}
|
||||||
}, init);
|
}, init);
|
||||||
let asJSON = 1,
|
let asJSON = 1,
|
||||||
XToken = RL_APP_DATA.System?.token,
|
XToken = (RL_APP_DATA.System || {}).token,
|
||||||
object = {};
|
object = {};
|
||||||
if (postData) {
|
if (postData) {
|
||||||
init.method = 'POST';
|
init.method = 'POST';
|
||||||
|
|
|
||||||
106
examples/crypto/3140898548F06E97-private.asc
Normal file
106
examples/crypto/3140898548F06E97-private.asc
Normal file
|
|
@ -0,0 +1,106 @@
|
||||||
|
-----BEGIN PGP PRIVATE KEY BLOCK-----
|
||||||
|
|
||||||
|
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
|
||||||
|
YXYPCv6njzAv1+fTgemAZYyomQ1qhVg=
|
||||||
|
=a9os
|
||||||
|
-----END PGP PRIVATE KEY BLOCK-----
|
||||||
50
examples/crypto/3140898548F06E97-public.asc
Normal file
50
examples/crypto/3140898548F06E97-public.asc
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||||
|
|
||||||
|
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
|
||||||
|
=0ljX
|
||||||
|
-----END PGP PUBLIC KEY BLOCK-----
|
||||||
16
examples/crypto/5F3A5CDC09AD8AE3-private.asc
Normal file
16
examples/crypto/5F3A5CDC09AD8AE3-private.asc
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
-----BEGIN PGP PRIVATE KEY BLOCK-----
|
||||||
|
|
||||||
|
lIYEYfQDqBYJKwYBBAHaRw8BAQdAy+llquGb/U4M0kD2xyJoQ2pwlDN02C7XD906
|
||||||
|
7I8zdB3+BwMC7y2sGFhuzTHldCm0gfdOH3H2PkmhDfM/OSTYGVEu1mKInC3xmp2F
|
||||||
|
HmC2wUd+yM+hzMrQMgHn61iT9CHDeunUhQqG8PoXfAUAL7syGiXQ0rQZZGVtbyA8
|
||||||
|
ZGVtb0BzbmFwcHltYWlsLmV1PoiMBBAWCgAdBQJh9AOoBAsJBwgDFQgKBBYAAgEC
|
||||||
|
GQECGwMCHgEAIQkQXzpc3AmtiuMWIQQsIj8g6irbTLaPgdlfOlzcCa2K4/nPAP4u
|
||||||
|
UrWr39wv+YKsNcLwHwOpljyu59iHOXA3halUbVCeJwD/dY6JXCwMDgG+BmurPcJh
|
||||||
|
S/S8Q6fjlN9hUi/za3acYASciwRh9AOoEgorBgEEAZdVAQUBAQdAvXl+RCkqtUqN
|
||||||
|
VQ3Fj3bFTZjZOeNlI3ibK2eN6EjlnwcDAQgH/gcDAjAzqrbZEDg05XxnZqG/vHCg
|
||||||
|
1bJ/OofU+0oYZy6DHhAVDa5IhNLkC7kDeA8gXtQnNVuOd5kSIWvLcVRMZZval8iI
|
||||||
|
Xfrt037sAAciOIft1uSIeAQYFggACQUCYfQDqAIbDAAhCRBfOlzcCa2K4xYhBCwi
|
||||||
|
PyDqKttMto+B2V86XNwJrYrjolQBAMqdz8VkgMYjM7tinwUUTe4JjZoCsuhHPN6S
|
||||||
|
pQLd/UzKAQDgRlbAdrl042/nJcdrBrQz3+wVzkaF0ehvihBf4/tfDw==
|
||||||
|
=5A1c
|
||||||
|
-----END PGP PRIVATE KEY BLOCK-----
|
||||||
13
examples/crypto/5F3A5CDC09AD8AE3-public.asc
Normal file
13
examples/crypto/5F3A5CDC09AD8AE3-public.asc
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||||
|
|
||||||
|
mDMEYfQDqBYJKwYBBAHaRw8BAQdAy+llquGb/U4M0kD2xyJoQ2pwlDN02C7XD906
|
||||||
|
7I8zdB20GWRlbW8gPGRlbW9Ac25hcHB5bWFpbC5ldT6IjAQQFgoAHQUCYfQDqAQL
|
||||||
|
CQcIAxUICgQWAAIBAhkBAhsDAh4BACEJEF86XNwJrYrjFiEELCI/IOoq20y2j4HZ
|
||||||
|
Xzpc3AmtiuP5zwD+LlK1q9/cL/mCrDXC8B8DqZY8rufYhzlwN4WpVG1QnicA/3WO
|
||||||
|
iVwsDA4BvgZrqz3CYUv0vEOn45TfYVIv82t2nGAEuDgEYfQDqBIKKwYBBAGXVQEF
|
||||||
|
AQEHQL15fkQpKrVKjVUNxY92xU2Y2TnjZSN4mytnjehI5Z8HAwEIB4h4BBgWCAAJ
|
||||||
|
BQJh9AOoAhsMACEJEF86XNwJrYrjFiEELCI/IOoq20y2j4HZXzpc3AmtiuOiVAEA
|
||||||
|
yp3PxWSAxiMzu2KfBRRN7gmNmgKy6Ec83pKlAt39TMoBAOBGVsB2uXTjb+clx2sG
|
||||||
|
tDPf7BXORoXR6G+KEF/j+18P
|
||||||
|
=fymh
|
||||||
|
-----END PGP PUBLIC KEY BLOCK-----
|
||||||
18
examples/crypto/A0B428E12402D875-private.asc
Normal file
18
examples/crypto/A0B428E12402D875-private.asc
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
-----BEGIN PGP PRIVATE KEY BLOCK-----
|
||||||
|
|
||||||
|
xYYEZfCT4RYJKwYBBAHaRw8BAQdABPuO2YQhhYx3HV4l8NGfC7TAUICCXYPB
|
||||||
|
B2YAgTIDR1j+CQMIGrTjdLMkuHvgN0hXM6160YQj51KfsJrY2uWtwTtjiJa7
|
||||||
|
CgyvXj8krDEGrtdTWL0aGGZN+LpMbieyVPstLjycdxEMx0VwT5VOaMZX68As
|
||||||
|
vs0vU25hcHB5TWFpbCBEZW1vMyA8ZGVtbzNAeG4tLWR1OGguc25hcHB5bWFp
|
||||||
|
bC5ldT7CjAQQFgoAPgUCZfCT4QQLCQcICRCgtCjhJALYdQMVCAoEFgACAQIZ
|
||||||
|
AQIbAwIeARYhBA2rP+d/WzbDEuhm0aC0KOEkAth1AAA7fwEA2spNcx/tdZz8
|
||||||
|
yBxsTbaOzWXrPa2RnU24QWI3nKiH65IA/j2UV2O6QwoB9PVoLLhqdsP0w7HH
|
||||||
|
msAcOY47Kt/rkqIHx4sEZfCT4RIKKwYBBAGXVQEFAQEHQB7lSl1px2GgNclu
|
||||||
|
DDFneXLe+ZjQAY6OvK23ti0zaCsFAwEIB/4JAwiaLdYHtWW/YOCOgzSJiwKU
|
||||||
|
4zmfRHIkkbCVyJO19Fpc4DwLx8dMKyPBQbLT0X7JI6SnR9qYPm35pJj2Cbrl
|
||||||
|
GNk3bImoPkcLVrwkrbUo+33gwngEGBYIACoFAmXwk+EJEKC0KOEkAth1AhsM
|
||||||
|
FiEEDas/539bNsMS6GbRoLQo4SQC2HUAAD+5APwO89z/dHpWj2GWRoLIbGsM
|
||||||
|
0zJcy7rldxp7FD+joiMOTQD/TnMrX8vwJCG/RMKzEVY02EnTScACrCzWIoLl
|
||||||
|
SxgrqQo=
|
||||||
|
=SBKt
|
||||||
|
-----END PGP PRIVATE KEY BLOCK-----
|
||||||
14
examples/crypto/A0B428E12402D875-public.asc
Normal file
14
examples/crypto/A0B428E12402D875-public.asc
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||||
|
|
||||||
|
xjMEZfCT4RYJKwYBBAHaRw8BAQdABPuO2YQhhYx3HV4l8NGfC7TAUICCXYPB
|
||||||
|
B2YAgTIDR1jNL1NuYXBweU1haWwgRGVtbzMgPGRlbW8zQHhuLS1kdThoLnNu
|
||||||
|
YXBweW1haWwuZXU+wowEEBYKAD4FAmXwk+EECwkHCAkQoLQo4SQC2HUDFQgK
|
||||||
|
BBYAAgECGQECGwMCHgEWIQQNqz/nf1s2wxLoZtGgtCjhJALYdQAAO38BANrK
|
||||||
|
TXMf7XWc/MgcbE22js1l6z2tkZ1NuEFiN5yoh+uSAP49lFdjukMKAfT1aCy4
|
||||||
|
anbD9MOxx5rAHDmOOyrf65KiB844BGXwk+ESCisGAQQBl1UBBQEBB0Ae5Upd
|
||||||
|
acdhoDXJbgwxZ3ly3vmY0AGOjrytt7YtM2grBQMBCAfCeAQYFggAKgUCZfCT
|
||||||
|
4QkQoLQo4SQC2HUCGwwWIQQNqz/nf1s2wxLoZtGgtCjhJALYdQAAP7kA/A7z
|
||||||
|
3P90elaPYZZGgshsawzTMlzLuuV3GnsUP6OiIw5NAP9Ocytfy/AkIb9EwrMR
|
||||||
|
VjTYSdNJwAKsLNYiguVLGCupCg==
|
||||||
|
=FuIG
|
||||||
|
-----END PGP PUBLIC KEY BLOCK-----
|
||||||
34
examples/crypto/smime.crt
Normal file
34
examples/crypto/smime.crt
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
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
|
||||||
|
r7HREQ/4hof+B0bTZCma/l0n
|
||||||
|
-----END CERTIFICATE-----
|
||||||
54
examples/crypto/smime.key
Normal file
54
examples/crypto/smime.key
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
-----BEGIN ENCRYPTED PRIVATE KEY-----
|
||||||
|
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
|
||||||
|
/ZYVVu8GG0wUcrvQizogAg==
|
||||||
|
-----END ENCRYPTED PRIVATE KEY-----
|
||||||
1745
examples/eml/tnef.eml
Normal file
1745
examples/eml/tnef.eml
Normal file
File diff suppressed because it is too large
Load diff
101
examples/eml/tnef2.eml
Normal file
101
examples/eml/tnef2.eml
Normal file
|
|
@ -0,0 +1,101 @@
|
||||||
|
From: Alice <alice@example.net>
|
||||||
|
To: Bob <bob@example.net>
|
||||||
|
Subject: TNEF test 2
|
||||||
|
Date: Thu, 10 Feb 2022 12:19:57 +0100
|
||||||
|
Message-ID: <001d01d81e70$23211250$696336f0$@example.net>
|
||||||
|
MIME-Version: 1.0
|
||||||
|
Content-Type: multipart/mixed;
|
||||||
|
boundary="----=_NextPart_000_001E_01D81E78.84E7EB50"
|
||||||
|
X-Mailer: Microsoft Outlook 16.0
|
||||||
|
Content-Language: cs
|
||||||
|
X-MS-TNEF-Correlator: 0000000045EA84AB25A1494B8CD618FC27E0940E04CA7500
|
||||||
|
Authentication-Results: example.net;
|
||||||
|
dkim=pass header.d=example.net header.s=mail header.b=UH8A3Z8P;
|
||||||
|
dmarc=pass (policy=reject) header.from=example.net;
|
||||||
|
spf=pass () smtp.mailfrom=example@example.net
|
||||||
|
|
||||||
|
This is a multipart message in MIME format.
|
||||||
|
|
||||||
|
------=_NextPart_000_001E_01D81E78.84E7EB50
|
||||||
|
Content-Type: text/plain;
|
||||||
|
charset="iso-8859-2"
|
||||||
|
Content-Transfer-Encoding: quoted-printable
|
||||||
|
|
||||||
|
Dobr=FD den,
|
||||||
|
|
||||||
|
=20
|
||||||
|
|
||||||
|
pos=EDl=E1m zku=B9ebn=ED e-mail:
|
||||||
|
|
||||||
|
=20
|
||||||
|
|
||||||
|
P=F8=EDli=B9 =BElu=BBou=E8k=FD k=F9=F2 =FAp=ECl =EF=E1belsk=E9 =F3dy
|
||||||
|
|
||||||
|
=20
|
||||||
|
|
||||||
|
S pozdravem
|
||||||
|
|
||||||
|
=20
|
||||||
|
|
||||||
|
Mgr. Milo=B9 Bart=E1k
|
||||||
|
|
||||||
|
|
||||||
|
------=_NextPart_000_001E_01D81E78.84E7EB50
|
||||||
|
Content-Type: application/ms-tnef;
|
||||||
|
name="winmail.dat"
|
||||||
|
Content-Transfer-Encoding: base64
|
||||||
|
Content-Disposition: attachment;
|
||||||
|
filename="winmail.dat"
|
||||||
|
|
||||||
|
eJ8+IjoLAQaQCAAEAAAAAAABAAEAAQeQBgAIAAAA4gQAAAAAAADmAAEIgAcAGAAAAElQTS5NaWNy
|
||||||
|
b3NvZnQgTWFpbC5Ob3RlADEIAQOQBgCMCgAAJwAAAAsAAgABAAAAAwAmAAAAAAALACkAAAAAAB4A
|
||||||
|
cAABAAAAHAAAAFprb3Waa2EgZS1tYWlsdSBiZXogcPjtbG9oeQACAXEAAQAAABYAAAAB2B5wHowu
|
||||||
|
HckFefFLTYHQP0bV2A3UAAALAAEOAAAAAAIBCg4BAAAAGAAAAAAAAABF6oSrJaFJS4zWGPwn4JQO
|
||||||
|
woAAAAMAFA4BAAAAHgAoDgEAAAAvAAAAMDAwMDAwMDMBYmFydGFrQGVzcC1tYWlsLmN6AWJhcnRh
|
||||||
|
a0Blc3AtbWFpbC5jegAAHgApDgEAAAAvAAAAMDAwMDAwMDMBYmFydGFrQGVzcC1tYWlsLmN6AWJh
|
||||||
|
cnRha0Blc3AtbWFpbC5jegAAAwDeP7BvAAADAPE/BQQAAAMAAlkAABYAAwAJWQIAAAALABCACCAG
|
||||||
|
AAAAAADAAAAAAAAARgAAAAADhQAAAAAAAAMAEoAIIAYAAAAAAMAAAAAAAABGAAAAABCFAAAAAAAA
|
||||||
|
AwBzgAggBgAAAAAAwAAAAAAAAEYAAAAAAYUAAAAAAAALAKmACCAGAAAAAADAAAAAAAAARgAAAAAG
|
||||||
|
hQAAAAAAAAsArYAIIAYAAAAAAMAAAAAAAABGAAAAAA6FAAAAAAAAAwCwgAggBgAAAAAAwAAAAAAA
|
||||||
|
AEYAAAAAGIUAAAAAAAALAMaACCAGAAAAAADAAAAAAAAARgAAAACChQAAAAAAAAMAAIEIIAYAAAAA
|
||||||
|
AMAAAAAAAABGAAAAAOuFAAAFBAAAAgELggggBgAAAAAAwAAAAAAAAEYBAAAANgAAAEkAbgBUAHIA
|
||||||
|
YQBuAHMAaQB0AE0AZQBzAHMAYQBnAGUAQwBvAHIAcgBlAGwAYQB0AG8AcgAAAAAAAQAAABAAAAAW
|
||||||
|
G8UG6IOCQIQ5QUZYFGJYHgAxggggBgAAAAAAwAAAAAAAAEYBAAAAGAAAAEMAbwBtAHAAbwBzAGUA
|
||||||
|
VAB5AHAAZQAAAAEAAAAIAAAAbmV3TWFpbAALAB8OAQAAAAIB+A8BAAAAEAAAAEXqhKsloUlLjNYY
|
||||||
|
/CfglA4CAfoPAQAAABAAAABF6oSrJaFJS4zWGPwn4JQOAwD+DwUAAAACAQkQAQAAAFYGAABSBgAA
|
||||||
|
YREAAExaRnVvWyMSAwAKAHJjcGcxMjWDAtEDYWh0bWwxAzH4YmlkBAADMAEDAfcKgCcCpAPjAgBj
|
||||||
|
aArAc2XgdDIzOCAHEwKAEIPfAFAEVghVB7ISdTIC4xFYtwYABsMSdTMERhFXMBN8VxKDCO8J9zsZ
|
||||||
|
YjUJtDliOQojMTkzEnIMYGNnAFALCQFkMzYXoAulNHIgEIIqXA6yAZAOEDlkIDwOsiB4DtAAgDpI
|
||||||
|
dj0iCHBuOgTwaGJlAMBzLW0N4ANgc2pvAYAtBaBtIAAO0CJdH6VvIC8hOSFgZg3gZXskVSIWdyKv
|
||||||
|
I78loAWwZA0iFm0lwA6wdHA6L6ovJiUuIQcuIaEvJGRiLwHQMDQvDiAqoG0LIfcox3cs4C53My4B
|
||||||
|
BbBnL1RSL1JFhEMtDrI0MCI+EoONHpczHjAfUGVhZC59NDE2DvA8B4ABkCBuMmEHgD1HCfAEkGF0
|
||||||
|
rwWxBaACMAnwdCXATSEWBCBXKAEgMTUgKG8kgB7gBJAJgCAHgA9QdRxtKS5uHiAw8XN0eYxsZTU/
|
||||||
|
MPEhLS0Ko5AvKiBGAiEgRAEQMQuAaXRpAiAEICovpQqjQAISLWYA0GUKpBsBkQMwezpFIQBseTp0
|
||||||
|
IkMxgGIHIQXQMhBoLiIagDrnCrBuIUBlLdwxOhWQHjAz8DM+oR4w7jY+wT5xGoB9Oe86/zwD+Txg
|
||||||
|
bGk8kT0/PkMz4TPw3xWQRGIeMD9bOIFTNoI4/9EKwXAuTSFQTgWwAMC4bCwgQmBHig9QdkeID0DK
|
||||||
|
AMAtYAuAOjBjbRdCuUAzAJB6JLAxMS4eMAUwS1476EJjIixz7wBxTEAGckK5bSFQQHEZcMcmcCGA
|
||||||
|
C2BuZ3UfACSwoEVOLVVTP6ZzPeE6LkXiRSDwC3AZAHZawHBydnkxN0n7UHH1NnMtNoBwJLBVgBGg
|
||||||
|
AiD7B0AhknA+EU0PTh9PL0ExRxjjJaALgGRvdzKQeEdM8T/ER4JDaHBGQWF+dR7gVA9VGFrgVnAA
|
||||||
|
IC3/AiA8IFa/V89Y31A/UU8/8u8KsFGAM4MGYGM5YhxTQQaLTFMw4DJMwiA3OWajg2GKSsQ3MC44
|
||||||
|
NWbSf2ivaTM/pkkiZL9BFWRiOv9rSj+mOBA2zx8xKUA2jzeYMlsGkCBnMpBiIiA5KF0+PB+xPgqj
|
||||||
|
PG+/JhARgFWAAQFcMgQgdl4RezLBNMF0IhBSgA9AAMB4oSXAMTAyNiIQL3K1Ni9ygjfwWwnwD1Bm
|
||||||
|
Xd9uX3EPch9zJQtgeQhgBUBXdCpyt3UScHQcZDIQYX91YXXJent5sHZvLow2Mi+fL68fAA5AH0AG
|
||||||
|
4GR5SDCZYwE9QwXwQmBuayXAQCMwNTYzQ34xduGEZTk1NEYBwHTRb9LkPScn8i13MgApEDyQ7YIw
|
||||||
|
a4cAKAEnLnAeowAhfwMwYvJ1gR8wiDYXoB5bOb0/MDxJITJQC2AEED1rSu+IGQAAiX8e0zYvcXzg
|
||||||
|
i1QnR5eMf42DRG88kFwndwVwSRAJ8Cx3jS9xcyBw/XduNRWQftKTYYg2CqKIJ/8KcohHCrGWeInt
|
||||||
|
AcBvkZNuX46vj7+Xn5KvHnk4HjAmnG5iUoACgIg4J2EBQP+cb5QflS+WP5dPmF+Zb5p/f5uPpXRW
|
||||||
|
cZGQCYAZUKrgMRBtIHprDHAnOWFsZWILkKrhID4wUzI6/5z/ng+h/6MPpB+lL6Y/p0//qF+pb6Ef
|
||||||
|
rc+e75//t3+vL3+wP7FPu3+zb7R/tY+2nyB6UJGROKrTAKCrwbsROY960KuiWqCroWU4a5GT/8di
|
||||||
|
G1CRoBWQkZFzYKrRi1BXuxEBEKsiYnrQc8dhZfuJQZGgM4Owt7+4z7y/vc//vt+/78D/wg/DH8Qv
|
||||||
|
u9/Lf/+5r7q/1S/M383vzv/ZL9Efn9Iv0z/UTwYAqpF6ZDIA/nYmUNVv1n/ab9t/3I/dn//er9+/
|
||||||
|
4M/h39mP5K/XX9hv/+5f5g/nH+gv8l/qT+tf7G//7X8eTPDRg4D66ztR+78e1L40EgA2YD3hhkZM
|
||||||
|
CTRMw41aFCOJMC9gN0Q7Yj+vY0KEMIgK+5lNCcAuPNC7U1HGE0IKwFxgqzFrHlz/hOBvkj3hiBn1
|
||||||
|
KIn989L9WvcJP/9fAG83AZ8CrwO/+2//7s/lD/PIBx8ILwvJ9G/1f//2j/efL+4rQG+RSSEXfxuf
|
||||||
|
34NADWEK8IOhd243gfIuEQUucH0jAAAAAwAWEAAAAAADAA00/T+lDgMADzT9P6UOAgEUNAEAAAAQ
|
||||||
|
AAAATklUQfm/uAEAqgA32W4AAAIBfwABAAAAMQAAADAwMDAwMDAwNDVFQTg0QUIyNUExNDk0QjhD
|
||||||
|
RDYxOEZDMjdFMDk0MEUwNENBNzUwMAAAAAADAAYQuPK1DAMABxBYAAAAAwAQEAAAAAADABEQAAAA
|
||||||
|
AB4ACBABAAAAWQAAAERPQlL9REVOLFBPU+1M4U1aS1W5RUJO7UUtTUFJTDpQ+O1MSbm+TFW7T1Xo
|
||||||
|
S/1L+fL6UOxM7+FCRUxTS+nzRFlTUE9aRFJBVkVNTUdSTUlMT7lCQVJU4UsAAAAAvTM=
|
||||||
|
|
||||||
|
------=_NextPart_000_001E_01D81E78.84E7EB50--
|
||||||
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
This app packages SnappyMail <upstream>2.35.2</upstream>.
|
This app packages SnappyMail <upstream>2.35.4</upstream>.
|
||||||
|
|
||||||
SnappyMail is a simple, modern, lightweight & fast web-based email client.
|
SnappyMail is a simple, modern, lightweight & fast web-based email client.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ RUN mkdir -p /app/code
|
||||||
WORKDIR /app/code
|
WORKDIR /app/code
|
||||||
|
|
||||||
# If you change the extraction below, be sure to test on scaleway
|
# If you change the extraction below, be sure to test on scaleway
|
||||||
VERSION=2.35.2
|
VERSION=2.35.4
|
||||||
RUN wget https://github.com/the-djmaze/snappymail/releases/download/v${VERSION}/snappymail-${VERSION}.zip -O /tmp/snappymail.zip && \
|
RUN wget https://github.com/the-djmaze/snappymail/releases/download/v${VERSION}/snappymail-${VERSION}.zip -O /tmp/snappymail.zip && \
|
||||||
unzip /tmp/snappymail.zip -d /app/code && \
|
unzip /tmp/snappymail.zip -d /app/code && \
|
||||||
rm /tmp/snappymail.zip && \
|
rm /tmp/snappymail.zip && \
|
||||||
|
|
|
||||||
|
|
@ -3,10 +3,10 @@
|
||||||
<id>snappymail</id>
|
<id>snappymail</id>
|
||||||
<name>SnappyMail</name>
|
<name>SnappyMail</name>
|
||||||
<summary>SnappyMail Webmail</summary>
|
<summary>SnappyMail Webmail</summary>
|
||||||
<version>2.35.2</version>
|
<version>2.35.4</version>
|
||||||
<licence>agpl</licence>
|
<licence>agpl</licence>
|
||||||
<author>SnappyMail, RainLoop Team, Nextgen-Networks, Tab Fitts, Nathan Kinkade, Pierre-Alain Bandinelli</author>
|
<author>SnappyMail, RainLoop Team, Nextgen-Networks, Tab Fitts, Nathan Kinkade, Pierre-Alain Bandinelli</author>
|
||||||
<description><![CDATA[**Simple, modern, lightweight & fast web-based email client.**
|
<description><![CDATA[**Lightweight & fast email client.**
|
||||||
|
|
||||||
- **Dark mode**
|
- **Dark mode**
|
||||||
- **Responsive design**
|
- **Responsive design**
|
||||||
|
|
@ -16,6 +16,7 @@
|
||||||
- **Integration with other Nextcloud apps** (Contacts, Files and Calendar)
|
- **Integration with other Nextcloud apps** (Contacts, Files and Calendar)
|
||||||
- **Multiple mail accounts and identities**
|
- **Multiple mail accounts and identities**
|
||||||
- **Send & receive OpenPGP encrypted/signed emails** (With full HTML support, ECC keys, or plain text with Mailvelope)
|
- **Send & receive OpenPGP encrypted/signed emails** (With full HTML support, ECC keys, or plain text with Mailvelope)
|
||||||
|
- **Send & receive S/MIME encrypted/signed emails**
|
||||||
- **Many security features** (Sodium encrypted passwords, Sec-Fetch, TOTP 2FA, DKIM, prevent tracking, etc.)
|
- **Many security features** (Sodium encrypted passwords, Sec-Fetch, TOTP 2FA, DKIM, prevent tracking, etc.)
|
||||||
- **Kolab integrations**
|
- **Kolab integrations**
|
||||||
|
|
||||||
|
|
@ -63,7 +64,7 @@ There, click on the link to go to the SnappyMail admin panel.
|
||||||
<lib>xxtea</lib>
|
<lib>xxtea</lib>
|
||||||
<lib>zip</lib>
|
<lib>zip</lib>
|
||||||
-->
|
-->
|
||||||
<nextcloud min-version="20" max-version="28" />
|
<nextcloud min-version="20" max-version="29" />
|
||||||
</dependencies>
|
</dependencies>
|
||||||
<settings>
|
<settings>
|
||||||
<admin>OCA\SnappyMail\Settings\AdminSettings</admin>
|
<admin>OCA\SnappyMail\Settings\AdminSettings</admin>
|
||||||
|
|
|
||||||
|
|
@ -75,20 +75,24 @@ class Application extends App implements IBootstrap
|
||||||
|
|
||||||
$dispatcher = $context->getAppContainer()->query('OCP\EventDispatcher\IEventDispatcher');
|
$dispatcher = $context->getAppContainer()->query('OCP\EventDispatcher\IEventDispatcher');
|
||||||
$dispatcher->addListener(PostLoginEvent::class, function (PostLoginEvent $Event) {
|
$dispatcher->addListener(PostLoginEvent::class, function (PostLoginEvent $Event) {
|
||||||
|
/*
|
||||||
$config = \OC::$server->getConfig();
|
$config = \OC::$server->getConfig();
|
||||||
// Only store the user's password in the current session if they have
|
// Only store the user's password in the current session if they have
|
||||||
// enabled auto-login using Nextcloud username or email address.
|
// enabled auto-login using Nextcloud username or email address.
|
||||||
if ($config->getAppValue('snappymail', 'snappymail-autologin', false)
|
if ($config->getAppValue('snappymail', 'snappymail-autologin', false)
|
||||||
|| $config->getAppValue('snappymail', 'snappymail-autologin-with-email', false)) {
|
|| $config->getAppValue('snappymail', 'snappymail-autologin-with-email', false)) {
|
||||||
|
*/
|
||||||
$sUID = $Event->getUser()->getUID();
|
$sUID = $Event->getUser()->getUID();
|
||||||
\OC::$server->getSession()['snappymail-nc-uid'] = $sUID;
|
\OC::$server->getSession()['snappymail-nc-uid'] = $sUID;
|
||||||
\OC::$server->getSession()['snappymail-password'] = SnappyMailHelper::encodePassword($Event->getPassword(), $sUID);
|
\OC::$server->getSession()['snappymail-passphrase'] = SnappyMailHelper::encodePassword($Event->getPassword(), $sUID);
|
||||||
|
/*
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
});
|
});
|
||||||
|
|
||||||
$dispatcher->addListener(BeforeUserLoggedOutEvent::class, function (BeforeUserLoggedOutEvent $Event) {
|
$dispatcher->addListener(BeforeUserLoggedOutEvent::class, function (BeforeUserLoggedOutEvent $Event) {
|
||||||
// https://github.com/nextcloud/server/issues/36083#issuecomment-1387370634
|
// https://github.com/nextcloud/server/issues/36083#issuecomment-1387370634
|
||||||
// \OC::$server->getSession()['snappymail-password'] = '';
|
// \OC::$server->getSession()['snappymail-passphrase'] = '';
|
||||||
SnappyMailHelper::loadApp();
|
SnappyMailHelper::loadApp();
|
||||||
// \RainLoop\Api::Actions()->Logout(true);
|
// \RainLoop\Api::Actions()->Logout(true);
|
||||||
\RainLoop\Api::Actions()->DoLogout();
|
\RainLoop\Api::Actions()->DoLogout();
|
||||||
|
|
@ -99,12 +103,12 @@ class Application extends App implements IBootstrap
|
||||||
$class = 'OCA\Impersonate\Events\BeginImpersonateEvent';
|
$class = 'OCA\Impersonate\Events\BeginImpersonateEvent';
|
||||||
if (\class_exists($class)) {
|
if (\class_exists($class)) {
|
||||||
$dispatcher->addListener($class, function ($Event) {
|
$dispatcher->addListener($class, function ($Event) {
|
||||||
\OC::$server->getSession()['snappymail-password'] = '';
|
\OC::$server->getSession()['snappymail-passphrase'] = '';
|
||||||
SnappyMailHelper::loadApp();
|
SnappyMailHelper::loadApp();
|
||||||
\RainLoop\Api::Actions()->Logout(true);
|
\RainLoop\Api::Actions()->Logout(true);
|
||||||
});
|
});
|
||||||
$dispatcher->addListener('OCA\Impersonate\Events\EndImpersonateEvent', function ($Event) {
|
$dispatcher->addListener('OCA\Impersonate\Events\EndImpersonateEvent', function ($Event) {
|
||||||
\OC::$server->getSession()['snappymail-password'] = '';
|
\OC::$server->getSession()['snappymail-passphrase'] = '';
|
||||||
SnappyMailHelper::loadApp();
|
SnappyMailHelper::loadApp();
|
||||||
\RainLoop\Api::Actions()->Logout(true);
|
\RainLoop\Api::Actions()->Logout(true);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -103,7 +103,7 @@ class FetchController extends Controller {
|
||||||
|
|
||||||
$sPass = $_POST['snappymail-password'];
|
$sPass = $_POST['snappymail-password'];
|
||||||
if ('******' !== $sPass) {
|
if ('******' !== $sPass) {
|
||||||
$this->config->setUserValue($sUser, 'snappymail', 'snappymail-password',
|
$this->config->setUserValue($sUser, 'snappymail', 'passphrase',
|
||||||
$sPass ? SnappyMailHelper::encodePassword($sPass, \md5($sEmail)) : '');
|
$sPass ? SnappyMailHelper::encodePassword($sPass, \md5($sEmail)) : '');
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -85,11 +85,15 @@ class InstallStep implements IRepairStep
|
||||||
// $oDomain = \RainLoop\Model\Domain::fromIniArray('nextcloud', []);
|
// $oDomain = \RainLoop\Model\Domain::fromIniArray('nextcloud', []);
|
||||||
$oDomain = new \RainLoop\Model\Domain('nextcloud');
|
$oDomain = new \RainLoop\Model\Domain('nextcloud');
|
||||||
$iSecurityType = \MailSo\Net\Enumerations\ConnectionSecurityType::NONE;
|
$iSecurityType = \MailSo\Net\Enumerations\ConnectionSecurityType::NONE;
|
||||||
$oDomain->SetConfig(
|
$oDomain->ImapSettings()->host = 'localhost';
|
||||||
'localhost', 143, $iSecurityType, true,
|
$oDomain->ImapSettings()->type = $iSecurityType;
|
||||||
true, 'localhost', 4190, $iSecurityType,
|
$oDomain->ImapSettings()->shortLogin = true;
|
||||||
'localhost', 25, $iSecurityType, true, true, false, false,
|
$oDomain->SieveSettings()->enabled = true;
|
||||||
'');
|
$oDomain->SieveSettings()->host = 'localhost';
|
||||||
|
$oDomain->SieveSettings()->type = $iSecurityType;
|
||||||
|
$oDomain->SmtpSettings()->host = 'localhost';
|
||||||
|
$oDomain->SmtpSettings()->type = $iSecurityType;
|
||||||
|
$oDomain->SmtpSettings()->shortLogin = true;
|
||||||
$oProvider->Save($oDomain);
|
$oProvider->Save($oDomain);
|
||||||
if (!$oConfig->Get('login', 'default_domain', '')) {
|
if (!$oConfig->Get('login', 'default_domain', '')) {
|
||||||
$oConfig->Set('login', 'default_domain', 'nextcloud');
|
$oConfig->Set('login', 'default_domain', 'nextcloud');
|
||||||
|
|
|
||||||
|
|
@ -24,15 +24,19 @@ class PersonalSettings implements ISettings
|
||||||
$sEmail = $aRainLoop[0];
|
$sEmail = $aRainLoop[0];
|
||||||
$this->config->setUserValue($uid, 'snappymail', 'snappymail-email', $sEmail);
|
$this->config->setUserValue($uid, 'snappymail', 'snappymail-email', $sEmail);
|
||||||
if ($aRainLoop[1]) {
|
if ($aRainLoop[1]) {
|
||||||
$this->config->setUserValue($uid, 'snappymail', 'snappymail-password',
|
$this->config->setUserValue($uid, 'snappymail', 'passphrase',
|
||||||
\OCA\SnappyMail\Util\SnappyMailHelper::encodePassword($aRainLoop[1], \md5($sEmail))
|
\OCA\SnappyMail\Util\SnappyMailHelper::encodePassword($aRainLoop[1], \md5($sEmail))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if ($sPass = $this->config->getUserValue($uid, 'snappymail', 'snappymail-password')) {
|
||||||
|
$this->config->deleteUserValue($uid, 'snappymail', 'snappymail-password');
|
||||||
|
$this->config->setUserValue($uid, 'snappymail', 'passphrase', $sPass);
|
||||||
|
}
|
||||||
$parameters = [
|
$parameters = [
|
||||||
'snappymail-email' => $sEmail,
|
'snappymail-email' => $sEmail,
|
||||||
'snappymail-password' => $this->config->getUserValue($uid, 'snappymail', 'snappymail-password') ? '******' : ''
|
'snappymail-password' => $this->config->getUserValue($uid, 'snappymail', 'passphrase') ? '******' : ''
|
||||||
];
|
];
|
||||||
\OCP\Util::addScript('snappymail', 'snappymail');
|
\OCP\Util::addScript('snappymail', 'snappymail');
|
||||||
return new TemplateResponse('snappymail', 'personal_settings', $parameters, '');
|
return new TemplateResponse('snappymail', 'personal_settings', $parameters, '');
|
||||||
|
|
|
||||||
|
|
@ -4,15 +4,15 @@ namespace OCA\SnappyMail\Util;
|
||||||
|
|
||||||
class SnappyMailResponse extends \OCP\AppFramework\Http\Response
|
class SnappyMailResponse extends \OCP\AppFramework\Http\Response
|
||||||
{
|
{
|
||||||
public function render(): string
|
public function render(): string
|
||||||
{
|
{
|
||||||
$data = '';
|
$data = '';
|
||||||
$i = \ob_get_level();
|
$i = \ob_get_level();
|
||||||
while ($i--) {
|
while ($i--) {
|
||||||
$data .= \ob_get_clean();
|
$data .= \ob_get_clean();
|
||||||
}
|
}
|
||||||
return $data;
|
return $data;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class SnappyMailHelper
|
class SnappyMailHelper
|
||||||
|
|
@ -98,15 +98,16 @@ class SnappyMailHelper
|
||||||
$oActions->Plugins()->RunHook('login.success', array($oAccount));
|
$oActions->Plugins()->RunHook('login.success', array($oAccount));
|
||||||
|
|
||||||
$oActions->SetAuthToken($oAccount);
|
$oActions->SetAuthToken($oAccount);
|
||||||
if ($oConfig->Get('login', 'sign_me_auto', \RainLoop\Enumerations\SignMeType::DEFAULT_OFF) === \RainLoop\Enumerations\SignMeType::DEFAULT_ON) {
|
if ($oConfig->Get('login', 'sign_me_auto', \RainLoop\Enumerations\SignMeType::DefaultOff) === \RainLoop\Enumerations\SignMeType::DefaultOn) {
|
||||||
$oActions->SetSignMeToken($oAccount);
|
$oActions->SetSignMeToken($oAccount);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
// Login failure, reset password to prevent more attempts
|
// Login failure, reset password to prevent more attempts
|
||||||
$sUID = \OC::$server->getUserSession()->getUser()->getUID();
|
$sUID = \OC::$server->getUserSession()->getUser()->getUID();
|
||||||
\OC::$server->getSession()['snappymail-password'] = '';
|
\OC::$server->getSession()['snappymail-passphrase'] = '';
|
||||||
\OC::$server->getConfig()->setUserValue($sUID, 'snappymail', 'snappymail-password', '');
|
\OC::$server->getConfig()->setUserValue($sUID, 'snappymail', 'passphrase', '');
|
||||||
|
\SnappyMail\Log::error('Nextcloud', $e->getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -132,11 +133,14 @@ class SnappyMailHelper
|
||||||
// If the user has set credentials for SnappyMail in their personal settings,
|
// If the user has set credentials for SnappyMail in their personal settings,
|
||||||
// this has the first priority.
|
// this has the first priority.
|
||||||
$sEmail = $config->getUserValue($sUID, 'snappymail', 'snappymail-email');
|
$sEmail = $config->getUserValue($sUID, 'snappymail', 'snappymail-email');
|
||||||
$sPassword = $config->getUserValue($sUID, 'snappymail', 'snappymail-password');
|
$sPassword = $config->getUserValue($sUID, 'snappymail', 'passphrase')
|
||||||
|
?: $config->getUserValue($sUID, 'snappymail', 'snappymail-password');
|
||||||
if ($sEmail && $sPassword) {
|
if ($sEmail && $sPassword) {
|
||||||
$sPassword = static::decodePassword($sPassword, \md5($sEmail));
|
$sPassword = static::decodePassword($sPassword, \md5($sEmail));
|
||||||
if ($sPassword) {
|
if ($sPassword) {
|
||||||
return [$sUID, $sEmail, $sPassword];
|
return [$sUID, $sEmail, $sPassword];
|
||||||
|
} else {
|
||||||
|
\SnappyMail\Log::debug('Nextcloud', 'decodePassword failed for getUserValue');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -165,16 +169,18 @@ class SnappyMailHelper
|
||||||
$sPassword = '';
|
$sPassword = '';
|
||||||
if ($config->getAppValue('snappymail', 'snappymail-autologin', false)) {
|
if ($config->getAppValue('snappymail', 'snappymail-autologin', false)) {
|
||||||
$sEmail = $sUID;
|
$sEmail = $sUID;
|
||||||
$sPassword = $ocSession['snappymail-password'];
|
$sPassword = $ocSession['snappymail-passphrase'];
|
||||||
} else if ($config->getAppValue('snappymail', 'snappymail-autologin-with-email', false)) {
|
} else if ($config->getAppValue('snappymail', 'snappymail-autologin-with-email', false)) {
|
||||||
$sEmail = $config->getUserValue($sUID, 'settings', 'email');
|
$sEmail = $config->getUserValue($sUID, 'settings', 'email');
|
||||||
$sPassword = $ocSession['snappymail-password'];
|
$sPassword = $ocSession['snappymail-passphrase'];
|
||||||
} else {
|
} else {
|
||||||
\SnappyMail\Log::debug('Nextcloud', 'snappymail-autologin is off');
|
\SnappyMail\Log::debug('Nextcloud', 'snappymail-autologin is off');
|
||||||
}
|
}
|
||||||
if ($sPassword) {
|
if ($sPassword) {
|
||||||
return [$sUID, $sEmail, static::decodePassword($sPassword, $sUID)];
|
return [$sUID, $sEmail, static::decodePassword($sPassword, $sUID)];
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
\SnappyMail\Log::debug('Nextcloud', "snappymail-nc-uid mismatch '{$ocSession['snappymail-nc-uid']}' != '{$sUID}'");
|
||||||
}
|
}
|
||||||
|
|
||||||
return [$sUID, '', ''];
|
return [$sUID, '', ''];
|
||||||
|
|
@ -205,6 +211,6 @@ class SnappyMailHelper
|
||||||
{
|
{
|
||||||
static::loadApp();
|
static::loadApp();
|
||||||
$result = \SnappyMail\Crypt::DecryptUrlSafe($sPassword, $sSalt);
|
$result = \SnappyMail\Crypt::DecryptUrlSafe($sPassword, $sSalt);
|
||||||
return $result ? new \SnappyMail\SensitiveString($result) : $result;
|
return $result ? new \SnappyMail\SensitiveString($result) : null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -40,9 +40,9 @@
|
||||||
<br />
|
<br />
|
||||||
<!-- DISABLED https://github.com/the-djmaze/snappymail/issues/1420#issuecomment-1933045917
|
<!-- DISABLED https://github.com/the-djmaze/snappymail/issues/1420#issuecomment-1933045917
|
||||||
<p>
|
<p>
|
||||||
<input id="snappymail-autologin-oidc" name="snappymail-autologin-oidc" type="checkbox" class="checkbox" <?php if ($_['snappymail-autologin-oidc']) echo 'checked="checked"'; ?>>
|
<input id="snappymail-autologin-oidc" name="snappymail-autologin-oidc" type="checkbox" class="checkbox" <php if ($_['snappymail-autologin-oidc']) echo 'checked="checked"'; ?>>
|
||||||
<label for="snappymail-autologin-oidc">
|
<label for="snappymail-autologin-oidc">
|
||||||
<?php echo($l->t('Attempt to automatically login with OIDC when active')); ?>
|
<php echo($l->t('Attempt to automatically login with OIDC when active')); ?>
|
||||||
</label>
|
</label>
|
||||||
</p>
|
</p>
|
||||||
<br />
|
<br />
|
||||||
|
|
|
||||||
|
|
@ -78,11 +78,15 @@ class SnappyMailHelper
|
||||||
// $oDomain = \RainLoop\Model\Domain::fromIniArray('owncloud', []);
|
// $oDomain = \RainLoop\Model\Domain::fromIniArray('owncloud', []);
|
||||||
$oDomain = new \RainLoop\Model\Domain('owncloud');
|
$oDomain = new \RainLoop\Model\Domain('owncloud');
|
||||||
$iSecurityType = \MailSo\Net\Enumerations\ConnectionSecurityType::NONE;
|
$iSecurityType = \MailSo\Net\Enumerations\ConnectionSecurityType::NONE;
|
||||||
$oDomain->SetConfig(
|
$oDomain->ImapSettings()->host = 'localhost';
|
||||||
'localhost', 143, $iSecurityType, true,
|
$oDomain->ImapSettings()->type = $iSecurityType;
|
||||||
true, 'localhost', 4190, $iSecurityType,
|
$oDomain->ImapSettings()->shortLogin = true;
|
||||||
'localhost', 25, $iSecurityType, true, true, false, false,
|
$oDomain->SieveSettings()->enabled = true;
|
||||||
'');
|
$oDomain->SieveSettings()->host = 'localhost';
|
||||||
|
$oDomain->SieveSettings()->type = $iSecurityType;
|
||||||
|
$oDomain->SmtpSettings()->host = 'localhost';
|
||||||
|
$oDomain->SmtpSettings()->type = $iSecurityType;
|
||||||
|
$oDomain->SmtpSettings()->shortLogin = true;
|
||||||
$oProvider->Save($oDomain);
|
$oProvider->Save($oDomain);
|
||||||
if (!$oConfig->Get('login', 'default_domain', '')) {
|
if (!$oConfig->Get('login', 'default_domain', '')) {
|
||||||
$oConfig->Set('login', 'default_domain', 'owncloud');
|
$oConfig->Set('login', 'default_domain', 'owncloud');
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ return "SnappyMail Webmail is a browser-based multilingual IMAP client with an a
|
||||||
# script_snappymail_versions()
|
# script_snappymail_versions()
|
||||||
sub script_snappymail_versions
|
sub script_snappymail_versions
|
||||||
{
|
{
|
||||||
return ( "2.35.2" );
|
return ( "2.35.4" );
|
||||||
}
|
}
|
||||||
|
|
||||||
sub script_snappymail_version_desc
|
sub script_snappymail_version_desc
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
"title": "SnappyMail",
|
"title": "SnappyMail",
|
||||||
"description": "Simple, modern & fast web-based email client",
|
"description": "Simple, modern & fast web-based email client",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "2.35.2",
|
"version": "2.35.4",
|
||||||
"homepage": "https://snappymail.eu",
|
"homepage": "https://snappymail.eu",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "DJ Maze",
|
"name": "DJ Maze",
|
||||||
|
|
|
||||||
13
plugins/attachments-force-open/extension.js
Normal file
13
plugins/attachments-force-open/extension.js
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
(() => {
|
||||||
|
|
||||||
|
const dom = document.getElementById('MailMessageView').content;
|
||||||
|
|
||||||
|
dom.querySelector('.attachmentsControls').dataset.bind = '';
|
||||||
|
|
||||||
|
let ds = dom.querySelector('.attachmentsPlace').dataset;
|
||||||
|
ds.bind = ds.bind.replace('showAttachmentControls', 'true');
|
||||||
|
|
||||||
|
ds = dom.querySelector('.controls-handle').dataset;
|
||||||
|
ds.bind = ds.bind.replace('allowAttachmentControls', 'false');
|
||||||
|
|
||||||
|
})();
|
||||||
20
plugins/attachments-force-open/index.php
Normal file
20
plugins/attachments-force-open/index.php
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
class AttachmentsForceOpenPlugin extends \RainLoop\Plugins\AbstractPlugin
|
||||||
|
{
|
||||||
|
const
|
||||||
|
NAME = 'Attachments force open',
|
||||||
|
AUTHOR = 'SnappyMail',
|
||||||
|
URL = 'https://github.com/the-djmaze/snappymail/pull/1489',
|
||||||
|
VERSION = '0.1',
|
||||||
|
RELEASE = '2024-03-15',
|
||||||
|
REQUIRED = '2.14.0',
|
||||||
|
CATEGORY = 'General',
|
||||||
|
LICENSE = 'MIT',
|
||||||
|
DESCRIPTION = '';
|
||||||
|
|
||||||
|
public function Init() : void
|
||||||
|
{
|
||||||
|
$this->addJs('extension.js'); // add js file
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -10,8 +10,8 @@ class AvatarsPlugin extends \RainLoop\Plugins\AbstractPlugin
|
||||||
NAME = 'Avatars',
|
NAME = 'Avatars',
|
||||||
AUTHOR = 'SnappyMail',
|
AUTHOR = 'SnappyMail',
|
||||||
URL = 'https://snappymail.eu/',
|
URL = 'https://snappymail.eu/',
|
||||||
VERSION = '1.15',
|
VERSION = '1.16',
|
||||||
RELEASE = '2024-01-22',
|
RELEASE = '2024-03-12',
|
||||||
REQUIRED = '2.25.0',
|
REQUIRED = '2.25.0',
|
||||||
CATEGORY = 'Contacts',
|
CATEGORY = 'Contacts',
|
||||||
LICENSE = 'MIT',
|
LICENSE = 'MIT',
|
||||||
|
|
@ -208,7 +208,7 @@ class AvatarsPlugin extends \RainLoop\Plugins\AbstractPlugin
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
$sAsciiEmail = \mb_strtolower(\MailSo\Base\Utils::IdnToAscii($sEmail, true));
|
$sAsciiEmail = \mb_strtolower(\SnappyMail\IDN::emailToAscii($sEmail));
|
||||||
$sEmailId = \sha1($sAsciiEmail);
|
$sEmailId = \sha1($sAsciiEmail);
|
||||||
|
|
||||||
\MailSo\Base\Http::setETag($sEmailId);
|
\MailSo\Base\Http::setETag($sEmailId);
|
||||||
|
|
@ -310,7 +310,7 @@ class AvatarsPlugin extends \RainLoop\Plugins\AbstractPlugin
|
||||||
|
|
||||||
private static function cacheImage(string $sEmail, array $aResult) : void
|
private static function cacheImage(string $sEmail, array $aResult) : void
|
||||||
{
|
{
|
||||||
$sEmailId = \sha1(\mb_strtolower(\MailSo\Base\Utils::IdnToAscii($sEmail, true)));
|
$sEmailId = \sha1(\mb_strtolower(\SnappyMail\IDN::emailToAscii($sEmail)));
|
||||||
if (!\is_dir(\APP_PRIVATE_DATA . 'avatars')) {
|
if (!\is_dir(\APP_PRIVATE_DATA . 'avatars')) {
|
||||||
\mkdir(\APP_PRIVATE_DATA . 'avatars', 0700);
|
\mkdir(\APP_PRIVATE_DATA . 'avatars', 0700);
|
||||||
}
|
}
|
||||||
|
|
@ -323,7 +323,7 @@ class AvatarsPlugin extends \RainLoop\Plugins\AbstractPlugin
|
||||||
|
|
||||||
private static function getCachedImage(string $sEmail) : ?array
|
private static function getCachedImage(string $sEmail) : ?array
|
||||||
{
|
{
|
||||||
$sEmail = \mb_strtolower(\MailSo\Base\Utils::IdnToAscii($sEmail, true));
|
$sEmail = \mb_strtolower(\SnappyMail\IDN::emailToAscii($sEmail));
|
||||||
$aFiles = \glob(\APP_PRIVATE_DATA . "avatars/{$sEmail}.*");
|
$aFiles = \glob(\APP_PRIVATE_DATA . "avatars/{$sEmail}.*");
|
||||||
if ($aFiles) {
|
if ($aFiles) {
|
||||||
\MailSo\Base\Http::setLastModified(\filemtime($aFiles[0]));
|
\MailSo\Base\Http::setLastModified(\filemtime($aFiles[0]));
|
||||||
|
|
|
||||||
|
|
@ -4,35 +4,31 @@ class BlackListPlugin extends \RainLoop\Plugins\AbstractPlugin
|
||||||
{
|
{
|
||||||
const
|
const
|
||||||
NAME = 'Blacklist',
|
NAME = 'Blacklist',
|
||||||
VERSION = '2.1',
|
VERSION = '2.2',
|
||||||
RELEASE = '2021-04-21',
|
RELEASE = '2024-03-04',
|
||||||
REQUIRED = '2.5.0',
|
REQUIRED = '2.5.0',
|
||||||
CATEGORY = 'Login',
|
CATEGORY = 'Login',
|
||||||
DESCRIPTION = 'Simple blacklist extension (with wildcard and exceptions functionality).';
|
DESCRIPTION = 'Simple blacklist extension (with wildcard and exceptions functionality).';
|
||||||
|
|
||||||
public function Init() : void
|
public function Init() : void
|
||||||
{
|
{
|
||||||
$this->addHook('login.credentials', 'FilterLoginCredentials');
|
$this->addHook('login.credentials.step-1', 'FilterLoginCredentials');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param string $sEmail
|
|
||||||
* @param string $sLogin
|
|
||||||
* @param string $sPassword
|
|
||||||
*
|
|
||||||
* @throws \RainLoop\Exceptions\ClientException
|
* @throws \RainLoop\Exceptions\ClientException
|
||||||
*/
|
*/
|
||||||
public function FilterLoginCredentials(&$sEmail, &$sLogin, &$sPassword)
|
public function FilterLoginCredentials(string &$sEmail)
|
||||||
{
|
{
|
||||||
$sBlackList = \trim($this->Config()->Get('plugin', 'black_list', ''));
|
$sBlackList = \trim($this->Config()->Get('plugin', 'black_list', ''));
|
||||||
if (0 < \strlen($sBlackList) && \RainLoop\Plugins\Helper::ValidateWildcardValues($sEmail, $sBlackList))
|
if (\strlen($sBlackList) && \RainLoop\Plugins\Helper::ValidateWildcardValues($sEmail, $sBlackList)) {
|
||||||
{
|
|
||||||
$sExceptions = \trim($this->Config()->Get('plugin', 'exceptions', ''));
|
$sExceptions = \trim($this->Config()->Get('plugin', 'exceptions', ''));
|
||||||
if (0 === \strlen($sExceptions) || !\RainLoop\Plugins\Helper::ValidateWildcardValues($sEmail, $sExceptions))
|
if (!\strlen($sExceptions) || !\RainLoop\Plugins\Helper::ValidateWildcardValues($sEmail, $sExceptions)) {
|
||||||
{
|
|
||||||
throw new \RainLoop\Exceptions\ClientException(
|
throw new \RainLoop\Exceptions\ClientException(
|
||||||
$this->Config()->Get('plugin', 'auth_error', true) ?
|
$this->Config()->Get('plugin', 'auth_error', false)
|
||||||
\RainLoop\Notifications::AuthError : \RainLoop\Notifications::AccountNotAllowed);
|
? \RainLoop\Notifications::AuthError
|
||||||
|
: \RainLoop\Notifications::AccountNotAllowed
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -46,7 +42,7 @@ class BlackListPlugin extends \RainLoop\Plugins\AbstractPlugin
|
||||||
\RainLoop\Plugins\Property::NewInstance('auth_error')->SetLabel('Auth Error')
|
\RainLoop\Plugins\Property::NewInstance('auth_error')->SetLabel('Auth Error')
|
||||||
->SetType(\RainLoop\Enumerations\PluginPropertyType::BOOL)
|
->SetType(\RainLoop\Enumerations\PluginPropertyType::BOOL)
|
||||||
->SetDescription('Throw an authentication error instead of an access error.')
|
->SetDescription('Throw an authentication error instead of an access error.')
|
||||||
->SetDefaultValue(true),
|
->SetDefaultValue(false),
|
||||||
\RainLoop\Plugins\Property::NewInstance('black_list')->SetLabel('Black List')
|
\RainLoop\Plugins\Property::NewInstance('black_list')->SetLabel('Black List')
|
||||||
->SetType(\RainLoop\Enumerations\PluginPropertyType::STRING_TEXT)
|
->SetType(\RainLoop\Enumerations\PluginPropertyType::STRING_TEXT)
|
||||||
->SetDescription('Emails black list, space as delimiter, wildcard supported.')
|
->SetDescription('Emails black list, space as delimiter, wildcard supported.')
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
use SnappyMail\SensitiveString;
|
||||||
|
|
||||||
class ChangePasswordFroxlorDriver
|
class ChangePasswordFroxlorDriver
|
||||||
{
|
{
|
||||||
const
|
const
|
||||||
|
|
@ -44,7 +46,7 @@ class ChangePasswordFroxlorDriver
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function ChangePassword(\RainLoop\Model\Account $oAccount, string $sPrevPassword, string $sNewPassword) : bool
|
public function ChangePassword(\RainLoop\Model\Account $oAccount, SensitiveString $oPrevPassword, SensitiveString $oNewPassword) : bool
|
||||||
{
|
{
|
||||||
if (!\RainLoop\Plugins\Helper::ValidateWildcardValues($oAccount->Email(), $this->oConfig->Get('plugin', 'froxlor_allowed_emails', ''))) {
|
if (!\RainLoop\Plugins\Helper::ValidateWildcardValues($oAccount->Email(), $this->oConfig->Get('plugin', 'froxlor_allowed_emails', ''))) {
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -72,12 +74,12 @@ class ChangePasswordFroxlorDriver
|
||||||
if (!empty($aFetchResult['id'])) {
|
if (!empty($aFetchResult['id'])) {
|
||||||
$sDbPassword = $aFetchResult['password_enc'];
|
$sDbPassword = $aFetchResult['password_enc'];
|
||||||
$sDbSalt = \substr($sDbPassword, 0, \strrpos($sDbPassword, '$'));
|
$sDbSalt = \substr($sDbPassword, 0, \strrpos($sDbPassword, '$'));
|
||||||
if (\crypt($sPrevPassword, $sDbSalt) === $sDbPassword) {
|
if (\crypt($oPrevPassword, $sDbSalt) === $sDbPassword) {
|
||||||
|
|
||||||
$oStmt = $oPdo->prepare('UPDATE mail_users SET password_enc = ? WHERE id = ?');
|
$oStmt = $oPdo->prepare('UPDATE mail_users SET password_enc = ? WHERE id = ?');
|
||||||
|
|
||||||
return !!$oStmt->execute(array(
|
return !!$oStmt->execute(array(
|
||||||
$this->cryptPassword($sNewPassword),
|
$this->cryptPassword($oNewPassword),
|
||||||
$aFetchResult['id']
|
$aFetchResult['id']
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
@ -93,7 +95,7 @@ class ChangePasswordFroxlorDriver
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function cryptPassword(string $sPassword) : string
|
private function cryptPassword(SensitiveString $oPassword) : string
|
||||||
{
|
{
|
||||||
if (\defined('CRYPT_SHA512') && CRYPT_SHA512) {
|
if (\defined('CRYPT_SHA512') && CRYPT_SHA512) {
|
||||||
$sSalt = '$6$rounds=5000$' . \bin2hex(\random_bytes(8)) . '$';
|
$sSalt = '$6$rounds=5000$' . \bin2hex(\random_bytes(8)) . '$';
|
||||||
|
|
@ -102,6 +104,6 @@ class ChangePasswordFroxlorDriver
|
||||||
} else {
|
} else {
|
||||||
$sSalt = '$1$' . \bin2hex(\random_bytes(6)) . '$';
|
$sSalt = '$1$' . \bin2hex(\random_bytes(6)) . '$';
|
||||||
}
|
}
|
||||||
return \crypt($sPassword, $sSalt);
|
return \crypt($oPassword, $sSalt);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,15 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
use \RainLoop\Exceptions\ClientException;
|
use RainLoop\Exceptions\ClientException;
|
||||||
|
|
||||||
class ChangePasswordFroxlorPlugin extends \RainLoop\Plugins\AbstractPlugin
|
class ChangePasswordFroxlorPlugin extends \RainLoop\Plugins\AbstractPlugin
|
||||||
{
|
{
|
||||||
const
|
const
|
||||||
NAME = 'Change Password Froxlor',
|
NAME = 'Change Password Froxlor',
|
||||||
AUTHOR = 'Euphonique',
|
AUTHOR = 'Euphonique',
|
||||||
VERSION = '1.0',
|
VERSION = '2.36',
|
||||||
RELEASE = '2022-05-30',
|
RELEASE = '2024-03-17',
|
||||||
REQUIRED = '2.15.3',
|
REQUIRED = '2.36.0',
|
||||||
CATEGORY = 'Security',
|
CATEGORY = 'Security',
|
||||||
DESCRIPTION = 'Extension to allow users to change their passwords through Froxlor';
|
DESCRIPTION = 'Extension to allow users to change their passwords through Froxlor';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
use SnappyMail\SensitiveString;
|
||||||
|
|
||||||
class ChangePasswordHestiaDriver
|
class ChangePasswordHestiaDriver
|
||||||
{
|
{
|
||||||
const
|
const
|
||||||
|
|
@ -39,7 +41,7 @@ class ChangePasswordHestiaDriver
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function ChangePassword(\RainLoop\Model\Account $oAccount, string $sPrevPassword, string $sNewPassword) : bool
|
public function ChangePassword(\RainLoop\Model\Account $oAccount, SensitiveString $oPrevPassword, SensitiveString $oNewPassword) : bool
|
||||||
{
|
{
|
||||||
if (!\RainLoop\Plugins\Helper::ValidateWildcardValues($oAccount->Email(), $this->oConfig->Get('plugin', 'hestia_allowed_emails', ''))) {
|
if (!\RainLoop\Plugins\Helper::ValidateWildcardValues($oAccount->Email(), $this->oConfig->Get('plugin', 'hestia_allowed_emails', ''))) {
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -53,8 +55,8 @@ class ChangePasswordHestiaDriver
|
||||||
$HTTP = \SnappyMail\HTTP\Request::factory();
|
$HTTP = \SnappyMail\HTTP\Request::factory();
|
||||||
$postvars = array(
|
$postvars = array(
|
||||||
'email' => $oAccount->Email(),
|
'email' => $oAccount->Email(),
|
||||||
'password' => $sPrevPassword,
|
'password' => (string) $oPrevPassword,
|
||||||
'new' => $sNewPassword,
|
'new' => (string) $oNewPassword,
|
||||||
);
|
);
|
||||||
$response = $HTTP->doRequest('POST', 'https://'.$sHost.':'.$sPort.'/reset/mail/', \http_build_query($postvars));
|
$response = $HTTP->doRequest('POST', 'https://'.$sHost.':'.$sPort.'/reset/mail/', \http_build_query($postvars));
|
||||||
if (!$response) {
|
if (!$response) {
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,15 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
use \RainLoop\Exceptions\ClientException;
|
use RainLoop\Exceptions\ClientException;
|
||||||
|
|
||||||
class ChangePasswordHestiaPlugin extends \RainLoop\Plugins\AbstractPlugin
|
class ChangePasswordHestiaPlugin extends \RainLoop\Plugins\AbstractPlugin
|
||||||
{
|
{
|
||||||
const
|
const
|
||||||
NAME = 'Change Password Hestia',
|
NAME = 'Change Password Hestia',
|
||||||
AUTHOR = 'Jaap Marcus',
|
AUTHOR = 'Jaap Marcus',
|
||||||
VERSION = '1.2',
|
VERSION = '2.36',
|
||||||
RELEASE = '2023-05-16',
|
RELEASE = '2024-03-17',
|
||||||
REQUIRED = '2.16.3',
|
REQUIRED = '2.36.0',
|
||||||
CATEGORY = 'Security',
|
CATEGORY = 'Security',
|
||||||
DESCRIPTION = 'Extension to allow users to change their passwords through HestiaCP';
|
DESCRIPTION = 'Extension to allow users to change their passwords through HestiaCP';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
use MailSo\Net\ConnectSettings;
|
use MailSo\Net\ConnectSettings;
|
||||||
|
use SnappyMail\SensitiveString;
|
||||||
|
|
||||||
class ChangePasswordHMailServerDriver
|
class ChangePasswordHMailServerDriver
|
||||||
{
|
{
|
||||||
|
|
@ -37,7 +38,7 @@ class ChangePasswordHMailServerDriver
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function ChangePassword(\RainLoop\Model\Account $oAccount, string $sPrevPassword, string $sNewPassword) : bool
|
public function ChangePassword(\RainLoop\Model\Account $oAccount, SensitiveString $oPrevPassword, SensitiveString $oNewPassword) : bool
|
||||||
{
|
{
|
||||||
if (!\RainLoop\Plugins\Helper::ValidateWildcardValues($oAccount->Email(), $this->oConfig->Get('plugin', 'hmailserver_emails', ''))) {
|
if (!\RainLoop\Plugins\Helper::ValidateWildcardValues($oAccount->Email(), $this->oConfig->Get('plugin', 'hmailserver_emails', ''))) {
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -57,12 +58,12 @@ class ChangePasswordHMailServerDriver
|
||||||
$this->oConfig->Get('plugin', 'hmailserver_password', '')
|
$this->oConfig->Get('plugin', 'hmailserver_password', '')
|
||||||
)) {
|
)) {
|
||||||
$sEmail = $oAccount->Email();
|
$sEmail = $oAccount->Email();
|
||||||
$sDomain = \MailSo\Base\Utils::GetDomainFromEmail($sEmail);
|
$sDomain = \MailSo\Base\Utils::getEmailAddressDomain($sEmail);
|
||||||
$oHmailDomain = $oHmailApp->Domains->ItemByName($sDomain);
|
$oHmailDomain = $oHmailApp->Domains->ItemByName($sDomain);
|
||||||
if ($oHmailDomain) {
|
if ($oHmailDomain) {
|
||||||
$oHmailAccount = $oHmailDomain->Accounts->ItemByAddress($sEmail);
|
$oHmailAccount = $oHmailDomain->Accounts->ItemByAddress($sEmail);
|
||||||
if ($oHmailAccount) {
|
if ($oHmailAccount) {
|
||||||
$oHmailAccount->Password = $sNewPassword;
|
$oHmailAccount->Password = (string) $oNewPassword;
|
||||||
$oHmailAccount->Save();
|
$oHmailAccount->Save();
|
||||||
$bResult = true;
|
$bResult = true;
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,14 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
use \RainLoop\Exceptions\ClientException;
|
use RainLoop\Exceptions\ClientException;
|
||||||
|
|
||||||
class ChangePasswordHMailServerPlugin extends \RainLoop\Plugins\AbstractPlugin
|
class ChangePasswordHMailServerPlugin extends \RainLoop\Plugins\AbstractPlugin
|
||||||
{
|
{
|
||||||
const
|
const
|
||||||
NAME = 'Change Password hMailServer',
|
NAME = 'Change Password hMailServer',
|
||||||
VERSION = '2.0',
|
VERSION = '2.36',
|
||||||
RELEASE = '2022-10-14',
|
RELEASE = '2024-03-17',
|
||||||
REQUIRED = '2.15.3',
|
REQUIRED = '2.36.0',
|
||||||
CATEGORY = 'Security',
|
CATEGORY = 'Security',
|
||||||
DESCRIPTION = 'Extension to allow users to change their passwords through hMailServer';
|
DESCRIPTION = 'Extension to allow users to change their passwords through hMailServer';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
use SnappyMail\SensitiveString;
|
||||||
|
|
||||||
class ChangePasswordISPConfigDriver
|
class ChangePasswordISPConfigDriver
|
||||||
{
|
{
|
||||||
const
|
const
|
||||||
|
|
@ -44,7 +46,7 @@ class ChangePasswordISPConfigDriver
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function ChangePassword(\RainLoop\Model\Account $oAccount, string $sPrevPassword, string $sNewPassword) : bool
|
public function ChangePassword(\RainLoop\Model\Account $oAccount, SensitiveString $oPrevPassword, SensitiveString $oNewPassword) : bool
|
||||||
{
|
{
|
||||||
if (!\RainLoop\Plugins\Helper::ValidateWildcardValues($oAccount->Email(), $this->oConfig->Get('plugin', 'ispconfig_allowed_emails', ''))) {
|
if (!\RainLoop\Plugins\Helper::ValidateWildcardValues($oAccount->Email(), $this->oConfig->Get('plugin', 'ispconfig_allowed_emails', ''))) {
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -71,10 +73,10 @@ class ChangePasswordISPConfigDriver
|
||||||
if (!empty($aFetchResult['mailuser_id'])) {
|
if (!empty($aFetchResult['mailuser_id'])) {
|
||||||
$sDbPassword = $aFetchResult['password'];
|
$sDbPassword = $aFetchResult['password'];
|
||||||
$sDbSalt = \substr($sDbPassword, 0, \strrpos($sDbPassword, '$'));
|
$sDbSalt = \substr($sDbPassword, 0, \strrpos($sDbPassword, '$'));
|
||||||
if (\crypt($sPrevPassword, $sDbSalt) === $sDbPassword) {
|
if (\crypt($oPrevPassword, $sDbSalt) === $sDbPassword) {
|
||||||
$oStmt = $oPdo->prepare('UPDATE mail_user SET password = ? WHERE mailuser_id = ?');
|
$oStmt = $oPdo->prepare('UPDATE mail_user SET password = ? WHERE mailuser_id = ?');
|
||||||
return !!$oStmt->execute(array(
|
return !!$oStmt->execute(array(
|
||||||
$this->cryptPassword($sNewPassword),
|
$this->cryptPassword($oNewPassword),
|
||||||
$aFetchResult['mailuser_id']
|
$aFetchResult['mailuser_id']
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
@ -90,7 +92,7 @@ class ChangePasswordISPConfigDriver
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function cryptPassword(string $sPassword) : string
|
private function cryptPassword(SensitiveString $oPassword) : string
|
||||||
{
|
{
|
||||||
if (\defined('CRYPT_SHA512') && CRYPT_SHA512) {
|
if (\defined('CRYPT_SHA512') && CRYPT_SHA512) {
|
||||||
$sSalt = '$6$rounds=5000$' . \bin2hex(\random_bytes(8)) . '$';
|
$sSalt = '$6$rounds=5000$' . \bin2hex(\random_bytes(8)) . '$';
|
||||||
|
|
@ -99,6 +101,6 @@ class ChangePasswordISPConfigDriver
|
||||||
} else {
|
} else {
|
||||||
$sSalt = '$1$' . \bin2hex(\random_bytes(6)) . '$';
|
$sSalt = '$1$' . \bin2hex(\random_bytes(6)) . '$';
|
||||||
}
|
}
|
||||||
return \crypt($sPassword, $sSalt);
|
return \crypt($oPassword, $sSalt);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,14 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
use \RainLoop\Exceptions\ClientException;
|
use RainLoop\Exceptions\ClientException;
|
||||||
|
|
||||||
class ChangePasswordISPConfigPlugin extends \RainLoop\Plugins\AbstractPlugin
|
class ChangePasswordISPConfigPlugin extends \RainLoop\Plugins\AbstractPlugin
|
||||||
{
|
{
|
||||||
const
|
const
|
||||||
NAME = 'Change Password ISPConfig',
|
NAME = 'Change Password ISPConfig',
|
||||||
VERSION = '2.19',
|
VERSION = '2.36',
|
||||||
RELEASE = '2023-04-11',
|
RELEASE = '2024-03-17',
|
||||||
REQUIRED = '2.23.0',
|
REQUIRED = '2.36.0',
|
||||||
CATEGORY = 'Security',
|
CATEGORY = 'Security',
|
||||||
DESCRIPTION = 'Extension to allow users to change their passwords through ISPConfig';
|
DESCRIPTION = 'Extension to allow users to change their passwords through ISPConfig';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
use MailSo\Net\ConnectSettings;
|
use MailSo\Net\ConnectSettings;
|
||||||
|
use SnappyMail\SensitiveString;
|
||||||
|
|
||||||
class ChangePasswordPoppassdDriver extends \MailSo\Net\NetClient
|
class ChangePasswordPoppassdDriver extends \MailSo\Net\NetClient
|
||||||
{
|
{
|
||||||
|
|
@ -33,7 +34,7 @@ class ChangePasswordPoppassdDriver extends \MailSo\Net\NetClient
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function ChangePassword(\RainLoop\Model\Account $oAccount, string $sPrevPassword, string $sNewPassword) : bool
|
public function ChangePassword(\RainLoop\Model\Account $oAccount, SensitiveString $oPrevPassword, SensitiveString $oNewPassword) : bool
|
||||||
{
|
{
|
||||||
if (!\RainLoop\Plugins\Helper::ValidateWildcardValues($oAccount->Email(), $this->oConfig->Get('plugin', 'poppassd_allowed_emails', ''))) {
|
if (!\RainLoop\Plugins\Helper::ValidateWildcardValues($oAccount->Email(), $this->oConfig->Get('plugin', 'poppassd_allowed_emails', ''))) {
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -55,7 +56,7 @@ class ChangePasswordPoppassdDriver extends \MailSo\Net\NetClient
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
$this->sendRequestWithCheck('user', $oAccount->IncLogin(), true);
|
$this->sendRequestWithCheck('user', $oAccount->IncLogin(), true);
|
||||||
$this->sendRequestWithCheck('pass', $sPrevPassword, true);
|
$this->sendRequestWithCheck('pass', $oPrevPassword, true);
|
||||||
}
|
}
|
||||||
catch (\Throwable $oException)
|
catch (\Throwable $oException)
|
||||||
{
|
{
|
||||||
|
|
@ -65,7 +66,7 @@ class ChangePasswordPoppassdDriver extends \MailSo\Net\NetClient
|
||||||
$this->bIsLoggined = true;
|
$this->bIsLoggined = true;
|
||||||
|
|
||||||
if ($this->bIsLoggined) {
|
if ($this->bIsLoggined) {
|
||||||
$this->sendRequestWithCheck('newpass', $sNewPassword);
|
$this->sendRequestWithCheck('newpass', $oNewPassword);
|
||||||
} else {
|
} else {
|
||||||
$this->writeLogException(
|
$this->writeLogException(
|
||||||
new \RuntimeException('Required login'),
|
new \RuntimeException('Required login'),
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,14 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
use \RainLoop\Exceptions\ClientException;
|
use RainLoop\Exceptions\ClientException;
|
||||||
|
|
||||||
class ChangePasswordPoppassdPlugin extends \RainLoop\Plugins\AbstractPlugin
|
class ChangePasswordPoppassdPlugin extends \RainLoop\Plugins\AbstractPlugin
|
||||||
{
|
{
|
||||||
const
|
const
|
||||||
NAME = 'Change Password Poppassd',
|
NAME = 'Change Password Poppassd',
|
||||||
VERSION = '2.20',
|
VERSION = '2.36',
|
||||||
RELEASE = '2023-12-08',
|
RELEASE = '2024-03-17',
|
||||||
REQUIRED = '2.28.0',
|
REQUIRED = '2.36.0',
|
||||||
CATEGORY = 'Security',
|
CATEGORY = 'Security',
|
||||||
DESCRIPTION = 'Extension to allow users to change their passwords through Poppassd';
|
DESCRIPTION = 'Extension to allow users to change their passwords through Poppassd';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
use SnappyMail\SensitiveString;
|
||||||
|
|
||||||
class ChangePasswordDriverLDAP
|
class ChangePasswordDriverLDAP
|
||||||
{
|
{
|
||||||
const
|
const
|
||||||
|
|
@ -55,19 +57,19 @@ class ChangePasswordDriverLDAP
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function ChangePassword(\RainLoop\Model\Account $oAccount, string $sPrevPassword, string $sNewPassword) : bool
|
public function ChangePassword(\RainLoop\Model\Account $oAccount, SensitiveString $oPrevPassword, SensitiveString $oNewPassword) : bool
|
||||||
{
|
{
|
||||||
$sDomain = \MailSo\Base\Utils::GetDomainFromEmail($oAccount->Email());
|
$sDomain = \MailSo\Base\Utils::getEmailAddressDomain($oAccount->Email());
|
||||||
$sUserDn = \strtr($this->sUserDnFormat, array(
|
$sUserDn = \strtr($this->sUserDnFormat, array(
|
||||||
'{domain}' => $sDomain,
|
'{domain}' => $sDomain,
|
||||||
'{domain:dc}' => 'dc='.\strtr($sDomain, array('.' => ',dc=')),
|
'{domain:dc}' => 'dc='.\strtr($sDomain, array('.' => ',dc=')),
|
||||||
'{email}' => $oAccount->Email(),
|
'{email}' => $oAccount->Email(),
|
||||||
'{email:user}' => \MailSo\Base\Utils::GetAccountNameFromEmail($oAccount->Email()),
|
'{email:user}' => \MailSo\Base\Utils::getEmailAddressLocalPart($oAccount->Email()),
|
||||||
'{email:domain}' => $sDomain,
|
'{email:domain}' => $sDomain,
|
||||||
'{login}' => $oAccount->IncLogin(),
|
'{login}' => $oAccount->IncLogin(),
|
||||||
'{imap:login}' => $oAccount->IncLogin(),
|
'{imap:login}' => $oAccount->IncLogin(),
|
||||||
'{imap:host}' => $oAccount->Domain()->IncHost(),
|
'{imap:host}' => $oAccount->Domain()->ImapSettings()->host,
|
||||||
'{imap:port}' => $oAccount->Domain()->IncPort(),
|
'{imap:port}' => $oAccount->Domain()->ImapSettings()->port,
|
||||||
'{gecos}' => \function_exists('posix_getpwnam') ? \posix_getpwnam($oAccount->IncLogin()) : ''
|
'{gecos}' => \function_exists('posix_getpwnam') ? \posix_getpwnam($oAccount->IncLogin()) : ''
|
||||||
));
|
));
|
||||||
|
|
||||||
|
|
@ -89,25 +91,25 @@ class ChangePasswordDriverLDAP
|
||||||
throw new \Exception('ldap_start_tls error '.\ldap_errno($oCon).': '.\ldap_error($oCon));
|
throw new \Exception('ldap_start_tls error '.\ldap_errno($oCon).': '.\ldap_error($oCon));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!\ldap_bind($oCon, $sUserDn, $sPrevPassword)) {
|
if (!\ldap_bind($oCon, $sUserDn, $oPrevPassword)) {
|
||||||
throw new \Exception('ldap_bind error '.\ldap_errno($oCon).': '.\ldap_error($oCon));
|
throw new \Exception('ldap_bind error '.\ldap_errno($oCon).': '.\ldap_error($oCon));
|
||||||
}
|
}
|
||||||
|
|
||||||
$sSshaSalt = '';
|
$sSshaSalt = '';
|
||||||
$sPrefix = '{'.\strtoupper($this->sPasswordEncType).'}';
|
$sPrefix = '{'.\strtoupper($this->sPasswordEncType).'}';
|
||||||
$sEncodedNewPassword = $sNewPassword;
|
$sEncodedNewPassword = $oNewPassword;
|
||||||
switch ($sPrefix)
|
switch ($sPrefix)
|
||||||
{
|
{
|
||||||
case '{SSHA}':
|
case '{SSHA}':
|
||||||
$sSshaSalt = $this->getSalt(4);
|
$sSshaSalt = $this->getSalt(4);
|
||||||
case '{SHA}':
|
case '{SHA}':
|
||||||
$sEncodedNewPassword = $sPrefix.\base64_encode(\hash('sha1', $sNewPassword.$sSshaSalt, true).$sSshaSalt);
|
$sEncodedNewPassword = $sPrefix.\base64_encode(\hash('sha1', $oNewPassword.$sSshaSalt, true).$sSshaSalt);
|
||||||
break;
|
break;
|
||||||
case '{MD5}':
|
case '{MD5}':
|
||||||
$sEncodedNewPassword = $sPrefix.\base64_encode(\md5($sNewPassword, true));
|
$sEncodedNewPassword = $sPrefix.\base64_encode(\md5($oNewPassword, true));
|
||||||
break;
|
break;
|
||||||
case '{CRYPT}':
|
case '{CRYPT}':
|
||||||
$sEncodedNewPassword = $sPrefix.\crypt($sNewPassword, $this->getSalt(2));
|
$sEncodedNewPassword = $sPrefix.\crypt($oNewPassword, $this->getSalt(2));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
use SnappyMail\SensitiveString;
|
||||||
|
|
||||||
class ChangePasswordDriverPDO
|
class ChangePasswordDriverPDO
|
||||||
{
|
{
|
||||||
const
|
const
|
||||||
|
|
@ -58,7 +60,7 @@ class ChangePasswordDriverPDO
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function ChangePassword(\RainLoop\Model\Account $oAccount, string $sPrevPassword, string $sNewPassword) : bool
|
public function ChangePassword(\RainLoop\Model\Account $oAccount, SensitiveString $oPrevPassword, SensitiveString $oNewPassword) : bool
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
|
@ -85,10 +87,10 @@ class ChangePasswordDriverPDO
|
||||||
|
|
||||||
$placeholders = array(
|
$placeholders = array(
|
||||||
':email' => $sEmail,
|
':email' => $sEmail,
|
||||||
':oldpass' => $encrypt_prefix . \ChangePasswordPlugin::encrypt($encrypt, $sPrevPassword),
|
':oldpass' => $encrypt_prefix . \ChangePasswordPlugin::encrypt($encrypt, $oPrevPassword),
|
||||||
':newpass' => $encrypt_prefix . \ChangePasswordPlugin::encrypt($encrypt, $sNewPassword),
|
':newpass' => $encrypt_prefix . \ChangePasswordPlugin::encrypt($encrypt, $oNewPassword),
|
||||||
':domain' => \MailSo\Base\Utils::GetDomainFromEmail($sEmail),
|
':domain' => \MailSo\Base\Utils::getEmailAddressDomain($sEmail),
|
||||||
':username' => \MailSo\Base\Utils::GetAccountNameFromEmail($sEmail),
|
':username' => \MailSo\Base\Utils::getEmailAddressLocalPart($sEmail),
|
||||||
':login_name' => $oAccount->IncLogin()
|
':login_name' => $oAccount->IncLogin()
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,15 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
use \RainLoop\Exceptions\ClientException;
|
use RainLoop\Exceptions\ClientException;
|
||||||
|
use SnappyMail\SensitiveString;
|
||||||
|
|
||||||
class ChangePasswordPlugin extends \RainLoop\Plugins\AbstractPlugin
|
class ChangePasswordPlugin extends \RainLoop\Plugins\AbstractPlugin
|
||||||
{
|
{
|
||||||
const
|
const
|
||||||
NAME = 'Change Password',
|
NAME = 'Change Password',
|
||||||
VERSION = '2.19',
|
VERSION = '2.36',
|
||||||
RELEASE = '2023-04-11',
|
RELEASE = '2024-03-17',
|
||||||
REQUIRED = '2.23.0',
|
REQUIRED = '2.36.0',
|
||||||
CATEGORY = 'Security',
|
CATEGORY = 'Security',
|
||||||
DESCRIPTION = 'Extension to allow users to change their passwords';
|
DESCRIPTION = 'Extension to allow users to change their passwords';
|
||||||
|
|
||||||
|
|
@ -149,15 +150,16 @@ class ChangePasswordPlugin extends \RainLoop\Plugins\AbstractPlugin
|
||||||
if ($sPrevPassword !== $oAccount->IncPassword()) {
|
if ($sPrevPassword !== $oAccount->IncPassword()) {
|
||||||
throw new ClientException(static::CurrentPasswordIncorrect, null, $oActions->StaticI18N('NOTIFICATIONS/CURRENT_PASSWORD_INCORRECT'));
|
throw new ClientException(static::CurrentPasswordIncorrect, null, $oActions->StaticI18N('NOTIFICATIONS/CURRENT_PASSWORD_INCORRECT'));
|
||||||
}
|
}
|
||||||
|
$oPrevPassword = new \SnappyMail\SensitiveString($sPrevPassword);
|
||||||
|
|
||||||
$sNewPassword = $this->jsonParam('NewPassword');
|
$sNewPassword = $this->jsonParam('NewPassword');
|
||||||
if ($this->Config()->Get('plugin', 'pass_min_length', 10) > \strlen($sNewPassword)) {
|
if ($this->Config()->Get('plugin', 'pass_min_length', 10) > \strlen($sNewPassword)) {
|
||||||
throw new ClientException(static::NewPasswordShort, null, $oActions->StaticI18N('NOTIFICATIONS/NEW_PASSWORD_SHORT'));
|
throw new ClientException(static::NewPasswordShort, null, $oActions->StaticI18N('NOTIFICATIONS/NEW_PASSWORD_SHORT'));
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($this->Config()->Get('plugin', 'pass_min_strength', 70) > static::PasswordStrength($sNewPassword)) {
|
if ($this->Config()->Get('plugin', 'pass_min_strength', 70) > static::PasswordStrength($sNewPassword)) {
|
||||||
throw new ClientException(static::NewPasswordWeak, null, $oActions->StaticI18N('NOTIFICATIONS/NEW_PASSWORD_WEAK'));
|
throw new ClientException(static::NewPasswordWeak, null, $oActions->StaticI18N('NOTIFICATIONS/NEW_PASSWORD_WEAK'));
|
||||||
}
|
}
|
||||||
|
$oNewPassword = new \SnappyMail\SensitiveString($sNewPassword);
|
||||||
|
|
||||||
$bResult = false;
|
$bResult = false;
|
||||||
$oConfig = $this->Config();
|
$oConfig = $this->Config();
|
||||||
|
|
@ -171,7 +173,7 @@ class ChangePasswordPlugin extends \RainLoop\Plugins\AbstractPlugin
|
||||||
$oConfig,
|
$oConfig,
|
||||||
$oLogger
|
$oLogger
|
||||||
);
|
);
|
||||||
if (!$oDriver->ChangePassword($oAccount, $sPrevPassword, $sNewPassword)) {
|
if (!$oDriver->ChangePassword($oAccount, $oPrevPassword, $oNewPassword)) {
|
||||||
throw new ClientException(static::CouldNotSaveNewPassword);
|
throw new ClientException(static::CouldNotSaveNewPassword);
|
||||||
}
|
}
|
||||||
$bResult = true;
|
$bResult = true;
|
||||||
|
|
@ -196,15 +198,16 @@ class ChangePasswordPlugin extends \RainLoop\Plugins\AbstractPlugin
|
||||||
throw new ClientException(static::CouldNotSaveNewPassword);
|
throw new ClientException(static::CouldNotSaveNewPassword);
|
||||||
}
|
}
|
||||||
|
|
||||||
$oAccount->SetPassword($sNewPassword);
|
$oAccount->SetPassword($oNewPassword);
|
||||||
if ($oAccount instanceof \RainLoop\Model\MainAccount) {
|
if ($oAccount instanceof \RainLoop\Model\MainAccount) {
|
||||||
$oActions->SetAuthToken($oAccount);
|
$oActions->SetAuthToken($oAccount);
|
||||||
|
$oAccount->resealCryptKey($oPrevPassword);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->jsonResponse(__FUNCTION__, $oActions->AppData(false));
|
return $this->jsonResponse(__FUNCTION__, $oActions->AppData(false));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function encrypt(string $algo, string $password)
|
public static function encrypt(string $algo, SensitiveString $password)
|
||||||
{
|
{
|
||||||
switch (\strtolower($algo))
|
switch (\strtolower($algo))
|
||||||
{
|
{
|
||||||
|
|
@ -233,7 +236,7 @@ class ChangePasswordPlugin extends \RainLoop\Plugins\AbstractPlugin
|
||||||
private static function PasswordStrength(string $sPassword) : int
|
private static function PasswordStrength(string $sPassword) : int
|
||||||
{
|
{
|
||||||
$i = \strlen($sPassword);
|
$i = \strlen($sPassword);
|
||||||
$max = min(100, $i * 8);
|
$max = \min(100, $i * 8);
|
||||||
$s = 0;
|
$s = 0;
|
||||||
while (--$i) {
|
while (--$i) {
|
||||||
$s += ($sPassword[$i] != $sPassword[$i-1] ? 1 : -0.5);
|
$s += ($sPassword[$i] != $sPassword[$i-1] ? 1 : -0.5);
|
||||||
|
|
|
||||||
88
plugins/compact-composer/css/composer.css
Normal file
88
plugins/compact-composer/css/composer.css
Normal file
|
|
@ -0,0 +1,88 @@
|
||||||
|
.squire-toolbar {
|
||||||
|
padding-top: 4px;
|
||||||
|
padding-bottom: 0;
|
||||||
|
overflow: visible;
|
||||||
|
z-index: 200;
|
||||||
|
white-space: normal;
|
||||||
|
min-height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.squire-toolbar > .btn-group {
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.squire-toolbar > .btn-group > a.btn,
|
||||||
|
.squire-toolbar button.btn,
|
||||||
|
.squire-toolbar select.btn {
|
||||||
|
line-height: 20px;
|
||||||
|
padding-top: 4px;
|
||||||
|
padding-bottom: 4px;
|
||||||
|
min-height: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.squire-toolbar-menu-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: .25em;
|
||||||
|
margin: .1em !important;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: .25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.squire-toolbar-menu-item.active {
|
||||||
|
background-color: rgba(128, 128, 128, .1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.squire-toolbar-menu-item:hover {
|
||||||
|
background-color: rgba(128, 128, 128, .2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.squire-toolbar-svg-icon {
|
||||||
|
display: block;
|
||||||
|
fill: var(--dialog-clr, #333);
|
||||||
|
}
|
||||||
|
|
||||||
|
.squire2-mode-wysiwyg .squire-plain,
|
||||||
|
.squire2-mode-source .squire-wysiwyg,
|
||||||
|
.squire2-mode-plain .squire-wysiwyg {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.squire2-mode-source .squire-plain,
|
||||||
|
.squire2-mode-plain .squire-plain {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.squire-toolbar > .squire-toolbar-menu-wrap:last-child {
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.squire-toolbar.mode-plain .squire-html-mode-item {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#V-PopupsCompose .attachmentAreaParent.compact {
|
||||||
|
height: auto;
|
||||||
|
min-height: auto;
|
||||||
|
padding: 0;
|
||||||
|
overflow: auto;
|
||||||
|
flex: 1 0 auto;
|
||||||
|
max-height: 12em;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#V-PopupsCompose .compact > .b-attachment-place {
|
||||||
|
position: static;
|
||||||
|
display: none;
|
||||||
|
margin: .375em;
|
||||||
|
line-height: 4em;
|
||||||
|
}
|
||||||
|
|
||||||
|
#V-PopupsCompose .compact > .b-attachment-place.dragAndDropOver {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
#V-PopupsCompose .compact .attachmentList {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
23
plugins/compact-composer/index.php
Normal file
23
plugins/compact-composer/index.php
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
class CompactComposerPlugin extends \RainLoop\Plugins\AbstractPlugin
|
||||||
|
{
|
||||||
|
const
|
||||||
|
NAME = 'Compact Composer',
|
||||||
|
AUTHOR = 'Sergey Mosin',
|
||||||
|
URL = 'https://github.com/the-djmaze/snappymail/pull/1466',
|
||||||
|
VERSION = '1.0.2',
|
||||||
|
RELEASE = '2024-02-23',
|
||||||
|
REQUIRED = '2.34.0',
|
||||||
|
LICENSE = 'AGPL v3',
|
||||||
|
DESCRIPTION = 'WYSIWYG editor with a compact toolbar';
|
||||||
|
|
||||||
|
public function Init(): void
|
||||||
|
{
|
||||||
|
$this->addTemplate('templates/PopupsCompactCompose.html');
|
||||||
|
$this->addCss('css/composer.css');
|
||||||
|
$this->addJs('js/squire-raw.js');
|
||||||
|
$this->addJs('js/parsel.js');
|
||||||
|
$this->addJs('js/CompactComposer.js');
|
||||||
|
}
|
||||||
|
}
|
||||||
991
plugins/compact-composer/js/CompactComposer.js
Normal file
991
plugins/compact-composer/js/CompactComposer.js
Normal file
|
|
@ -0,0 +1,991 @@
|
||||||
|
/* eslint max-len: 0 */
|
||||||
|
(win => {
|
||||||
|
|
||||||
|
const rl = win.rl;
|
||||||
|
|
||||||
|
if (!rl) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
rl.registerWYSIWYG('CompactComposer', (owner, container, onReady) => {
|
||||||
|
const editor = new CompactComposer(container);
|
||||||
|
onReady(editor);
|
||||||
|
});
|
||||||
|
|
||||||
|
const doc = win.document;
|
||||||
|
|
||||||
|
// If a user (or admin) selected the CompactComposer we need to
|
||||||
|
// replace PopupsCompose template with PopupsCompactCompose template.
|
||||||
|
// --
|
||||||
|
// This might break some plugins if they query/change PopupsCompose template
|
||||||
|
// before this code is called. They should instead listen for
|
||||||
|
// 'rl-view-model.create' to work properly.
|
||||||
|
if (rl.settings.get('editorWysiwyg') === 'CompactComposer') {
|
||||||
|
const compactTemplate = doc.getElementById('PopupsCompactCompose');
|
||||||
|
if (!compactTemplate) {
|
||||||
|
console.error('CompactComposer: PopupsCompactCompose template not found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const originalTemplate = doc.getElementById('PopupsCompose');
|
||||||
|
if (originalTemplate) {
|
||||||
|
originalTemplate.id = 'PopupsCompose_replaced';
|
||||||
|
} else {
|
||||||
|
console.warn('CompactComposer: PopupsCompose template not found');
|
||||||
|
}
|
||||||
|
compactTemplate.id = 'PopupsCompose';
|
||||||
|
|
||||||
|
addEventListener('rl-view-model.create', e => {
|
||||||
|
if (e.detail.viewModelTemplateID === 'PopupsCompose') {
|
||||||
|
// There is a better way to do this probably,
|
||||||
|
// but we need this for drag and drop to work
|
||||||
|
e.detail.attachmentsArea = e.detail.bodyArea;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const
|
||||||
|
removeElements = 'HEAD,LINK,META,NOSCRIPT,SCRIPT,TEMPLATE,TITLE',
|
||||||
|
allowedElements = 'A,B,BLOCKQUOTE,BR,DIV,EM,FONT,H1,H2,H3,H4,H5,H6,HR,I,IMG,LI,OL,P,SPAN,STRONG,TABLE,TD,TH,TR,U,UL',
|
||||||
|
allowedAttributes = 'abbr,align,background,bgcolor,border,cellpadding,cellspacing,class,color,colspan,dir,face,frame,height,href,hspace,id,lang,rowspan,rules,scope,size,src,style,target,type,usemap,valign,vspace,width'.split(','),
|
||||||
|
|
||||||
|
// TODO: labels translations
|
||||||
|
i18n = (str, def) => rl.i18n(str) || def,
|
||||||
|
|
||||||
|
ctrlKey = shortcuts.getMetaKey() + ' + ',
|
||||||
|
|
||||||
|
createElement = name => doc.createElement(name),
|
||||||
|
|
||||||
|
tpl = createElement('template'),
|
||||||
|
|
||||||
|
trimLines = html => html.trim().replace(/^(<div>\s*<br\s*\/?>\s*<\/div>)+/, '').trim(),
|
||||||
|
htmlToPlain = html => rl.Utils.htmlToPlain(html).trim(),
|
||||||
|
plainToHtml = text => rl.Utils.plainToHtml(text),
|
||||||
|
|
||||||
|
getFragmentOfChildren = parent => {
|
||||||
|
let frag = doc.createDocumentFragment();
|
||||||
|
frag.append(...parent.childNodes);
|
||||||
|
return frag;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Array} data
|
||||||
|
* @param {String} prop
|
||||||
|
*/
|
||||||
|
getByProp = (data, prop) => {
|
||||||
|
for (let i = 0; i < data.length; i++) {
|
||||||
|
const outer = data[i];
|
||||||
|
if (outer.hasOwnProperty(prop)) {
|
||||||
|
return outer;
|
||||||
|
}
|
||||||
|
if (outer.items && Array.isArray(outer.items)) {
|
||||||
|
const item = outer.items.find(item => item.prop === prop);
|
||||||
|
if (item) {
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new Error('item with prop ' + prop + ' not found');
|
||||||
|
},
|
||||||
|
|
||||||
|
SquireDefaultConfig = {
|
||||||
|
/*
|
||||||
|
addLinks: true // allow_smart_html_links
|
||||||
|
*/
|
||||||
|
sanitizeToDOMFragment: (html) => {
|
||||||
|
tpl.innerHTML = (html || '')
|
||||||
|
.replace(/<\/?(BODY|HTML)[^>]*>/gi, '')
|
||||||
|
.replace(/<!--[^>]+-->/g, '')
|
||||||
|
.replace(/<span[^>]*>\s*<\/span>/gi, '')
|
||||||
|
.trim();
|
||||||
|
tpl.querySelectorAll('a:empty,span:empty').forEach(el => el.remove());
|
||||||
|
return tpl.content;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
pasteSanitizer = (event) => {
|
||||||
|
const frag = event.detail.fragment;
|
||||||
|
frag.querySelectorAll('a:empty,span:empty').forEach(el => el.remove());
|
||||||
|
frag.querySelectorAll(removeElements).forEach(el => el.remove());
|
||||||
|
frag.querySelectorAll('*').forEach(el => {
|
||||||
|
if (!el.matches(allowedElements)) {
|
||||||
|
el.replaceWith(getFragmentOfChildren(el));
|
||||||
|
} else if (el.hasAttributes()) {
|
||||||
|
[...el.attributes].forEach(attr => {
|
||||||
|
let name = attr.name.toLowerCase();
|
||||||
|
if (!allowedAttributes.includes(name)) {
|
||||||
|
el.removeAttribute(name);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
pasteImageHandler = (e, squire) => {
|
||||||
|
|
||||||
|
const items = [...e.detail.clipboardData.items];
|
||||||
|
const imageItems = items.filter((item) => /image/.test(item.type));
|
||||||
|
if (!imageItems.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let reader = new FileReader();
|
||||||
|
reader.onload = (loadEvent) => {
|
||||||
|
squire.insertImage(loadEvent.target.result);
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(imageItems[0].getAsFile());
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
class CompactComposer {
|
||||||
|
constructor(container) {
|
||||||
|
const
|
||||||
|
plain = createElement('textarea'),
|
||||||
|
wysiwyg = createElement('div'),
|
||||||
|
toolbar = createElement('div'),
|
||||||
|
squire = new win.Squire2(wysiwyg, SquireDefaultConfig);
|
||||||
|
|
||||||
|
this.container = container;
|
||||||
|
|
||||||
|
plain.className = 'squire-plain';
|
||||||
|
wysiwyg.className = 'squire-wysiwyg';
|
||||||
|
wysiwyg.dir = 'auto';
|
||||||
|
this.mode = ''; // 'plain' | 'wysiwyg'
|
||||||
|
this.squire = squire;
|
||||||
|
this.plain = plain;
|
||||||
|
this.wysiwyg = wysiwyg;
|
||||||
|
this.toolbar = toolbar;
|
||||||
|
|
||||||
|
toolbar.className = 'squire-toolbar btn-toolbar';
|
||||||
|
const actions = this.#makeActions(squire, toolbar);
|
||||||
|
|
||||||
|
this.squire.addEventListener('willPaste', pasteSanitizer);
|
||||||
|
this.squire.addEventListener('pasteImage', (e) => {
|
||||||
|
pasteImageHandler(e, squire);
|
||||||
|
});
|
||||||
|
|
||||||
|
// squire.addEventListener('focus', () => shortcuts.off());
|
||||||
|
// squire.addEventListener('blur', () => shortcuts.on());
|
||||||
|
|
||||||
|
container.append(toolbar, wysiwyg, plain);
|
||||||
|
|
||||||
|
const fontFamilySelect = getByProp(actions, 'fontFamily').element;
|
||||||
|
|
||||||
|
const fontSizeAction = getByProp(actions, 'fontSize');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} fontName
|
||||||
|
* @return {string}
|
||||||
|
*/
|
||||||
|
const normalizeFontName = (fontName) => fontName.trim().replace(/(^["']*|["']*$)/g, '').trim().toLowerCase();
|
||||||
|
|
||||||
|
/** @type {string[]} - lower cased array of available font families*/
|
||||||
|
const fontFamiliesLowerCase = Object.values(fontFamilySelect.options).map(option => option.value.toLowerCase());
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A theme might have CSS like div.squire-wysiwyg[contenteditable="true"] {
|
||||||
|
* font-family: 'Times New Roman', Times, serif; }
|
||||||
|
* so let's find the best match squire.getRoot()'s font
|
||||||
|
* it will also help to properly handle generic font names like 'sans-serif'
|
||||||
|
* @type {number}
|
||||||
|
*/
|
||||||
|
let defaultFontFamilyIndex = 0;
|
||||||
|
const squireRootFonts = getComputedStyle(squire.getRoot()).fontFamily.split(',').map(normalizeFontName);
|
||||||
|
fontFamiliesLowerCase.some((family, index) => {
|
||||||
|
const matchFound = family.split(',').some(availableFontName => {
|
||||||
|
const normalizedFontName = normalizeFontName(availableFontName);
|
||||||
|
return squireRootFonts.some(squireFontName => squireFontName === normalizedFontName);
|
||||||
|
});
|
||||||
|
if (matchFound) {
|
||||||
|
defaultFontFamilyIndex = index;
|
||||||
|
}
|
||||||
|
return matchFound;
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instead of comparing whole 'font-family' strings,
|
||||||
|
* we are going to look for individual font names, because we might be
|
||||||
|
* editing a Draft started in another email client for example
|
||||||
|
*
|
||||||
|
* @type {Object.<string,number>}
|
||||||
|
*/
|
||||||
|
const fontNamesMap = {};
|
||||||
|
/**
|
||||||
|
* @param {string} fontFamily
|
||||||
|
* @param {number} index
|
||||||
|
*/
|
||||||
|
const processFontFamilyString = (fontFamily, index) => {
|
||||||
|
fontFamily.split(',').forEach(fontName => {
|
||||||
|
const key = normalizeFontName(fontName);
|
||||||
|
if (fontNamesMap[key] === undefined) {
|
||||||
|
fontNamesMap[key] = index;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
// first deal with the default font family
|
||||||
|
processFontFamilyString(fontFamiliesLowerCase[defaultFontFamilyIndex], defaultFontFamilyIndex);
|
||||||
|
// and now with the rest of the font families
|
||||||
|
fontFamiliesLowerCase.forEach((fontFamily, index) => {
|
||||||
|
if (index !== defaultFontFamilyIndex) {
|
||||||
|
processFontFamilyString(fontFamily, index);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// -----
|
||||||
|
|
||||||
|
let ignoreNextSelectEvent = false;
|
||||||
|
|
||||||
|
squire.addEventListener('pathChange', e => {
|
||||||
|
|
||||||
|
const tokensMap = this.buildTokensMap(e.detail);
|
||||||
|
|
||||||
|
if (tokensMap.has('__selection__')) {
|
||||||
|
ignoreNextSelectEvent = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.indicators.forEach((indicator) => {
|
||||||
|
indicator.element.classList.toggle('active', indicator.selectors.some(selector => tokensMap.has(selector)));
|
||||||
|
});
|
||||||
|
|
||||||
|
let familySelectedIndex = defaultFontFamilyIndex;
|
||||||
|
const fontFamily = tokensMap.get('__font_family__');
|
||||||
|
if (fontFamily) {
|
||||||
|
familySelectedIndex = -1; // show empty select if we don't know the font
|
||||||
|
const fontNames = fontFamily.split(',');
|
||||||
|
for (let i = 0; i < fontNames.length; i++) {
|
||||||
|
const index = fontNamesMap[normalizeFontName(fontNames[i])];
|
||||||
|
if (index !== undefined) {
|
||||||
|
familySelectedIndex = index;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fontFamilySelect.selectedIndex = familySelectedIndex;
|
||||||
|
|
||||||
|
let sizeSelectedIndex = fontSizeAction.defaultValueIndex;
|
||||||
|
const fontSize = tokensMap.get('__font_size__');
|
||||||
|
if (fontSize) {
|
||||||
|
// -1 is ok because it will just show a blank <select>
|
||||||
|
sizeSelectedIndex = fontSizeAction.items.indexOf(fontSize);
|
||||||
|
}
|
||||||
|
fontSizeAction.element.selectedIndex = sizeSelectedIndex;
|
||||||
|
|
||||||
|
ignoreNextSelectEvent = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
squire.addEventListener('select', e => {
|
||||||
|
if (ignoreNextSelectEvent) {
|
||||||
|
ignoreNextSelectEvent = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.detail.range.collapsed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.indicators.forEach((indicator) => {
|
||||||
|
indicator.element.classList.toggle('active', indicator.selectors.some(selector => squire.hasFormat(selector)));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
squire.addEventListener('cursor', e => {
|
||||||
|
console.dir({cursor:e.range});
|
||||||
|
});
|
||||||
|
squire.addEventListener('select', e => {
|
||||||
|
console.dir({select:e.range});
|
||||||
|
});
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Squire} squire
|
||||||
|
* @param {HTMLDivElement} toolbar
|
||||||
|
* @returns {Array}
|
||||||
|
*/
|
||||||
|
#makeActions(squire, toolbar) {
|
||||||
|
|
||||||
|
const clr = this.#makeClr();
|
||||||
|
const doClr = name => input => {
|
||||||
|
// https://github.com/the-djmaze/snappymail/issues/826
|
||||||
|
clr.style.left = (input.offsetLeft + input.parentNode.offsetLeft) + 'px';
|
||||||
|
clr.style.width = input.offsetWidth + 'px';
|
||||||
|
|
||||||
|
clr.value = '';
|
||||||
|
clr.onchange = () => {
|
||||||
|
switch (name) {
|
||||||
|
case 'color':
|
||||||
|
squire.setTextColor(clr.value);
|
||||||
|
break;
|
||||||
|
case 'backgroundColor':
|
||||||
|
squire.setHighlightColor(clr.value);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.error('invalid name:', name);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// Chrome 110+ https://github.com/the-djmaze/snappymail/issues/1199
|
||||||
|
// clr.oninput = () => squire.setStyle({[name]:clr.value});
|
||||||
|
setTimeout(() => clr.click(), 1);
|
||||||
|
};
|
||||||
|
toolbar.append(clr);
|
||||||
|
|
||||||
|
const browseImage = createElement('input');
|
||||||
|
browseImage.type = 'file';
|
||||||
|
browseImage.accept = 'image/*';
|
||||||
|
browseImage.style.display = 'none';
|
||||||
|
browseImage.onchange = () => {
|
||||||
|
if (browseImage.files.length) {
|
||||||
|
let reader = new FileReader();
|
||||||
|
reader.readAsDataURL(browseImage.files[0]);
|
||||||
|
reader.onloadend = () => reader.result && squire.insertImage(reader.result);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const actions = [
|
||||||
|
{
|
||||||
|
type: 'group',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
type: 'select',
|
||||||
|
label: 'Font',
|
||||||
|
cmd: s => squire.setFontFace(s.value),
|
||||||
|
prop: 'fontFamily',
|
||||||
|
items: {
|
||||||
|
'sans-serif': {
|
||||||
|
Arial: '\'Nimbus Sans L\', \'Liberation sans\', \'Arial Unicode MS\', Arial, Helvetica, Garuda, Utkal, FreeSans, sans-serif',
|
||||||
|
Tahoma: '\'Luxi Sans\', Tahoma, Loma, Geneva, Meera, sans-serif',
|
||||||
|
Trebuchet: '\'DejaVu Sans Condensed\', Trebuchet, \'Trebuchet MS\', sans-serif',
|
||||||
|
Lucida: '\'Lucida Sans Unicode\', \'Lucida Sans\', \'DejaVu Sans\', \'Bitstream Vera Sans\', \'DejaVu LGC Sans\', sans-serif',
|
||||||
|
Verdana: '\'DejaVu Sans\', Verdana, Geneva, \'Bitstream Vera Sans\', \'DejaVu LGC Sans\', sans-serif'
|
||||||
|
},
|
||||||
|
monospace: {
|
||||||
|
Courier: '\'Liberation Mono\', \'Courier New\', FreeMono, Courier, monospace',
|
||||||
|
Lucida: '\'DejaVu Sans Mono\', \'DejaVu LGC Sans Mono\', \'Bitstream Vera Sans Mono\', \'Lucida Console\', Monaco, monospace'
|
||||||
|
},
|
||||||
|
sans: {
|
||||||
|
Times: '\'Nimbus Roman No9 L\', \'Times New Roman\', Times, FreeSerif, serif',
|
||||||
|
Palatino: '\'Bitstream Charter\', \'Palatino Linotype\', Palatino, Palladio, \'URW Palladio L\', \'Book Antiqua\', Times, serif',
|
||||||
|
Georgia: '\'URW Palladio L\', Georgia, Times, serif'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'select',
|
||||||
|
label: 'Font size',
|
||||||
|
cmd: s => squire.setFontSize(s.value),
|
||||||
|
prop: 'fontSize',
|
||||||
|
items: ['11px', '13px', '16px', '20px', '24px', '30px'],
|
||||||
|
defaultValueIndex: 2
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'menu',
|
||||||
|
label: 'Colors',
|
||||||
|
icon: '<svg width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="m8 7.75c0.713 0 1.25-0.559 1.25-1.25 0-0.691-0.54-1.25-1.25-1.25-0.71 0-1.25 0.556-1.25 1.25 0 0.694 0.537 1.25 1.25 1.25zm6.5 3c0.713 0 1.25-0.559 1.25-1.25 0-0.691-0.54-1.25-1.25-1.25s-1.25 0.556-1.25 1.25c0 0.694 0.537 1.25 1.25 1.25zm-9 0c0.713 0 1.25-0.559 1.25-1.25 0-0.691-0.54-1.25-1.25-1.25s-1.25 0.556-1.25 1.25c0 0.694 0.537 1.25 1.25 1.25zm4.5 7.25c-4.47 0-8-3.63-8-8 0-4.81 3.97-8 8.17-8 4.12 0 7.83 3.02 7.83 7.21 0 2.83-2.2 4.79-4.79 4.79h-1.42c-0.277 0-0.417 0.2-0.417 0.375 0 0.208 0.104 0.382 0.312 0.521 0.208 0.139 0.312 0.507 0.312 1.1 0 1.09-0.858 2-2 2zm2-10.2c0.713 0 1.25-0.559 1.25-1.25s-0.54-1.25-1.25-1.25-1.25 0.556-1.25 1.25 0.537 1.25 1.25 1.25zm-2 8.75c0.477 0 0.737-0.739 0.188-1.08-0.226-0.142-0.312-0.514-0.312-1.04 0-1.18 0.934-1.88 1.9-1.88h1.44c2.09-0.032 3.27-1.65 3.29-3.29 0-3.19-2.79-5.71-6.33-5.71-3.74 0-6.67 2.89-6.67 6.5 0 3.49 2.68 6.45 6.5 6.5z"/></svg>',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
type: 'menu_item',
|
||||||
|
label: 'Text Color',
|
||||||
|
cmd: doClr('color'),
|
||||||
|
icon: '<svg width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="m3 18v-3h14v3zm2.35-5 3.75-10h1.79l3.75 10h-1.73l-0.896-2.56h-4.02l-0.917 2.56zm3.15-4h3l-1.46-4.04h-0.0833z"/></svg>'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'menu_item',
|
||||||
|
label: 'Background Color',
|
||||||
|
cmd: doClr('backgroundColor'),
|
||||||
|
icon: '<svg width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="m7.22 16.6-4.87-4.85q-0.166-0.166-0.26-0.364-0.0936-0.198-0.0936-0.427t0.0936-0.447q0.0936-0.218 0.26-0.385l4.6-4.6-2.52-2.52 1.06-1.06 8.18 8.18q0.166 0.166 0.25 0.375 0.0832 0.208 0.0832 0.437 0 0.229-0.0832 0.437-0.0832 0.208-0.25 0.375l-4.85 4.85q-0.166 0.166-0.375 0.26-0.208 0.0936-0.437 0.0936-0.229-0.0208-0.427-0.104-0.198-0.0832-0.364-0.25zm0.791-10-4.35 4.35v-0.0208 0.0208h8.7v-0.0208 0.0208zm8.18 10.3q-0.77 0-1.3-0.52-0.531-0.52-0.531-1.29 0-0.499 0.229-0.967 0.229-0.468 0.541-0.884l1.06-1.33 1.04 1.33q0.291 0.416 0.531 0.884 0.239 0.468 0.239 0.967 0 0.77-0.531 1.29-0.531 0.52-1.28 0.52z"/></svg>'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'group',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
type: 'button',
|
||||||
|
label: 'Bold',
|
||||||
|
cmd: () => this.doAction('bold', 'B'),
|
||||||
|
key: 'B',
|
||||||
|
matches: 'B,STRONT',
|
||||||
|
icon: '<svg width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="m5.53 16v-12h4.75q1.4 0 2.57 0.861 1.18 0.861 1.18 2.39 0 1.06-0.469 1.66t-0.885 0.865q0.542 0.249 1.17 0.895 0.625 0.646 0.625 1.9 0 1.9-1.4 2.67-1.4 0.771-2.62 0.771zm2.65-2.46h2.18q1.01 0 1.21-0.51t0.208-0.74q0-0.229-0.219-0.74-0.219-0.51-1.28-0.51h-2.1zm0-4.83h1.94q0.688 0 1.01-0.365 0.323-0.365 0.323-0.781 0-0.5-0.356-0.812-0.356-0.312-0.923-0.312h-1.99z"/></svg>'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'button',
|
||||||
|
label: 'Italic',
|
||||||
|
cmd: () => this.doAction('italic', 'I'),
|
||||||
|
key: 'I',
|
||||||
|
matches: 'I,EM',
|
||||||
|
icon: '<svg width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="m4.5 16v-2h3.33l2.58-8h-3.42v-2h8.5v2h-3.08l-2.58 8h3.17v2z"/></svg>'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'button',
|
||||||
|
label: 'Underline',
|
||||||
|
cmd: () => this.doAction('underline', 'U'),
|
||||||
|
key: 'U',
|
||||||
|
matches: 'U',
|
||||||
|
icon: '<svg width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="m5 17v-1.5h10v1.5zm5-3q-2 0-3.09-1.24t-1.09-3.28v-6.48h2.03v6.61q0 1.1 0.551 1.79 0.551 0.688 1.61 0.688 1.06 0 1.61-0.688 0.55-0.688 0.55-1.79v-6.61h2.02v6.48q0 2.04-1.09 3.28t-3.09 1.24z"/></svg>'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'group',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
type: 'button',
|
||||||
|
label: 'Ordered List',
|
||||||
|
cmd: () => this.doList('OL'),
|
||||||
|
key: 'Shift + 8',
|
||||||
|
matches: 'OL',
|
||||||
|
icon: '<svg width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="m3 17v-1h2v-0.5h-1v-1h1v-0.5h-2v-1h2.5q0.212 0 0.356 0.144t0.144 0.356v1q0 0.212-0.144 0.356t-0.356 0.144q0.212 0 0.356 0.144t0.144 0.356v1q0 0.212-0.144 0.356t-0.356 0.144zm0-5v-2q0-0.212 0.144-0.356t0.356-0.144h1.5v-0.5h-2v-1h2.5q0.212 0 0.356 0.144t0.144 0.356v1.5q0 0.212-0.144 0.356t-0.356 0.144h-1.5v0.5h2v1zm1-5v-3h-1v-1h2v4zm3.5 8v-1.5h9.5v1.5zm0-4.25v-1.5h9.5v1.5zm0-4.25v-1.5h9.5v1.5z"/></svg>'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'button',
|
||||||
|
label: 'List',
|
||||||
|
cmd: () => this.doList('UL'),
|
||||||
|
key: 'Shift + 9',
|
||||||
|
matches: 'UL',
|
||||||
|
icon: '<svg width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="m7.5 15v-1.5h9.5v1.5zm0-4.25v-1.5h9.5v1.5zm0-4.25v-1.5h9.5v1.5zm-3 9.25q-0.621 0-1.06-0.442-0.438-0.442-0.438-1.06 0-0.621 0.442-1.06 0.442-0.438 1.06-0.438 0.621 0 1.06 0.442 0.438 0.442 0.438 1.06 0 0.621-0.442 1.06-0.442 0.438-1.06 0.438zm0-4.25q-0.621 0-1.06-0.442-0.438-0.442-0.438-1.06 0-0.621 0.442-1.06 0.442-0.438 1.06-0.438 0.621 0 1.06 0.442 0.438 0.442 0.438 1.06 0 0.621-0.442 1.06-0.442 0.438-1.06 0.438zm0-4.25q-0.621 0-1.06-0.442-0.438-0.442-0.438-1.06 0-0.621 0.442-1.06 0.442-0.438 1.06-0.438 0.621 0 1.06 0.442 0.438 0.442 0.438 1.06 0 0.621-0.442 1.06-0.442 0.438-1.06 0.438z"/></svg>'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'button',
|
||||||
|
label: 'Decrease Indent',
|
||||||
|
cmd: () => this.changeLevel('decrease'),
|
||||||
|
key: ']',
|
||||||
|
icon: '<svg width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="m3 17v-1.5h14v1.5zm6-3.12v-1.5h8v1.5zm0-3.12v-1.5h8v1.5zm0-3.12v-1.5h8v1.5zm-6-3.12v-1.5h14v1.5zm4 8.5v-6l-4 3z"/></svg>'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'button',
|
||||||
|
label: 'Increase Indent',
|
||||||
|
cmd: () => this.changeLevel('increase'),
|
||||||
|
key: '[',
|
||||||
|
icon: '<svg width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="m3 17v-1.5h14v1.5zm6-3.12v-1.5h8v1.5zm0-3.12v-1.5h8v1.5zm0-3.12v-1.5h8v1.5zm-6-3.12v-1.5h14v1.5zm0 8.5v-6l4 3z"/></svg>'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'menu',
|
||||||
|
rightEdge: true,
|
||||||
|
label: 'Insert Image',
|
||||||
|
icon: '<svg width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="m15 1v2h-2v2h2v2h2v-2h2v-2h-2v-2zm-12 2c-1.09 0-2 0.909-2 2v10c0 1.09 0.909 2 2 2h14c1.09 0 2-0.909 2-2v-5h-1.75v5.25h-14.5v-10.5h7.25v-1.75zm9 6-3 4-2-3-3 4h12z"/></svg>',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
type: 'menu_item',
|
||||||
|
label: 'Image File',
|
||||||
|
cmd: () => browseImage.click(),
|
||||||
|
icon: '<svg width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="m7 11h8l-2.62-3.5-1.88 2.5-1.37-1.83zm-4.5 6q-0.604 0-1.05-0.448t-0.448-1.05v-10h1.5v10h13.5v1.5zm3-3q-0.604 0-1.05-0.448-0.448-0.448-0.448-1.05v-9q0-0.619 0.448-1.06t1.05-0.441h3.52l2 2h5.48q0.619 0 1.06 0.441 0.441 0.441 0.441 1.06v7q0 0.604-0.441 1.05-0.441 0.448-1.06 0.448zm0-1.5h11v-7h-6.08l-2-2h-2.92zm0 0v-9z"/></svg>'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'menu_item',
|
||||||
|
label: 'From URL',
|
||||||
|
cmd: () => {
|
||||||
|
//TODO: check is if an IMG node is in range already
|
||||||
|
const src = prompt('Image', 'https://');
|
||||||
|
if (src) {
|
||||||
|
this.squire.insertImage(src);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
icon: '<svg width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="m11.9 8h-1.88c-0.692 0-1.28-0.244-1.77-0.732-0.488-0.488-0.731-1.08-0.731-1.77s0.244-1.28 0.731-1.77 1.08-0.729 1.77-0.729h1.88v0.938h-1.88c-0.434 0-0.803 0.152-1.11 0.456s-0.456 0.673-0.456 1.11 0.152 0.803 0.456 1.11 0.607 0.456 1.11 0.456h1.88zm-1.25-2.03v-0.938h3.75v0.938zm2.5 2.03v-0.938h1.88c0.434 0 0.803-0.152 1.11-0.456s0.456-0.673 0.456-1.11-0.152-0.803-0.456-1.11-0.673-0.456-1.11-0.456h-1.88v-0.938h1.88c0.692 0 1.28 0.244 1.77 0.732 0.488 0.488 0.731 1.08 0.731 1.77 0 0.692-0.244 1.28-0.731 1.77-0.488 0.486-1.08 0.729-1.77 0.729zm3.38 2v5.5c0 0.403-0.147 0.753-0.441 1.05s-0.647 0.448-1.06 0.448h-11c-0.412 0-0.766-0.149-1.06-0.448s-0.441-0.649-0.441-1.05v-9.25c0-0.403 0.147-0.753 0.441-1.05s0.649-0.407 1.06-0.448h1.5v1.5h-1.5v9.25h11v-5.5zm-11.5 4h9l-3-4-2.25 3-1.5-2z"/></svg>'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// this is a special case: we move the "attach" button group to the toolbar
|
||||||
|
// TODO: there is probably a better way of doing this in the template
|
||||||
|
// TODO: move Encrypt/Sign button group ?
|
||||||
|
type: 'move_parent',
|
||||||
|
label: 'Attach File',
|
||||||
|
id: 'composeUploadButton',
|
||||||
|
icon: '<svg width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="m12.5 9.5v-4.5h1.5v4.5zm-3.5 5.44c-1.02-0.272-1.5-1.15-1.5-2.02v-7.92h1.5zm0.5 3.06c-2.59 0-4.5-2.11-4.5-4.65v-8.1c0-1.95 1.55-3.25 3.25-3.25 1.85 0 3.25 1.51 3.25 3.4v6.35h-1.5v-6.5c0-1.09-0.883-1.75-1.75-1.75-1.06 0-1.75 0.906-1.75 1.79v8.21c0 2.63 3.33 4.15 5.25 1.96v1.94c-0.706 0.428-1.56 0.604-2.25 0.604zm3.75-1v-2.25h-2.25v-1.5h2.25v-2.25h1.5v2.25h2.25v1.5h-2.25v2.25z"/></svg>'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'menu_more',
|
||||||
|
label: 'More',
|
||||||
|
rightEdge: true,
|
||||||
|
showInPlainMode: true,
|
||||||
|
icon: '<svg width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="m2 4v2h16v-2zm0 5v2h16v-2zm0 5v2h16v-2z"/></svg>',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
type: 'menu_item',
|
||||||
|
label: 'Undo',
|
||||||
|
cmd: () => squire.undo(),
|
||||||
|
key: 'Z',
|
||||||
|
icon: '<svg width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="m4.82 9.3c1.45-1.26 3.32-2.03 5.4-2.03 3.64 0 6.71 2.37 7.79 5.65l-1.85 0.61c-0.821-2.49-3.17-4.3-5.94-4.3-1.52 0-2.92 0.563-4 1.47l2.83 2.83h-7.04v-7.04z"/></svg>'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'menu_item',
|
||||||
|
label: 'Redo',
|
||||||
|
cmd: () => squire.redo(),
|
||||||
|
key: 'Y',
|
||||||
|
icon: '<svg width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="m15.2 9.3c-1.45-1.26-3.32-2.03-5.4-2.03-3.64 0-6.71 2.37-7.79 5.65l1.85 0.61c0.821-2.49 3.17-4.3 5.94-4.3 1.52 0 2.92 0.563 4 1.47l-2.83 2.83h7.04v-7.04z"/></svg>'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'menu_item',
|
||||||
|
label: 'Blockquote',
|
||||||
|
cmd: () => {
|
||||||
|
if (!['UL', 'OL', 'BLOCKQUOTE'].some(listTag => this.squire.hasFormat(listTag))) {
|
||||||
|
this.changeLevel('increase');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
matches: 'BLOCKQUOTE',
|
||||||
|
icon: '<svg width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="m4 3c-0.554 0-1 0.446-1 1s0.446 1 1 1h12c0.554 0 1-0.446 1-1s-0.446-1-1-1h-12zm0 6c-0.554 0-1 0.446-1 1v6c0 0.554 0.446 1 1 1s1-0.446 1-1v-6c0-0.554-0.446-1-1-1zm5 0c-0.554 0-1 0.446-1 1s0.446 1 1 1h7c0.554 0 1-0.446 1-1s-0.446-1-1-1h-7zm0 6c-0.554 0-1 0.446-1 1s0.446 1 1 1h7c0.554 0 1-0.446 1-1s-0.446-1-1-1h-7z"/></svg>'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'menu_item',
|
||||||
|
label: 'Link',
|
||||||
|
cmd: () => {
|
||||||
|
/** @type {Range} range */
|
||||||
|
const range = this.squire.getSelection();
|
||||||
|
let linkNode;
|
||||||
|
if (range.collapsed || range.startContainer.parentNode === range.endContainer.parentNode) {
|
||||||
|
const root = this.squire.getRoot();
|
||||||
|
for (let node = range.startContainer; node !== root; node = node.parentNode) {
|
||||||
|
if (node.tagName === 'A') {
|
||||||
|
range.selectNode(node);
|
||||||
|
linkNode = node;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const url = prompt('Link', linkNode?.href || 'https://');
|
||||||
|
if (url != null) {
|
||||||
|
if (url.length) {
|
||||||
|
if (range.collapsed === false) {
|
||||||
|
// squire breaks the wrapping node, so if we have a <b>
|
||||||
|
// inside the selection it will create something like this:
|
||||||
|
// <a>t</a><b><a>ex</a></b><a>t</a> and we don't want that
|
||||||
|
// TODO: this could be more elegant
|
||||||
|
this.squire.removeAllFormatting(range);
|
||||||
|
}
|
||||||
|
this.squire.makeLink(url);
|
||||||
|
} else if (linkNode) {
|
||||||
|
this.squire.removeLink();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
matches: 'A',
|
||||||
|
icon: '<svg width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="m15.4 16.7c-0.509 0.521-1.13 0.781-1.87 0.781-0.735 0-1.36-0.256-1.87-0.771l-1.91-1.91c-0.515-0.515-0.771-1.14-0.771-1.87 0-0.743 0.267-1.38 0.801-1.9l-0.809-0.809c-0.525 0.533-1.16 0.801-1.91 0.801-0.735 0-1.36-0.255-1.87-0.763l-1.9-1.89c-0.521-0.509-0.781-1.13-0.781-1.87-6e-7 -0.735 0.255-1.36 0.763-1.87l1.34-1.35c0.509-0.521 1.13-0.781 1.87-0.781 0.735 1.6e-4 1.36 0.256 1.87 0.771l1.91 1.91c0.515 0.515 0.772 1.14 0.772 1.87 0 0.739-0.265 1.37-0.792 1.9l0.809 0.809c0.524-0.527 1.16-0.792 1.9-0.792 0.735 0 1.36 0.255 1.87 0.763l1.9 1.89c0.521 0.509 0.781 1.13 0.781 1.87 0 0.735-0.255 1.36-0.763 1.87zm-1.25-1.24 1.34-1.35c0.165-0.178 0.248-0.385 0.248-0.624 0-0.245-0.0862-0.455-0.258-0.626l-1.9-1.89c-0.172-0.172-0.38-0.257-0.625-0.257-0.249-3.1e-5 -0.466 0.0966-0.653 0.286l0.577 0.577c0.353 0.353 0.353 0.922 0 1.28-0.353 0.353-0.922 0.353-1.28 0l-0.577-0.577c-0.184 0.182-0.278 0.401-0.278 0.654 0 0.251 0.0824 0.459 0.248 0.624l1.91 1.91c0.172 0.171 0.38 0.257 0.625 0.257 0.239 0 0.444-0.0849 0.615-0.257zm-6.43-6.5-0.577-0.577c-0.353-0.353-0.353-0.922 0-1.28 0.353-0.353 0.922-0.353 1.28 0l0.575 0.575c0.184-0.18 0.279-0.394 0.279-0.643-6e-7 -0.245-0.0849-0.454-0.257-0.625l-1.91-1.91c-0.172-0.172-0.38-0.257-0.625-0.257-0.239 0-0.444 0.0852-0.615 0.257l-1.34 1.35c-0.159 0.172-0.238 0.379-0.238 0.624 2e-7 0.251 0.0811 0.46 0.247 0.625l1.9 1.89c0.172 0.172 0.38 0.257 0.625 0.257 0.253-9e-7 0.473-0.0991 0.661-0.295z"/></svg>'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'menu_item',
|
||||||
|
label: 'Strikethrough',
|
||||||
|
cmd: () => this.doAction('strikethrough', 'S'),
|
||||||
|
key: 'Shift + 7',
|
||||||
|
matches: 'S',
|
||||||
|
icon: '<svg width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="m10.1 15.9q-1.5 0-2.62-0.875t-1.5-2.31l1.69-0.688q0.333 1.06 0.969 1.6 0.635 0.542 1.51 0.542 0.937 0 1.49-0.448 0.552-0.448 0.552-1.2 0-0.333-0.125-0.615t-0.375-0.51h2.15q0.104 0.229 0.135 0.49t0.0312 0.615q0 1.5-1.08 2.45-1.08 0.948-2.81 0.948zm-8.12-6v-1.5h16v1.5zm8-6q1.33 0 2.21 0.562t1.42 1.79l-1.62 0.708q-0.229-0.625-0.76-1.01-0.531-0.385-1.2-0.385-0.771 0-1.28 0.375-0.51 0.375-0.552 0.958h-1.81q0.0417-1.31 1.06-2.16 1.02-0.844 2.54-0.844z"/></svg>'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'menu_item',
|
||||||
|
label: 'Superscript',
|
||||||
|
cmd: () => this.doAction('superscript', 'SUP'),
|
||||||
|
key: 'Shift + 6',
|
||||||
|
matches: 'SUP',
|
||||||
|
icon: '<svg width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="m13.4 8v-1.99q0-0.424 0.288-0.715 0.288-0.291 0.712-0.292h1v-1h-2v-1h2q0.425 0 0.712 0.287 0.288 0.286 0.288 0.71v0.997q0 0.424-0.288 0.715-0.288 0.292-0.712 0.292h-1v1h2v1zm-9.38 8 3.31-5.21-3.08-4.79h1.89l2.21 3.56h0.0833l2.21-3.56h1.9l-3.1 4.79 3.33 5.21h-1.9l-2.44-3.88h-0.0833l-2.44 3.88z"/></svg>'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'menu_item',
|
||||||
|
label: 'Subscript',
|
||||||
|
cmd: () => this.doAction('subscript', 'SUB'),
|
||||||
|
key: 'Shift + 5',
|
||||||
|
matches: 'SUB',
|
||||||
|
icon: '<svg width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="m13.4 18v-1.99q0-0.424 0.288-0.715 0.288-0.292 0.712-0.292h1v-1h-2v-1h2q0.425 0 0.712 0.287 0.288 0.286 0.288 0.71v0.997q0 0.424-0.288 0.715-0.288 0.292-0.712 0.292h-1v1h2v1zm-9.38-3 3.31-5.21-3.08-4.79h1.89l2.21 3.56h0.0833l2.21-3.56h1.9l-3.1 4.79 3.33 5.21h-1.9l-2.44-3.88h-0.0833l-2.44 3.88z"/></svg>'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'menu_item',
|
||||||
|
label: 'Left to Right',
|
||||||
|
cmd: () => squire.setTextDirection('ltr'),
|
||||||
|
icon: '<svg width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="m8 12v-4q-1.25 0-2.12-0.875-0.875-0.875-0.875-2.12t0.875-2.12q0.875-0.875 2.11-0.875h6.01v1.5h-1.5v8.5h-1.5v-8.5h-1.5v8.5zm6 6-1.06-1.06 1.19-1.19h-11.1v-1.5h11.1l-1.19-1.19 1.06-1.06 3 3zm-6-11.5v-3q-0.625 0-1.06 0.442-0.438 0.442-0.438 1.06 0 0.621 0.441 1.06 0.441 0.437 1.06 0.437z"/></svg>'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'menu_item',
|
||||||
|
label: 'Right to Left',
|
||||||
|
cmd: () => squire.setTextDirection('rtl'),
|
||||||
|
icon: '<svg width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="m6 18-3-3 3-3 1.06 1.06-1.19 1.19h11.1v1.5h-11.1l1.19 1.19zm2-6v-4q-1.25 0-2.12-0.875-0.875-0.875-0.875-2.12t0.875-2.12q0.875-0.875 2.11-0.875h6.01v1.5h-1.5v8.5h-1.5v-8.5h-1.5v8.5zm0-5.5v-3q-0.625 0-1.06 0.442-0.438 0.442-0.438 1.06 0 0.621 0.441 1.06 0.441 0.437 1.06 0.437z"/></svg>'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'menu_item',
|
||||||
|
label: 'HTML Mode',
|
||||||
|
id: 'menu-item-mode-wysiwyg',
|
||||||
|
cmd: () => this.setMode('wysiwyg'),
|
||||||
|
showInPlainMode: true,
|
||||||
|
icon: '<svg width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="m2 2v3h1v-2h2v-1zm13 0v1h2v2h1v-3zm-9 3v10h2v-4h4v4h2v-10h-2v4h-4v-4zm-4 10v3h3v-1h-2v-2zm15 0v2h-2v1h3v-3z"/></svg>'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'menu_item',
|
||||||
|
label: 'Edit Source',
|
||||||
|
id: 'menu-item-mode-source',
|
||||||
|
cmd: () => this.setMode('source'),
|
||||||
|
showInPlainMode: true,
|
||||||
|
icon: '<svg width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="m12 2.83c-0.478-0.138-0.976 0.141-1.11 0.619l-3.6 12.6c-0.138 0.478 0.141 0.976 0.619 1.11 0.478 0.138 0.976-0.141 1.11-0.619l3.6-12.6c0.138-0.478-0.141-0.976-0.619-1.11zm2.27 4.65 2.51 2.51-2.51 2.51c-0.352 0.352-0.352 0.923 0 1.27 0.352 0.352 0.923 0.352 1.27 0l3.15-3.15c0.352-0.352 0.352-0.923 0-1.27l-3.15-3.15c-0.352-0.352-0.923-0.352-1.27-0.00141-0.35 0.35-0.35 0.921 0.00141 1.27zm-8.63-1.27c-0.352-0.352-0.923-0.352-1.27 0l-3.15 3.15c-0.352 0.352-0.352 0.923 0 1.27l3.15 3.15c0.352 0.352 0.923 0.352 1.27 0 0.352-0.352 0.352-0.923 0-1.27l-2.51-2.51 2.51-2.51c0.352-0.352 0.352-0.923 0-1.27z"/></svg>'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'menu_item',
|
||||||
|
label: 'Plain Text Mode',
|
||||||
|
id: 'menu-item-mode-plain',
|
||||||
|
cmd: () => this.setMode('plain'),
|
||||||
|
showInPlainMode: true,
|
||||||
|
icon: '<svg width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="m2 2v3h1v-2h2v-1zm13 0v1h2v2h1v-3zm-9 3v2h3v8h2v-8h3v-2zm-4 10v3h3v-1h-2v-2zm15 0v2h-2v1h3v-3z"/></svg>'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
// // clear: {
|
||||||
|
// // removeStyle: {
|
||||||
|
// // html: '⎚',
|
||||||
|
// // cmd: () => squire.setStyle()
|
||||||
|
// // }
|
||||||
|
// // }
|
||||||
|
|
||||||
|
dispatchEvent(new CustomEvent('squire2-toolbar', {
|
||||||
|
detail: {
|
||||||
|
squire: this,
|
||||||
|
actions: actions
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
this.indicators = this.#addActionsToParent(actions, toolbar);
|
||||||
|
return actions;
|
||||||
|
}
|
||||||
|
|
||||||
|
#makeClr() {
|
||||||
|
/**@type {HTMLInputElement} clr*/
|
||||||
|
const clr = createElement('input');
|
||||||
|
clr.type = 'color';
|
||||||
|
// Chrome https://github.com/the-djmaze/snappymail/issues/1199
|
||||||
|
let clrid = 'squire-colors',
|
||||||
|
colorlist = doc.getElementById(clrid),
|
||||||
|
add = hex => colorlist.append(new Option(hex));
|
||||||
|
if (!colorlist) {
|
||||||
|
colorlist = createElement('datalist');
|
||||||
|
colorlist.id = clrid;
|
||||||
|
// Color blind safe Tableau 10 by Maureen Stone
|
||||||
|
add('#4E79A7');
|
||||||
|
add('#F28E2B');
|
||||||
|
add('#E15759');
|
||||||
|
add('#76B7B2');
|
||||||
|
add('#59A14F');
|
||||||
|
add('#EDC948');
|
||||||
|
add('#B07AA1');
|
||||||
|
add('#FF9DA7');
|
||||||
|
add('#9C755F');
|
||||||
|
add('#BAB0AC');
|
||||||
|
doc.body.append(colorlist);
|
||||||
|
}
|
||||||
|
clr.setAttribute('list', clrid);
|
||||||
|
return clr;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Array} items
|
||||||
|
* @param {HTMLElement} parent
|
||||||
|
*/
|
||||||
|
#addActionsToParent(items, parent) {
|
||||||
|
const indicators = [];
|
||||||
|
items.forEach(item => {
|
||||||
|
let element, event;
|
||||||
|
switch (item.type) {
|
||||||
|
case 'group':
|
||||||
|
const group = createElement('div');
|
||||||
|
group.className = 'btn-group';
|
||||||
|
if (!item.showInPlainMode) {
|
||||||
|
group.className += ' squire-html-mode-item';
|
||||||
|
}
|
||||||
|
if (item.items) {
|
||||||
|
indicators.push(...this.#addActionsToParent(item.items, group));
|
||||||
|
}
|
||||||
|
parent.append(group);
|
||||||
|
return indicators;
|
||||||
|
case 'menu':
|
||||||
|
case 'menu_more':
|
||||||
|
const menuWrap = createElement('div');
|
||||||
|
menuWrap.className = 'btn-group dropdown squire-toolbar-menu-wrap';
|
||||||
|
menuWrap.title = item.label;
|
||||||
|
if (!item.showInPlainMode) {
|
||||||
|
menuWrap.className += ' squire-html-mode-item';
|
||||||
|
}
|
||||||
|
const menuBtn = createElement('button');
|
||||||
|
menuBtn.type = 'button';
|
||||||
|
menuBtn.className = 'btn dropdown-toggle';
|
||||||
|
if (item.icon !== '') {
|
||||||
|
menuBtn.innerHTML = item.icon;
|
||||||
|
menuBtn.firstElementChild.setAttribute('class', 'squire-toolbar-svg-icon');
|
||||||
|
} else {
|
||||||
|
menuBtn.className += ' fontastic';
|
||||||
|
menuBtn.textContent = '☰';
|
||||||
|
}
|
||||||
|
menuWrap.appendChild(menuBtn);
|
||||||
|
|
||||||
|
const menu = createElement('ul');
|
||||||
|
menu.className = 'dropdown-menu squire-toolbar-menu';
|
||||||
|
if (item.rightEdge) {
|
||||||
|
menu.className += ' right-edge';
|
||||||
|
}
|
||||||
|
menu.setAttribute('role', 'menu');
|
||||||
|
|
||||||
|
if (item.items) {
|
||||||
|
indicators.push(...this.#addActionsToParent(item.items, menu));
|
||||||
|
}
|
||||||
|
menuWrap.appendChild(menu);
|
||||||
|
parent.append(menuWrap);
|
||||||
|
ko.applyBindingAccessorsToNode(menuWrap, { registerBootstrapDropdown: true });
|
||||||
|
item.element = menuWrap;
|
||||||
|
return indicators;
|
||||||
|
case 'move_parent':
|
||||||
|
// we only move into main composer not the signature composer
|
||||||
|
if (this.container.className.indexOf('e-signature-place') === -1) {
|
||||||
|
element = doc.getElementById(item.id);
|
||||||
|
if (element) {
|
||||||
|
element.className = 'btn';
|
||||||
|
if (item.icon) {
|
||||||
|
element.innerHTML = item.icon;
|
||||||
|
element.firstElementChild.setAttribute('class', 'squire-toolbar-svg-icon');
|
||||||
|
}
|
||||||
|
if (item.label) {
|
||||||
|
element.title = item.label;
|
||||||
|
}
|
||||||
|
element.parentElement.className += ' ' + item.id + '-parent';
|
||||||
|
parent.append(element.parentElement);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
case 'button':
|
||||||
|
element = createElement('button');
|
||||||
|
element.type = 'button';
|
||||||
|
element.className = 'btn';
|
||||||
|
element.innerHTML = item.icon;
|
||||||
|
element.firstElementChild.setAttribute('class', 'squire-toolbar-svg-icon');
|
||||||
|
event = 'click';
|
||||||
|
break;
|
||||||
|
case 'select':
|
||||||
|
element = createElement('select');
|
||||||
|
element.className = 'btn';
|
||||||
|
element.innerHTML = item.icon;
|
||||||
|
event = 'input';
|
||||||
|
if (Array.isArray(item.items)) {
|
||||||
|
item.items.forEach(value => {
|
||||||
|
value = Array.isArray(value) ? value : [value, value];
|
||||||
|
const option = new Option(value[0], value[1]);
|
||||||
|
option.style[item.prop] = value[1];
|
||||||
|
element.append(option);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
Object.entries(item.items).forEach(([label, options]) => {
|
||||||
|
const optgroup = createElement('optgroup');
|
||||||
|
optgroup.label = label;
|
||||||
|
Object.entries(options).forEach(([text, value]) => {
|
||||||
|
const option = new Option(text, value);
|
||||||
|
option.style[item.prop] = value;
|
||||||
|
optgroup.append(option);
|
||||||
|
});
|
||||||
|
element.append(optgroup);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (item.defaultValueIndex) {
|
||||||
|
element.selectedIndex = item.defaultValueIndex;
|
||||||
|
}
|
||||||
|
item.element = element;
|
||||||
|
break;
|
||||||
|
case 'menu_item':
|
||||||
|
element = createElement('li');
|
||||||
|
element.className = 'squire-toolbar-menu-item';
|
||||||
|
if (!item.showInPlainMode) {
|
||||||
|
element.className += ' squire-html-mode-item';
|
||||||
|
}
|
||||||
|
element.innerHTML = item.icon + '<span>' + item.label + '</span>';
|
||||||
|
element.firstElementChild.setAttribute('class', 'squire-toolbar-svg-icon squire-toolbar-menu-item-icon');
|
||||||
|
event = 'click';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
element.title = item.label + (item.key ? ' (' + ctrlKey + item.key + ')' : '');
|
||||||
|
element.tabIndex = -1;
|
||||||
|
element.addEventListener(event, () => item.cmd(element));
|
||||||
|
if (item.id) {
|
||||||
|
element.id = item.id;
|
||||||
|
}
|
||||||
|
if (item.matches) {
|
||||||
|
indicators.push({
|
||||||
|
element: element,
|
||||||
|
selectors: item.matches.split(',')
|
||||||
|
});
|
||||||
|
}
|
||||||
|
parent.append(element);
|
||||||
|
});
|
||||||
|
return indicators;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Plugins might add their own pathChange listeners therefore they should
|
||||||
|
* use this utility function. @see example below
|
||||||
|
* @param {Object} eventDetail detail of pathChange event
|
||||||
|
* @returns {Map<any, any>}
|
||||||
|
*/
|
||||||
|
buildTokensMap(eventDetail) {
|
||||||
|
if (!eventDetail.tokensMap) {
|
||||||
|
const tokensMap = new Map();
|
||||||
|
if (eventDetail.path !== '(selection)') {
|
||||||
|
window.parsel.tokenize(eventDetail.path).forEach(token => {
|
||||||
|
if (token.type === 'type') {
|
||||||
|
// token.name is a tag like B, I, UL, etc...
|
||||||
|
tokensMap.set(token.name, '1');
|
||||||
|
} else if (token.name === 'fontFamily') {
|
||||||
|
// token.value can be a string like '"LucidaSansUnicode","DejaVuSans","BitstreamVeraSans",sans-serif'
|
||||||
|
tokensMap.set('__font_family__', token.value);
|
||||||
|
} else if (token.name === 'fontSize') {
|
||||||
|
// token.value can be a string like '24px' or 'Large'
|
||||||
|
tokensMap.set('__font_size__', token.value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
tokensMap.set('__selection__', '1');
|
||||||
|
}
|
||||||
|
eventDetail.tokensMap = tokensMap;
|
||||||
|
}
|
||||||
|
return eventDetail.tokensMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
doAction(name, tag) {
|
||||||
|
if (tag && this.squire.hasFormat(tag)) {
|
||||||
|
// ex: bold -> removeBold
|
||||||
|
name = 'remove' + name.charAt(0).toUpperCase() + name.slice(1);
|
||||||
|
}
|
||||||
|
this.squire[name]();
|
||||||
|
}
|
||||||
|
|
||||||
|
doList(type) {
|
||||||
|
if (this.squire.hasFormat(type)) {
|
||||||
|
this.squire.removeList();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (type === 'UL') {
|
||||||
|
this.squire.makeUnorderedList();
|
||||||
|
} else if (type === 'OL') {
|
||||||
|
this.squire.makeOrderedList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
changeLevel(incDec) {
|
||||||
|
const type = ['UL', 'OL'].some(listTag => this.squire.hasFormat(listTag))
|
||||||
|
? 'List'
|
||||||
|
: 'Quote';
|
||||||
|
this.squire[incDec + type + 'Level']();
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
testPresenceinSelection(format, validation) {
|
||||||
|
return validation.test(this.squire.getPath()) || this.squire.hasFormat(format);
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
setMode(mode) {
|
||||||
|
if (this.mode !== mode) {
|
||||||
|
let cl = this.container.classList,
|
||||||
|
source = 'source' === this.mode;
|
||||||
|
cl.remove('squire2-mode-' + this.mode);
|
||||||
|
if ('plain' === mode) {
|
||||||
|
this.plain.value = htmlToPlain(source ? this.plain.value : this.squire.getHTML(), true);
|
||||||
|
this.toolbar.classList.add('mode-plain');
|
||||||
|
} else if ('source' === mode) {
|
||||||
|
this.plain.value = this.squire.getHTML();
|
||||||
|
this.toolbar.classList.add('mode-plain');
|
||||||
|
} else {
|
||||||
|
this.setData(source ? this.plain.value : plainToHtml(this.plain.value, true));
|
||||||
|
mode = 'wysiwyg';
|
||||||
|
this.toolbar.classList.remove('mode-plain');
|
||||||
|
}
|
||||||
|
|
||||||
|
doc.getElementById('menu-item-mode-' + this.mode)?.classList.remove('active');
|
||||||
|
doc.getElementById('menu-item-mode-' + mode).classList.add('active');
|
||||||
|
|
||||||
|
this.mode = mode;
|
||||||
|
cl.add('squire2-mode-' + mode);
|
||||||
|
this.onModeChange?.();
|
||||||
|
setTimeout(() => this.focus(), 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
on(type, fn) {
|
||||||
|
if ('mode' === type) {
|
||||||
|
this.onModeChange = fn;
|
||||||
|
} else {
|
||||||
|
this.squire.addEventListener(type, fn);
|
||||||
|
this.plain.addEventListener(type, fn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
execCommand(cmd, cfg) {
|
||||||
|
if ('insertSignature' === cmd) {
|
||||||
|
cfg = Object.assign({
|
||||||
|
clearCache: false,
|
||||||
|
isHtml: false,
|
||||||
|
insertBefore: false,
|
||||||
|
signature: ''
|
||||||
|
}, cfg);
|
||||||
|
|
||||||
|
if (cfg.clearCache) {
|
||||||
|
this._prev_txt_sig = null;
|
||||||
|
} else try {
|
||||||
|
const signature = cfg.isHtml ? htmlToPlain(cfg.signature) : cfg.signature;
|
||||||
|
if ('plain' === this.mode) {
|
||||||
|
let
|
||||||
|
text = this.plain.value,
|
||||||
|
prevSignature = this._prev_txt_sig;
|
||||||
|
if (prevSignature) {
|
||||||
|
text = text.replace(prevSignature, '').trim();
|
||||||
|
}
|
||||||
|
this.plain.value = cfg.insertBefore ? '\n\n' + signature + '\n\n' + text : text + '\n\n' + signature;
|
||||||
|
} else {
|
||||||
|
const squire = this.squire,
|
||||||
|
root = squire.getRoot(),
|
||||||
|
div = createElement('div');
|
||||||
|
div.className = 'rl-signature';
|
||||||
|
div.innerHTML = cfg.isHtml ? cfg.signature : plainToHtml(cfg.signature);
|
||||||
|
root.querySelectorAll('div.rl-signature').forEach(node => node.remove());
|
||||||
|
cfg.insertBefore ? root.prepend(div) : root.append(div);
|
||||||
|
// Move cursor above signature
|
||||||
|
for (let i = 0; i < 2; i++) {
|
||||||
|
const divbr = createElement('div');
|
||||||
|
divbr.append(createElement('br'));
|
||||||
|
div.before(divbr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this._prev_txt_sig = signature;
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getData() {
|
||||||
|
return 'source' === this.mode ? this.plain.value : trimLines(this.squire.getHTML());
|
||||||
|
}
|
||||||
|
|
||||||
|
setData(html) {
|
||||||
|
// this.plain.value = html;
|
||||||
|
const squire = this.squire;
|
||||||
|
squire.setHTML(trimLines(html));
|
||||||
|
const node = squire.getRoot(),
|
||||||
|
range = squire.getSelection();
|
||||||
|
range.setStart(node, 0);
|
||||||
|
range.setEnd(node, 0);
|
||||||
|
squire.setSelection(range);
|
||||||
|
}
|
||||||
|
|
||||||
|
getPlainData() {
|
||||||
|
return this.plain.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
setPlainData(text) {
|
||||||
|
this.plain.value = text;
|
||||||
|
}
|
||||||
|
|
||||||
|
blur() {
|
||||||
|
this.squire.blur();
|
||||||
|
}
|
||||||
|
|
||||||
|
focus() {
|
||||||
|
if ('wysiwyg' === this.mode) {
|
||||||
|
this.squire.focus();
|
||||||
|
} else {
|
||||||
|
this.plain.focus();
|
||||||
|
this.plain.setSelectionRange(0, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})(window);
|
||||||
413
plugins/compact-composer/js/parsel.js
Normal file
413
plugins/compact-composer/js/parsel.js
Normal file
|
|
@ -0,0 +1,413 @@
|
||||||
|
var parsel = (function (exports) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const TOKENS = {
|
||||||
|
attribute: /\[\s*(?:(?<namespace>\*|[-\w\P{ASCII}]*)\|)?(?<name>[-\w\P{ASCII}]+)\s*(?:(?<operator>\W?=)\s*(?<value>.+?)\s*(\s(?<caseSensitive>[iIsS]))?\s*)?\]/gu,
|
||||||
|
id: /#(?<name>[-\w\P{ASCII}]+)/gu,
|
||||||
|
class: /\.(?<name>[-\w\P{ASCII}]+)/gu,
|
||||||
|
comma: /\s*,\s*/g,
|
||||||
|
combinator: /\s*[\s>+~]\s*/g,
|
||||||
|
'pseudo-element': /::(?<name>[-\w\P{ASCII}]+)(?:\((?<argument>¶*)\))?/gu,
|
||||||
|
'pseudo-class': /:(?<name>[-\w\P{ASCII}]+)(?:\((?<argument>¶*)\))?/gu,
|
||||||
|
universal: /(?:(?<namespace>\*|[-\w\P{ASCII}]*)\|)?\*/gu,
|
||||||
|
type: /(?:(?<namespace>\*|[-\w\P{ASCII}]*)\|)?(?<name>[-\w\P{ASCII}]+)/gu, // this must be last
|
||||||
|
};
|
||||||
|
const TRIM_TOKENS = new Set(['combinator', 'comma']);
|
||||||
|
const RECURSIVE_PSEUDO_CLASSES = new Set([
|
||||||
|
'not',
|
||||||
|
'is',
|
||||||
|
'where',
|
||||||
|
'has',
|
||||||
|
'matches',
|
||||||
|
'-moz-any',
|
||||||
|
'-webkit-any',
|
||||||
|
'nth-child',
|
||||||
|
'nth-last-child',
|
||||||
|
]);
|
||||||
|
const nthChildRegExp = /(?<index>[\dn+-]+)\s+of\s+(?<subtree>.+)/;
|
||||||
|
const RECURSIVE_PSEUDO_CLASSES_ARGS = {
|
||||||
|
'nth-child': nthChildRegExp,
|
||||||
|
'nth-last-child': nthChildRegExp,
|
||||||
|
};
|
||||||
|
const getArgumentPatternByType = (type) => {
|
||||||
|
switch (type) {
|
||||||
|
case 'pseudo-element':
|
||||||
|
case 'pseudo-class':
|
||||||
|
return new RegExp(TOKENS[type].source.replace('(?<argument>¶*)', '(?<argument>.*)'), 'gu');
|
||||||
|
default:
|
||||||
|
return TOKENS[type];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
function gobbleParens(text, offset) {
|
||||||
|
let nesting = 0;
|
||||||
|
let result = '';
|
||||||
|
for (; offset < text.length; offset++) {
|
||||||
|
const char = text[offset];
|
||||||
|
switch (char) {
|
||||||
|
case '(':
|
||||||
|
++nesting;
|
||||||
|
break;
|
||||||
|
case ')':
|
||||||
|
--nesting;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
result += char;
|
||||||
|
if (nesting === 0) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
function tokenizeBy(text, grammar = TOKENS) {
|
||||||
|
if (!text) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const tokens = [text];
|
||||||
|
for (const [type, pattern] of Object.entries(grammar)) {
|
||||||
|
for (let i = 0; i < tokens.length; i++) {
|
||||||
|
const token = tokens[i];
|
||||||
|
if (typeof token !== 'string') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
pattern.lastIndex = 0;
|
||||||
|
const match = pattern.exec(token);
|
||||||
|
if (!match) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const from = match.index - 1;
|
||||||
|
const args = [];
|
||||||
|
const content = match[0];
|
||||||
|
const before = token.slice(0, from + 1);
|
||||||
|
if (before) {
|
||||||
|
args.push(before);
|
||||||
|
}
|
||||||
|
args.push({
|
||||||
|
...match.groups,
|
||||||
|
type,
|
||||||
|
content,
|
||||||
|
});
|
||||||
|
const after = token.slice(from + content.length + 1);
|
||||||
|
if (after) {
|
||||||
|
args.push(after);
|
||||||
|
}
|
||||||
|
tokens.splice(i, 1, ...args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let offset = 0;
|
||||||
|
for (const token of tokens) {
|
||||||
|
switch (typeof token) {
|
||||||
|
case 'string':
|
||||||
|
throw new Error(`Unexpected sequence ${token} found at index ${offset}`);
|
||||||
|
case 'object':
|
||||||
|
offset += token.content.length;
|
||||||
|
token.pos = [offset - token.content.length, offset];
|
||||||
|
if (TRIM_TOKENS.has(token.type)) {
|
||||||
|
token.content = token.content.trim() || ' ';
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return tokens;
|
||||||
|
}
|
||||||
|
const STRING_PATTERN = /(['"])([^\\\n]+?)\1/g;
|
||||||
|
const ESCAPE_PATTERN = /\\./g;
|
||||||
|
function tokenize(selector, grammar = TOKENS) {
|
||||||
|
// Prevent leading/trailing whitespaces from being interpreted as combinators
|
||||||
|
selector = selector.trim();
|
||||||
|
if (selector === '') {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const replacements = [];
|
||||||
|
// Replace escapes with placeholders.
|
||||||
|
selector = selector.replace(ESCAPE_PATTERN, (value, offset) => {
|
||||||
|
replacements.push({ value, offset });
|
||||||
|
return '\uE000'.repeat(value.length);
|
||||||
|
});
|
||||||
|
// Replace strings with placeholders.
|
||||||
|
selector = selector.replace(STRING_PATTERN, (value, quote, content, offset) => {
|
||||||
|
replacements.push({ value, offset });
|
||||||
|
return `${quote}${'\uE001'.repeat(content.length)}${quote}`;
|
||||||
|
});
|
||||||
|
// Replace parentheses with placeholders.
|
||||||
|
{
|
||||||
|
let pos = 0;
|
||||||
|
let offset;
|
||||||
|
while ((offset = selector.indexOf('(', pos)) > -1) {
|
||||||
|
const value = gobbleParens(selector, offset);
|
||||||
|
replacements.push({ value, offset });
|
||||||
|
selector = `${selector.substring(0, offset)}(${'¶'.repeat(value.length - 2)})${selector.substring(offset + value.length)}`;
|
||||||
|
pos = offset + value.length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Now we have no nested structures and we can parse with regexes
|
||||||
|
const tokens = tokenizeBy(selector, grammar);
|
||||||
|
// Replace placeholders in reverse order.
|
||||||
|
const changedTokens = new Set();
|
||||||
|
for (const replacement of replacements.reverse()) {
|
||||||
|
for (const token of tokens) {
|
||||||
|
const { offset, value } = replacement;
|
||||||
|
if (!(token.pos[0] <= offset &&
|
||||||
|
offset + value.length <= token.pos[1])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const { content } = token;
|
||||||
|
const tokenOffset = offset - token.pos[0];
|
||||||
|
token.content =
|
||||||
|
content.slice(0, tokenOffset) +
|
||||||
|
value +
|
||||||
|
content.slice(tokenOffset + value.length);
|
||||||
|
if (token.content !== content) {
|
||||||
|
changedTokens.add(token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Update changed tokens.
|
||||||
|
for (const token of changedTokens) {
|
||||||
|
const pattern = getArgumentPatternByType(token.type);
|
||||||
|
if (!pattern) {
|
||||||
|
throw new Error(`Unknown token type: ${token.type}`);
|
||||||
|
}
|
||||||
|
pattern.lastIndex = 0;
|
||||||
|
const match = pattern.exec(token.content);
|
||||||
|
if (!match) {
|
||||||
|
throw new Error(`Unable to parse content for ${token.type}: ${token.content}`);
|
||||||
|
}
|
||||||
|
Object.assign(token, match.groups);
|
||||||
|
}
|
||||||
|
return tokens;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Convert a flat list of tokens into a tree of complex & compound selectors
|
||||||
|
*/
|
||||||
|
function nestTokens(tokens, { list = true } = {}) {
|
||||||
|
if (list && tokens.find((t) => t.type === 'comma')) {
|
||||||
|
const selectors = [];
|
||||||
|
const temp = [];
|
||||||
|
for (let i = 0; i < tokens.length; i++) {
|
||||||
|
if (tokens[i].type === 'comma') {
|
||||||
|
if (temp.length === 0) {
|
||||||
|
throw new Error('Incorrect comma at ' + i);
|
||||||
|
}
|
||||||
|
selectors.push(nestTokens(temp, { list: false }));
|
||||||
|
temp.length = 0;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
temp.push(tokens[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (temp.length === 0) {
|
||||||
|
throw new Error('Trailing comma');
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
selectors.push(nestTokens(temp, { list: false }));
|
||||||
|
}
|
||||||
|
return { type: 'list', list: selectors };
|
||||||
|
}
|
||||||
|
for (let i = tokens.length - 1; i >= 0; i--) {
|
||||||
|
let token = tokens[i];
|
||||||
|
if (token.type === 'combinator') {
|
||||||
|
let left = tokens.slice(0, i);
|
||||||
|
let right = tokens.slice(i + 1);
|
||||||
|
return {
|
||||||
|
type: 'complex',
|
||||||
|
combinator: token.content,
|
||||||
|
left: nestTokens(left),
|
||||||
|
right: nestTokens(right),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
switch (tokens.length) {
|
||||||
|
case 0:
|
||||||
|
throw new Error('Could not build AST.');
|
||||||
|
case 1:
|
||||||
|
// If we're here, there are no combinators, so it's just a list.
|
||||||
|
return tokens[0];
|
||||||
|
default:
|
||||||
|
return {
|
||||||
|
type: 'compound',
|
||||||
|
list: [...tokens], // clone to avoid pointers messing up the AST
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Traverse an AST in depth-first order
|
||||||
|
*/
|
||||||
|
function* flatten(node,
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
parent) {
|
||||||
|
switch (node.type) {
|
||||||
|
case 'list':
|
||||||
|
for (let child of node.list) {
|
||||||
|
yield* flatten(child, node);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'complex':
|
||||||
|
yield* flatten(node.left, node);
|
||||||
|
yield* flatten(node.right, node);
|
||||||
|
break;
|
||||||
|
case 'compound':
|
||||||
|
yield* node.list.map((token) => [token, node]);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
yield [node, parent];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Traverse an AST (or part thereof), in depth-first order
|
||||||
|
*/
|
||||||
|
function walk(node, visit,
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
parent) {
|
||||||
|
if (!node) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const [token, ast] of flatten(node, parent)) {
|
||||||
|
visit(token, ast);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Parse a CSS selector
|
||||||
|
*
|
||||||
|
* @param selector - The selector to parse
|
||||||
|
* @param options.recursive - Whether to parse the arguments of pseudo-classes like :is(), :has() etc. Defaults to true.
|
||||||
|
* @param options.list - Whether this can be a selector list (A, B, C etc). Defaults to true.
|
||||||
|
*/
|
||||||
|
function parse(selector, { recursive = true, list = true } = {}) {
|
||||||
|
const tokens = tokenize(selector);
|
||||||
|
if (!tokens) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const ast = nestTokens(tokens, { list });
|
||||||
|
if (!recursive) {
|
||||||
|
return ast;
|
||||||
|
}
|
||||||
|
for (const [token] of flatten(ast)) {
|
||||||
|
if (token.type !== 'pseudo-class' || !token.argument) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!RECURSIVE_PSEUDO_CLASSES.has(token.name)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let argument = token.argument;
|
||||||
|
const childArg = RECURSIVE_PSEUDO_CLASSES_ARGS[token.name];
|
||||||
|
if (childArg) {
|
||||||
|
const match = childArg.exec(argument);
|
||||||
|
if (!match) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Object.assign(token, match.groups);
|
||||||
|
argument = match.groups['subtree'];
|
||||||
|
}
|
||||||
|
if (!argument) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Object.assign(token, {
|
||||||
|
subtree: parse(argument, {
|
||||||
|
recursive: true,
|
||||||
|
list: true,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return ast;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Converts the given list or (sub)tree to a string.
|
||||||
|
*/
|
||||||
|
function stringify(listOrNode) {
|
||||||
|
let tokens;
|
||||||
|
if (Array.isArray(listOrNode)) {
|
||||||
|
tokens = listOrNode;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
tokens = [...flatten(listOrNode)].map(([token]) => token);
|
||||||
|
}
|
||||||
|
return tokens.map(token => token.content).join('');
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* To convert the specificity array to a number
|
||||||
|
*/
|
||||||
|
function specificityToNumber(specificity, base) {
|
||||||
|
base = base || Math.max(...specificity) + 1;
|
||||||
|
return (specificity[0] * (base << 1) + specificity[1] * base + specificity[2]);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Calculate specificity of a selector.
|
||||||
|
*
|
||||||
|
* If the selector is a list, the max specificity is returned.
|
||||||
|
*/
|
||||||
|
function specificity(selector) {
|
||||||
|
let ast = selector;
|
||||||
|
if (typeof ast === 'string') {
|
||||||
|
ast = parse(ast, { recursive: true });
|
||||||
|
}
|
||||||
|
if (!ast) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
if (ast.type === 'list' && 'list' in ast) {
|
||||||
|
let base = 10;
|
||||||
|
const specificities = ast.list.map((ast) => {
|
||||||
|
const sp = specificity(ast);
|
||||||
|
base = Math.max(base, ...specificity(ast));
|
||||||
|
return sp;
|
||||||
|
});
|
||||||
|
const numbers = specificities.map((ast) => specificityToNumber(ast, base));
|
||||||
|
return specificities[numbers.indexOf(Math.max(...numbers))];
|
||||||
|
}
|
||||||
|
const ret = [0, 0, 0];
|
||||||
|
for (const [token] of flatten(ast)) {
|
||||||
|
switch (token.type) {
|
||||||
|
case 'id':
|
||||||
|
ret[0]++;
|
||||||
|
break;
|
||||||
|
case 'class':
|
||||||
|
case 'attribute':
|
||||||
|
ret[1]++;
|
||||||
|
break;
|
||||||
|
case 'pseudo-element':
|
||||||
|
case 'type':
|
||||||
|
ret[2]++;
|
||||||
|
break;
|
||||||
|
case 'pseudo-class':
|
||||||
|
if (token.name === 'where') {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (!RECURSIVE_PSEUDO_CLASSES.has(token.name) ||
|
||||||
|
!token.subtree) {
|
||||||
|
ret[1]++;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const sub = specificity(token.subtree);
|
||||||
|
sub.forEach((s, i) => (ret[i] += s));
|
||||||
|
// :nth-child() & :nth-last-child() add (0, 1, 0) to the specificity of their most complex selector
|
||||||
|
if (token.name === 'nth-child' ||
|
||||||
|
token.name === 'nth-last-child') {
|
||||||
|
ret[1]++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.RECURSIVE_PSEUDO_CLASSES = RECURSIVE_PSEUDO_CLASSES;
|
||||||
|
exports.RECURSIVE_PSEUDO_CLASSES_ARGS = RECURSIVE_PSEUDO_CLASSES_ARGS;
|
||||||
|
exports.TOKENS = TOKENS;
|
||||||
|
exports.TRIM_TOKENS = TRIM_TOKENS;
|
||||||
|
exports.flatten = flatten;
|
||||||
|
exports.gobbleParens = gobbleParens;
|
||||||
|
exports.parse = parse;
|
||||||
|
exports.specificity = specificity;
|
||||||
|
exports.specificityToNumber = specificityToNumber;
|
||||||
|
exports.stringify = stringify;
|
||||||
|
exports.tokenize = tokenize;
|
||||||
|
exports.tokenizeBy = tokenizeBy;
|
||||||
|
exports.walk = walk;
|
||||||
|
|
||||||
|
Object.defineProperty(exports, '__esModule', { value: true });
|
||||||
|
|
||||||
|
return exports;
|
||||||
|
|
||||||
|
}({}));
|
||||||
4102
plugins/compact-composer/js/squire-raw.js
Normal file
4102
plugins/compact-composer/js/squire-raw.js
Normal file
File diff suppressed because it is too large
Load diff
BIN
plugins/compact-composer/screenshots/Screenshot1.png
Normal file
BIN
plugins/compact-composer/screenshots/Screenshot1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 88 KiB |
BIN
plugins/compact-composer/screenshots/Screenshot2.png
Normal file
BIN
plugins/compact-composer/screenshots/Screenshot2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 61 KiB |
BIN
plugins/compact-composer/screenshots/Screenshot3.png
Normal file
BIN
plugins/compact-composer/screenshots/Screenshot3.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 62 KiB |
BIN
plugins/compact-composer/screenshots/Screenshot4.png
Normal file
BIN
plugins/compact-composer/screenshots/Screenshot4.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 59 KiB |
BIN
plugins/compact-composer/screenshots/Screenshot5.png
Normal file
BIN
plugins/compact-composer/screenshots/Screenshot5.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 60 KiB |
BIN
plugins/compact-composer/screenshots/Screenshot6.png
Normal file
BIN
plugins/compact-composer/screenshots/Screenshot6.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 91 KiB |
201
plugins/compact-composer/templates/PopupsCompactCompose.html
Normal file
201
plugins/compact-composer/templates/PopupsCompactCompose.html
Normal file
|
|
@ -0,0 +1,201 @@
|
||||||
|
<header class="g-ui-user-select-none" data-bind="css: {loading: saving() || sending()}">
|
||||||
|
<a class="btn" data-bind="command: sendCommand, tooltipErrorTip: sendErrorDesc, css: {'btn-success': sendButtonSuccess, 'btn-danger': sendError, 'btn-warning': sendSuccessButSaveError }">
|
||||||
|
<i data-bind="css: {'icon-paper-plane': !sending(), 'icon-spinner': sending()}"></i>
|
||||||
|
<span class="hide-mobile" data-i18n="COMPOSE/BUTTON_SEND"></span>
|
||||||
|
</a>
|
||||||
|
<a class="btn button-save" data-bind="command: saveCommand, tooltipErrorTip: savedErrorDesc, css: {'btn-danger': savedError }">
|
||||||
|
<i class="fontastic" data-bind="css: {'icon-spinner': saving()}">💾</i>
|
||||||
|
<span class="hide-mobile" data-i18n="GLOBAL/SAVE"></span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a class="btn btn-danger button-delete fontastic" data-bind="command: deleteCommand">🗑</a>
|
||||||
|
<span class="saved-text hide-mobile" data-bind="text: savedTimeText"></span>
|
||||||
|
|
||||||
|
<div class="pull-right">
|
||||||
|
<a class="btn hide-mobile" data-i18n="GLOBAL/BCC" data-bind="visible: !showBcc(), toggle: showBcc"></a>
|
||||||
|
<a class="btn hide-mobile" data-i18n="GLOBAL/CC" data-bind="visible: !showCc(), toggle: showCc"></a>
|
||||||
|
<a class="btn fontastic" data-bind="visible: allowContacts, command: contactsCommand" data-i18n="[title]GLOBAL/CONTACTS">📇</a>
|
||||||
|
<div class="btn-group dropdown" data-bind="registerBootstrapDropdown: true" style="display:inline-block;vertical-align:top">
|
||||||
|
<a class="btn dropdown-toggle fontastic">☰</a>
|
||||||
|
<menu class="dropdown-menu right-edge" role="menu">
|
||||||
|
<li data-bind="toggle: showBcc">
|
||||||
|
<a>
|
||||||
|
<i class="fontastic" data-bind="text: showBcc() ? '☑' : '☐'"></i>
|
||||||
|
<span data-i18n="GLOBAL/BCC"></span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li data-bind="toggle: showCc">
|
||||||
|
<a>
|
||||||
|
<i class="fontastic" data-bind="text: showCc() ? '☑' : '☐'"></i>
|
||||||
|
<span data-i18n="GLOBAL/CC"></span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li data-bind="toggle: showReplyTo">
|
||||||
|
<a>
|
||||||
|
<i class="fontastic" data-bind="text: showReplyTo() ? '☑' : '☐'"></i>
|
||||||
|
<span data-i18n="GLOBAL/REPLY_TO"></span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li data-bind="toggle: requestReadReceipt">
|
||||||
|
<a>
|
||||||
|
<i class="fontastic" data-bind="text: requestReadReceipt() ? '☑' : '☐'"></i>
|
||||||
|
<span data-i18n="COMPOSE/BUTTON_REQUEST_READ_RECEIPT"></span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li data-bind="toggle: requestDsn">
|
||||||
|
<a>
|
||||||
|
<i class="fontastic" data-bind="text: requestDsn() ? '☑' : '☐'"></i>
|
||||||
|
<span data-i18n="COMPOSE/BUTTON_REQUEST_DSN"></span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li data-bind="toggle: requireTLS">
|
||||||
|
<a>
|
||||||
|
<i class="fontastic" data-bind="text: requireTLS() ? '☑' : '☐'"></i>
|
||||||
|
<span data-i18n="COMPOSE/BUTTON_REQUIRE_TLS"></span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li data-bind="toggle: markAsImportant">
|
||||||
|
<a>
|
||||||
|
<i class="fontastic" data-bind="text: markAsImportant() ? '☑' : '☐'"></i>
|
||||||
|
<span data-i18n="COMPOSE/BUTTON_MARK_AS_IMPORTANT"></span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li data-bind="toggle: doSign, visible: canSign">
|
||||||
|
<a>
|
||||||
|
<i class="fontastic" data-bind="text: doSign() ? '☑' : '☐'"></i>
|
||||||
|
<span data-icon="✍" data-i18n="CRYPTO/SIGN"></span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li data-bind="toggle: doEncrypt, visible: canEncrypt">
|
||||||
|
<a>
|
||||||
|
<i class="fontastic" data-bind="text: doEncrypt() || 'mailvelope' == viewArea() ? '☑' : '☐'"></i>
|
||||||
|
<span data-icon="🔒" data-i18n="CRYPTO/ENCRYPT"></span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</menu>
|
||||||
|
</div>
|
||||||
|
<a class="minimize-custom" data-bind="click: skipCommand" data-i18n="[title]COMPOSE/BUTTON_MINIMIZE"></a>
|
||||||
|
<a class="close" data-bind="click: tryToClose" data-i18n="[title]GLOBAL/CANCEL">×</a>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="b-header g-ui-user-select-none">
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<td data-i18n="GLOBAL/FROM"></td>
|
||||||
|
<td>
|
||||||
|
<!-- ko if: allowIdentities -->
|
||||||
|
<input type="text" data-bind="textInput: from" style="width:calc(100% - 20px)">
|
||||||
|
<!-- /ko -->
|
||||||
|
<span class="e-identity" data-bind="hidden: allowIdentities, text: from"></span>
|
||||||
|
<!-- ko if: 1 < identitiesOptions().length -->
|
||||||
|
<div class="dropdown" style="display:inline-block" data-bind="registerBootstrapDropdown: true, initDom: identitiesMenu">
|
||||||
|
<a class="dropdown-toggle" href="#" tabindex="-1" id="identity-toggle" role="button"></a>
|
||||||
|
<menu class="dropdown-menu right-edge" role="menu" aria-labelledby="identity-toggle" data-bind="foreach: identitiesOptions">
|
||||||
|
<li role="presentation">
|
||||||
|
<a tabindex="-1" href="#" data-bind="click: function (oIdentity) { $root.selectIdentity(oIdentity); return true; }, text: optText"></a>
|
||||||
|
</li>
|
||||||
|
</menu>
|
||||||
|
</div>
|
||||||
|
<!-- /ko -->
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<label data-bind="css: {'error-to': emptyToError}, tooltipErrorTip: emptyToErrorTooltip"
|
||||||
|
data-i18n="GLOBAL/TO"></label>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<input type="text" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" data-bind="emailsTags: to, autoCompleteSource: emailsSource">
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="cc-row" data-bind="visible: showCc">
|
||||||
|
<td data-i18n="GLOBAL/CC"></td>
|
||||||
|
<td>
|
||||||
|
<input type="text" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" data-bind="emailsTags: cc, autoCompleteSource: emailsSource">
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="bcc-row" data-bind="visible: showBcc">
|
||||||
|
<td data-i18n="GLOBAL/BCC"></td>
|
||||||
|
<td>
|
||||||
|
<input type="text" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" data-bind="emailsTags: bcc, autoCompleteSource: emailsSource">
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="reply-to-row" data-bind="visible: showReplyTo">
|
||||||
|
<td data-i18n="GLOBAL/REPLY_TO"></td>
|
||||||
|
<td>
|
||||||
|
<input type="text" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" data-bind="emailsTags: replyTo">
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td data-i18n="GLOBAL/SUBJECT"></td>
|
||||||
|
<td>
|
||||||
|
<input type="text" name="subject" autocomplete="off" data-bind="textInput: subject, attr:{spellcheck:allowSpellcheck()?'true':'false'}">
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<div style="display:flex">
|
||||||
|
<div class="btn-group" style="flex-grow:1"></div>
|
||||||
|
<div class="btn-group">
|
||||||
|
<a class="btn fontastic" data-bind="toggle: doSign, visible: canSign, css: {'btn-success': doSign()}" data-i18n="[title]CRYPTO/SIGN">
|
||||||
|
✍
|
||||||
|
</a>
|
||||||
|
<a class="btn fontastic" data-bind="toggle: doEncrypt, visible: canEncrypt, css: {'btn-success': doEncrypt() || 'mailvelope' == viewArea()}" data-i18n="[title]CRYPTO/ENCRYPT">
|
||||||
|
🔒
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="btn-group">
|
||||||
|
<button class="btn fontastic" id="composeUploadButton"
|
||||||
|
data-bind="visible: addAttachmentEnabled()" data-i18n="[title]COMPOSE/ATTACH_FILES">
|
||||||
|
⁺📎
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tabs">
|
||||||
|
<input type="radio" name="tabs" value="body" id="tab-body" data-bind="checked: viewArea">
|
||||||
|
<label for="tab-body"
|
||||||
|
role="tab"
|
||||||
|
aria-selected="true"
|
||||||
|
aria-controls="panel1"
|
||||||
|
data-bind="visible: canMailvelope"
|
||||||
|
tabindex="0">
|
||||||
|
<i class="icon-file-text"></i>
|
||||||
|
<span data-i18n="GLOBAL/TEXT"></span>
|
||||||
|
</label>
|
||||||
|
<div class="tab-content" role="tabpanel" aria-hidden="false" style="grid-column-end:3">
|
||||||
|
<div class="textAreaParent" data-bind="initDom: editorArea, attr:{spellcheck:allowSpellcheck()?'true':'false'}"></div>
|
||||||
|
</div>
|
||||||
|
<input type="radio" name="tabs" value="mailvelope" id="tab-mailvelope" data-bind="checked: viewArea">
|
||||||
|
<label for="tab-mailvelope"
|
||||||
|
role="tab"
|
||||||
|
aria-selected="false"
|
||||||
|
aria-controls="panel3"
|
||||||
|
tabindex="0"
|
||||||
|
data-bind="visible: canMailvelope">
|
||||||
|
<i class="mailvelope-icon"></i>
|
||||||
|
<span>Mailvelope</span>
|
||||||
|
</label>
|
||||||
|
<div class="tab-content textAreaParent" id="mailvelope-editor" role="tabpanel" aria-hidden="true" style="grid-column-end:3" data-bind="visible: canMailvelope"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="attachmentAreaParent compact">
|
||||||
|
<div class="b-attachment-place" data-bind="visible: addAttachmentEnabled(), css: {dragAndDropOver: dragAndDropVisible}"
|
||||||
|
data-i18n="COMPOSE/ATTACH_DROP_FILES_DESC"></div>
|
||||||
|
<ul class="attachmentList" data-bind="foreach: attachments">
|
||||||
|
<li class="attachmentItem" data-bind="attr: { title: title }, css: { waiting: waiting, error: '' !== error() }">
|
||||||
|
<div class="attachmentIcon">
|
||||||
|
<i class="iconMain" data-bind="css: iconClass, visible: !uploading() || 0 === progress()"></i>
|
||||||
|
<div class="iconProgress" data-bind="attr: { style: progressStyle }, visible: uploading"></div>
|
||||||
|
<div class="iconBG" data-bind="text: progressText, visible: uploading"></div>
|
||||||
|
</div>
|
||||||
|
<div class="attachmentNameParent">
|
||||||
|
<a href="#" class="close pull-right" style="margin-top:-4px;" data-bind="click: cancel">×</a>
|
||||||
|
<div class="attachmentName" data-bind="text: fileName"></div>
|
||||||
|
<span class="attachmentSize" data-bind="text: friendlySize"></span>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
<?php
|
getEmailAddressDomain<?php
|
||||||
|
|
||||||
class LdapContactsSuggestions implements \RainLoop\Providers\Suggestions\ISuggestions
|
class LdapContactsSuggestions implements \RainLoop\Providers\Suggestions\ISuggestions
|
||||||
{
|
{
|
||||||
|
|
@ -93,17 +93,17 @@ class LdapContactsSuggestions implements \RainLoop\Providers\Suggestions\ISugges
|
||||||
return $aResult;
|
return $aResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
$sDomain = \MailSo\Base\Utils::GetDomainFromEmail($oAccount->Email());
|
$sDomain = \MailSo\Base\Utils::getEmailAddressDomain($oAccount->Email());
|
||||||
$sBaseDn = \strtr($this->sBaseDn, array(
|
$sBaseDn = \strtr($this->sBaseDn, array(
|
||||||
'{domain}' => $sDomain,
|
'{domain}' => $sDomain,
|
||||||
'{domain:dc}' => 'dc='.\strtr($sDomain, array('.' => ',dc=')),
|
'{domain:dc}' => 'dc='.\strtr($sDomain, array('.' => ',dc=')),
|
||||||
'{email}' => $oAccount->Email(),
|
'{email}' => $oAccount->Email(),
|
||||||
'{email:user}' => \MailSo\Base\Utils::GetAccountNameFromEmail($oAccount->Email()),
|
'{email:user}' => \MailSo\Base\Utils::getEmailAddressLocalPart($oAccount->Email()),
|
||||||
'{email:domain}' => $sDomain,
|
'{email:domain}' => $sDomain,
|
||||||
'{login}' => $oAccount->IncLogin(),
|
'{login}' => $oAccount->IncLogin(),
|
||||||
'{imap:login}' => $oAccount->IncLogin(),
|
'{imap:login}' => $oAccount->IncLogin(),
|
||||||
'{imap:host}' => $oAccount->Domain()->IncHost(),
|
'{imap:host}' => $oAccount->Domain()->ImapSettings()->host,
|
||||||
'{imap:port}' => $oAccount->Domain()->IncPort()
|
'{imap:port}' => $oAccount->Domain()->ImapSettings()->port
|
||||||
));
|
));
|
||||||
|
|
||||||
$aObjectClasses = empty($this->sObjectClasses) ? array() : \explode(',', $this->sObjectClasses);
|
$aObjectClasses = empty($this->sObjectClasses) ? array() : \explode(',', $this->sObjectClasses);
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,9 @@ class LdapContactsSuggestionsPlugin extends \RainLoop\Plugins\AbstractPlugin
|
||||||
{
|
{
|
||||||
const
|
const
|
||||||
NAME = 'Contacts suggestions (LDAP)',
|
NAME = 'Contacts suggestions (LDAP)',
|
||||||
VERSION = '2.13',
|
VERSION = '2.14',
|
||||||
RELEASE = '2023-10-01',
|
RELEASE = '2024-03-12',
|
||||||
REQUIRED = '2.23.0',
|
REQUIRED = '2.35.3',
|
||||||
CATEGORY = 'Contacts',
|
CATEGORY = 'Contacts',
|
||||||
DESCRIPTION = 'Get contacts suggestions from LDAP.';
|
DESCRIPTION = 'Get contacts suggestions from LDAP.';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,10 +8,10 @@ class LDAPLoginMappingPlugin extends AbstractPlugin
|
||||||
{
|
{
|
||||||
const
|
const
|
||||||
NAME = 'LDAP login mapping',
|
NAME = 'LDAP login mapping',
|
||||||
VERSION = '2.1',
|
VERSION = '2.2',
|
||||||
AUTHOR = 'RainLoop Team, Ludovic Pouzenc<ludovic@pouzenc.fr>, ZephOne<zephone@protonmail.com>',
|
AUTHOR = 'RainLoop Team, Ludovic Pouzenc<ludovic@pouzenc.fr>, ZephOne<zephone@protonmail.com>',
|
||||||
RELEASE = '2023-01-19',
|
RELEASE = '2024-03-12',
|
||||||
REQUIRED = '2.19.2',
|
REQUIRED = '2.35.3',
|
||||||
CATEGORY = 'Login',
|
CATEGORY = 'Login',
|
||||||
DESCRIPTION = 'Enable custom mapping using ldap field';
|
DESCRIPTION = 'Enable custom mapping using ldap field';
|
||||||
/**
|
/**
|
||||||
|
|
@ -175,7 +175,7 @@ class LDAPLoginMappingPlugin extends AbstractPlugin
|
||||||
'LDAP');
|
'LDAP');
|
||||||
return FALSE;
|
return FALSE;
|
||||||
}
|
}
|
||||||
$sLogin = \MailSo\Base\Utils::GetAccountNameFromEmail($sEmail);
|
$sLogin = \MailSo\Base\Utils::getEmailAddressLocalPart($sEmail);
|
||||||
|
|
||||||
$this->oLogger->Write('ldap_connect: trying...', \LOG_INFO, 'LDAP');
|
$this->oLogger->Write('ldap_connect: trying...', \LOG_INFO, 'LDAP');
|
||||||
|
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue