mirror of
https://github.com/the-djmaze/snappymail.git
synced 2026-09-05 07:27:04 +03:00
Merge branch 'master' into dev/sync-nextcloud-addressbook
This commit is contained in:
commit
2b8f49fe39
189 changed files with 16593 additions and 4813 deletions
|
|
@ -52,6 +52,8 @@ http {
|
||||||
|
|
||||||
server {
|
server {
|
||||||
listen 8888;
|
listen 8888;
|
||||||
|
listen [::]:8888;
|
||||||
|
#listen [::]:8888 ipv6only=off;
|
||||||
root /snappymail;
|
root /snappymail;
|
||||||
index index.php;
|
index index.php;
|
||||||
charset utf-8;
|
charset utf-8;
|
||||||
|
|
|
||||||
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -25,6 +25,8 @@
|
||||||
/vendors/knockout/spec
|
/vendors/knockout/spec
|
||||||
/vendors/openpgp-5
|
/vendors/openpgp-5
|
||||||
!/vendors/openpgp-5/dist
|
!/vendors/openpgp-5/dist
|
||||||
|
/vendors/squire2
|
||||||
|
!/vendors/squire2/dist
|
||||||
/vendors/vanillaqr.js/
|
/vendors/vanillaqr.js/
|
||||||
/integrations/nextcloud/rainloop
|
/integrations/nextcloud/rainloop
|
||||||
/integrations/owncloud/rainloop
|
/integrations/owncloud/rainloop
|
||||||
|
|
|
||||||
4
.gitmodules
vendored
4
.gitmodules
vendored
|
|
@ -1,3 +1,7 @@
|
||||||
[submodule "vendors/openpgp-5"]
|
[submodule "vendors/openpgp-5"]
|
||||||
path = vendors/openpgp-5
|
path = vendors/openpgp-5
|
||||||
url = git@github.com:the-djmaze/openpgpjs.git
|
url = git@github.com:the-djmaze/openpgpjs.git
|
||||||
|
[submodule "vendors/squire2"]
|
||||||
|
path = vendors/squire2
|
||||||
|
url = git@github.com:the-djmaze/Squire.git
|
||||||
|
branch = snappymail
|
||||||
|
|
|
||||||
26
CHANGELOG.md
26
CHANGELOG.md
|
|
@ -1,3 +1,29 @@
|
||||||
|
## 2.38.0 – 2024-09-16
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Remove `tel:` links when converting HTML to plain
|
||||||
|
[#1724](https://github.com/the-djmaze/snappymail/issues/1724)
|
||||||
|
- Convert HTML to Markdown instead of plain, by using [Turndown](https://github.com/mixmark-io/turndown)
|
||||||
|
[#1604](https://github.com/the-djmaze/snappymail/issues/1604)
|
||||||
|
- Check HTMLInputElement.validity() for
|
||||||
|
[#1733](https://github.com/the-djmaze/snappymail/issues/1733)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Use a modified [Squire 2.3.2](https://github.com/the-djmaze/Squire/commits/snappymail/)
|
||||||
|
- cleanHtml use allowedTags instead of disallowedTags and improved CSS handling
|
||||||
|
- Update Portuguese by @ner00
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- mXSS exploit found by SonarSource
|
||||||
|
[CVE-2024-45800](https://github.com/the-djmaze/snappymail/security/advisories/GHSA-2rq7-79vp-ffxm)
|
||||||
|
- Call to a member function Email() on null
|
||||||
|
[#1706](https://github.com/the-djmaze/snappymail/issues/1706)
|
||||||
|
- IMAP capabilities via IMAP Proxy
|
||||||
|
[#1725](https://github.com/the-djmaze/snappymail/issues/1725)
|
||||||
|
- Messages on page setting is not validated against 999 max
|
||||||
|
[#1733](https://github.com/the-djmaze/snappymail/issues/1733)
|
||||||
|
|
||||||
|
|
||||||
## 2.37.3 – 2024-08-27
|
## 2.37.3 – 2024-08-27
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
|
||||||
26
README.md
26
README.md
|
|
@ -141,27 +141,27 @@ RainLoop 1.17 vs SnappyMail
|
||||||
|js/* |RainLoop |Snappy |
|
|js/* |RainLoop |Snappy |
|
||||||
|--------------- |--------: |--------: |
|
|--------------- |--------: |--------: |
|
||||||
|admin.js |2.170.153 | 84.203 |
|
|admin.js |2.170.153 | 84.203 |
|
||||||
|app.js |4.207.787 | 445.359 |
|
|app.js |4.207.787 | 446.887 |
|
||||||
|boot.js | 868.735 | 4.343 |
|
|boot.js | 868.735 | 4.343 |
|
||||||
|libs.js | 658.812 | 193.716 |
|
|libs.js | 658.812 | 236.059 |
|
||||||
|sieve.js | 0 | 84.571 |
|
|sieve.js | 0 | 84.571 |
|
||||||
|polyfills.js | 334.608 | 0 |
|
|polyfills.js | 334.608 | 0 |
|
||||||
|serviceworker.js | 0 | 285 |
|
|serviceworker.js | 0 | 285 |
|
||||||
|TOTAL |8.240.095 | 812.477 |
|
|TOTAL |8.240.095 | 856.348 |
|
||||||
|
|
||||||
|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 | 41.256 | 73.606 | 13.940 | 60.877 | 12.493 |
|
|admin.min.js | 256.831 | 41.256 | 73.606 | 13.940 | 60.877 | 12.493 |
|
||||||
|app.min.js | 515.367 | 201.270 |139.456 | 68.096 |110.485 | 58.078 |
|
|app.min.js | 515.367 | 201.917 |139.456 | 68.450 |110.485 | 58.321 |
|
||||||
|boot.min.js | 84.659 | 2.231 | 26.998 | 1.271 | 23.643 | 1.067 |
|
|boot.min.js | 84.659 | 2.231 | 26.998 | 1.271 | 23.643 | 1.067 |
|
||||||
|libs.min.js | 584.772 | 92.365 |180.901 | 34.487 |155.182 | 30.830 |
|
|libs.min.js | 584.772 | 111.742 |180.901 | 39.882 |155.182 | 35.584 |
|
||||||
|sieve.min.js | 0 | 41.288 | 0 | 10.327 | 0 | 9.318 |
|
|sieve.min.js | 0 | 41.288 | 0 | 10.327 | 0 | 9.318 |
|
||||||
|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 | 295.866 |358.761 |103.854 |299.485 | 89.975 |
|
|TOTAL user |1.217.635 | 315.890 |358.761 |109.603 |299.485 | 94.972 |
|
||||||
|TOTAL user+sieve |1.217.635 | 337.154 |358.761 |114.181 |299.485 | 99.293 |
|
|TOTAL user+sieve |1.217.635 | 357.178 |358.761 |119.930 |299.485 |104.290 |
|
||||||
|TOTAL admin | 959.099 | 135.852 |292.911 | 49.698 |249.877 | 44.390 |
|
|TOTAL admin | 959.099 | 155.229 |292.911 | 55.093 |249.877 | 49.144 |
|
||||||
|
|
||||||
For a user it is around 68% smaller and faster than traditional RainLoop.
|
For a user it is around 67% smaller and faster than traditional RainLoop.
|
||||||
|
|
||||||
### CSS changes
|
### CSS changes
|
||||||
|
|
||||||
|
|
@ -188,12 +188,12 @@ For a user it is around 68% 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.691 | 46.946 | 17.693 | 15.157 |
|
|app.css | 340.331 | 84.850 | 46.946 | 17.710 | 15.161 |
|
||||||
|app.min.css | 274.947 | 68.052 | 39.647 | 15.589 | 13.610 |
|
|app.min.css | 274.947 | 68.192 | 39.647 | 15.602 | 13.626 |
|
||||||
|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.602 | | 7.023 | 6.112 |
|
|admin.css | | 30.761 | | 7.038 | 6.118 |
|
||||||
|admin.min.css | | 24.717 | | 6.346 | 5.586 |
|
|admin.min.css | | 24.857 | | 6.360 | 5.598 |
|
||||||
|
|
||||||
### PGP
|
### PGP
|
||||||
RainLoop uses the old OpenPGP.js v2
|
RainLoop uses the old OpenPGP.js v2
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,8 @@ Currently due to the fast development only the latest version receives security
|
||||||
|
|
||||||
| Version | Supported |
|
| Version | Supported |
|
||||||
| -------- | --------- |
|
| -------- | --------- |
|
||||||
| 2.30.x | ✔ |
|
| 2.34.x | ✔ |
|
||||||
| < 2.30.0 | ❌ |
|
| < 2.34.0 | ❌ |
|
||||||
|
|
||||||
## Reporting a Vulnerability
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,9 @@ import { SettingsUserStore } from 'Stores/User/Settings';
|
||||||
|
|
||||||
const
|
const
|
||||||
tmpl = createElement('template'),
|
tmpl = createElement('template'),
|
||||||
|
|
||||||
|
turndown = new TurndownService(),
|
||||||
|
|
||||||
htmlre = /[&<>"']/g,
|
htmlre = /[&<>"']/g,
|
||||||
httpre = /^(https?:)?\/\//i,
|
httpre = /^(https?:)?\/\//i,
|
||||||
htmlmap = {
|
htmlmap = {
|
||||||
|
|
@ -14,14 +17,34 @@ const
|
||||||
"'": '''
|
"'": '''
|
||||||
},
|
},
|
||||||
|
|
||||||
disallowedTags = [
|
keepTagContent = 'form,button,data', // font
|
||||||
'svg','script','title','link','base','meta',
|
|
||||||
'input','output','select','button','textarea',
|
allowedTags = [
|
||||||
'bgsound','keygen','source','object','embed','applet','iframe','frame','frameset','video','audio','area','map'
|
// Structural Elements:
|
||||||
// not supported by <template> element
|
'blockquote','br','div','figcaption','figure','h1','h2','h3','h4','h5','h6','hgroup','hr','p','wbr',
|
||||||
// ,'html','head','body'
|
'article','aside','header','footer','main','section',
|
||||||
|
'details','summary',
|
||||||
|
// List Elements
|
||||||
|
'dd','dl','dt','li','ol','ul',
|
||||||
|
// Text Formatting Elements
|
||||||
|
'a','abbr','address','b','bdi','bdo','cite','code','del','dfn',
|
||||||
|
'em','i','ins','kbd','mark','pre','q','rp','rt','ruby','s','samp','small',
|
||||||
|
'span','strong','sub','sup','time','u','var',
|
||||||
|
// Deprecated by HTML Standard
|
||||||
|
'acronym','big','center','dir','font','marquee',
|
||||||
|
'nobr','plaintext','rb','rtc','strike','tt',
|
||||||
|
// Media Elements
|
||||||
|
'img',//'picture','source',
|
||||||
|
// Table Elements
|
||||||
|
'caption','col','colgroup','table','tbody','td','tfoot','th','thead','tr',
|
||||||
|
// Disallowed but converted later
|
||||||
|
'style','xmp'
|
||||||
].join(','),
|
].join(','),
|
||||||
|
|
||||||
|
nonEmptyTags = [
|
||||||
|
'A','B','EM','I','SPAN','STRONG'
|
||||||
|
],
|
||||||
|
|
||||||
blockquoteSwitcher = () => {
|
blockquoteSwitcher = () => {
|
||||||
SettingsUserStore.collapseBlockquotes() &&
|
SettingsUserStore.collapseBlockquotes() &&
|
||||||
// tmpl.content.querySelectorAll('blockquote').forEach(node => {
|
// tmpl.content.querySelectorAll('blockquote').forEach(node => {
|
||||||
|
|
@ -99,8 +122,10 @@ const
|
||||||
},
|
},
|
||||||
|
|
||||||
cleanCSS = source =>
|
cleanCSS = source =>
|
||||||
source.trim().replace(/(^|;)\s*-(ms|webkit)-[^;]+(;|$)/g, '')
|
source.trim()
|
||||||
.replace(/white-space[^;]+(;|$)/g, '')
|
.replace(/;\s*-[^;]+/g, '')
|
||||||
|
.replace(/^\s*-[^;]+(;|$)/g, '')
|
||||||
|
.replace(/white-space[^;]+/g, '')
|
||||||
// Drop Microsoft Office style properties
|
// Drop Microsoft Office style properties
|
||||||
// .replace(/mso-[^:;]+:[^;]+/gi, '')
|
// .replace(/mso-[^:;]+:[^;]+/gi, '')
|
||||||
,
|
,
|
||||||
|
|
@ -142,14 +167,15 @@ const
|
||||||
if (source) {
|
if (source) {
|
||||||
source = source
|
source = source
|
||||||
// strip comments
|
// strip comments
|
||||||
.replace(/\/\*[\s\S]*?\*\/|<!--|-->/gi, '')
|
.replace(/\/\*[\s\S]*?\*\//gi, '')
|
||||||
// strip import statements
|
// strip MS Word comments
|
||||||
.replace(/@import .*?;/gi , '')
|
.replace(/<!--[\s\S]*?-->/gi, '')
|
||||||
// strip keyframe statements
|
// strip HTML, as < is no CSS combinator anyway
|
||||||
.replace(/((@.*?keyframes [\s\S]*?){([\s\S]*?}\s*?)})/gi, '');
|
.replace(/<[\s\S]*/gi, '');
|
||||||
|
// .replace(/<\/?[a-z][\s\S]*?>/gi, '');
|
||||||
|
|
||||||
// unified regex to match css & media queries together
|
// unified regex to match css & media queries together
|
||||||
let unified = /((\s*?(?:\/\*[\s\S]*?\*\/)?\s*?@media[\s\S]*?){([\s\S]*?)}\s*?})|(([\s\S]*?){([\s\S]*?)})/gi,
|
let unified = /(?:(\s*?@(?:media)[\s\S]*?){([\s\S]*?)}\s*?})|(?:([\s\S]*?){([\s\S]*?)})/gi,
|
||||||
arr;
|
arr;
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
|
|
@ -158,7 +184,7 @@ const
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
let selector = arr[arr[2] === undefined ? 5 : 2].split('\r\n').join('\n').trim()
|
let selector = arr[arr[2] === undefined ? 3 : 1].split('\r\n').join('\n').trim()
|
||||||
// Never have more than a single line break in a row
|
// Never have more than a single line break in a row
|
||||||
.replace(/\n+/, "\n")
|
.replace(/\n+/, "\n")
|
||||||
// Remove :root and html
|
// Remove :root and html
|
||||||
|
|
@ -170,13 +196,14 @@ const
|
||||||
css.push({
|
css.push({
|
||||||
selector: selector,
|
selector: selector,
|
||||||
type: 'media',
|
type: 'media',
|
||||||
subStyles: parseCSS(arr[3] + '\n}') //recursively parse media query inner css
|
subStyles: parseCSS(arr[2] + '\n}') //recursively parse media query inner css
|
||||||
});
|
});
|
||||||
} else if (selector && !selector.includes('@')) {
|
} else if (selector && !selector.includes('@')) {
|
||||||
// we have standard css
|
// we have standard css
|
||||||
|
// ignores @import, @keyframe, @font-face statements
|
||||||
css.push({
|
css.push({
|
||||||
selector: selector,
|
selector: selector,
|
||||||
rules: cleanCSS(arr[6])
|
rules: cleanCSS(arr[4])
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -255,9 +282,6 @@ export const
|
||||||
'abbr', 'scope',
|
'abbr', 'scope',
|
||||||
// td
|
// td
|
||||||
'colspan', 'rowspan', 'headers'
|
'colspan', 'rowspan', 'headers'
|
||||||
],
|
|
||||||
nonEmptyTags = [
|
|
||||||
'A','B','EM','I','SPAN','STRONG'
|
|
||||||
];
|
];
|
||||||
|
|
||||||
if (SettingsUserStore.allowStyles()) {
|
if (SettingsUserStore.allowStyles()) {
|
||||||
|
|
@ -304,13 +328,21 @@ export const
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// https://github.com/the-djmaze/snappymail/issues/1125
|
||||||
|
tmpl.content.querySelectorAll(keepTagContent).forEach(oElement => replaceWithChildren(oElement));
|
||||||
|
|
||||||
tmpl.content.querySelectorAll(
|
tmpl.content.querySelectorAll(
|
||||||
disallowedTags
|
':not('+allowedTags+')'
|
||||||
+ (0 < bqLevel ? ',' + (new Array(1 + bqLevel).fill('blockquote').join(' ')) : '')
|
+ (0 < bqLevel ? ',' + (new Array(1 + bqLevel).fill('blockquote').join(' ')) : '')
|
||||||
).forEach(oElement => oElement.remove());
|
).forEach(oElement => oElement.remove());
|
||||||
|
/* // Is this slower or faster?
|
||||||
// https://github.com/the-djmaze/snappymail/issues/1125
|
).forEach(oElement => {
|
||||||
tmpl.content.querySelectorAll('form,button').forEach(oElement => replaceWithChildren(oElement));
|
if (!node || !node.contains(oElement)) {
|
||||||
|
oElement.remove();
|
||||||
|
node = oElement;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
*/
|
||||||
|
|
||||||
// https://github.com/the-djmaze/snappymail/issues/1641
|
// https://github.com/the-djmaze/snappymail/issues/1641
|
||||||
let body = tmpl.content.querySelector('.mail-body');
|
let body = tmpl.content.querySelector('.mail-body');
|
||||||
|
|
@ -343,6 +375,13 @@ export const
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ('XMP' === name) {
|
||||||
|
const pre = createElement('pre');
|
||||||
|
pre.innerHTML = encodeHtml(oElement.innerHTML);
|
||||||
|
oElement.replaceWith(pre);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// \MailSo\Base\HtmlUtils::ClearTags()
|
// \MailSo\Base\HtmlUtils::ClearTags()
|
||||||
if ('none' == oStyle.display
|
if ('none' == oStyle.display
|
||||||
|| 'hidden' == oStyle.visibility
|
|| 'hidden' == oStyle.visibility
|
||||||
|
|
@ -429,7 +468,7 @@ export const
|
||||||
}
|
}
|
||||||
|
|
||||||
// if (['CENTER','FORM'].includes(name)) {
|
// if (['CENTER','FORM'].includes(name)) {
|
||||||
if ('O:P' === name || (nonEmptyTags.includes(name) && ('' == oElement.textContent.trim()))) {
|
if (nonEmptyTags.includes(name) && ('' == oElement.textContent.trim())) {
|
||||||
('A' !== name || !oElement.querySelector('IMG')) && replaceWithChildren(oElement);
|
('A' !== name || !oElement.querySelector('IMG')) && replaceWithChildren(oElement);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -604,6 +643,9 @@ export const
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
htmlToPlain = html => {
|
htmlToPlain = html => {
|
||||||
|
if (SettingsUserStore.markdown()) {
|
||||||
|
return htmlToMarkdown(html);
|
||||||
|
}
|
||||||
const
|
const
|
||||||
hr = '⎯'.repeat(64),
|
hr = '⎯'.repeat(64),
|
||||||
forEach = (selector, fn) => tmpl.content.querySelectorAll(selector).forEach(fn),
|
forEach = (selector, fn) => tmpl.content.querySelectorAll(selector).forEach(fn),
|
||||||
|
|
@ -640,6 +682,8 @@ export const
|
||||||
.replace(/<t[dh](\s[\s\S]*?)?>/gi, '\t')
|
.replace(/<t[dh](\s[\s\S]*?)?>/gi, '\t')
|
||||||
.replace(/<\/tr(\s[\s\S]*?)?>/gi, '\n');
|
.replace(/<\/tr(\s[\s\S]*?)?>/gi, '\n');
|
||||||
|
|
||||||
|
forEach('style', node => node.remove());
|
||||||
|
|
||||||
// lines
|
// lines
|
||||||
forEach('hr', node => node.replaceWith(`\n\n${hr}\n\n`));
|
forEach('hr', node => node.replaceWith(`\n\n${hr}\n\n`));
|
||||||
|
|
||||||
|
|
@ -700,6 +744,11 @@ export const
|
||||||
return (tmpl.content.textContent || '').trim();
|
return (tmpl.content.textContent || '').trim();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
htmlToMarkdown = html => {
|
||||||
|
tmpl.innerHTML = html;
|
||||||
|
return turndown.turndown(tmpl.content);
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} plain
|
* @param {string} plain
|
||||||
* @param {boolean} findEmailAndLinksInText = false
|
* @param {boolean} findEmailAndLinksInText = false
|
||||||
|
|
@ -771,5 +820,7 @@ export const
|
||||||
|
|
||||||
rl.Utils = {
|
rl.Utils = {
|
||||||
htmlToPlain: htmlToPlain,
|
htmlToPlain: htmlToPlain,
|
||||||
plainToHtml: plainToHtml
|
plainToHtml: plainToHtml,
|
||||||
|
htmlToMarkdown: htmlToMarkdown
|
||||||
|
// markdownToHtml: md => marked.parse(md)
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,7 @@ export const
|
||||||
|
|
||||||
proxy = url =>
|
proxy = url =>
|
||||||
BASE + '?/ProxyExternal/'
|
BASE + '?/ProxyExternal/'
|
||||||
|
// + btoa(JSON.stringify([token,url]).replace(/ /g, '%20')).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''),
|
||||||
+ btoa(url.replace(/ /g, '%20')).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''),
|
+ btoa(url.replace(/ /g, '%20')).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''),
|
||||||
// + b64EncodeJSONSafe(url.replace(/ /g, '%20')),
|
// + b64EncodeJSONSafe(url.replace(/ /g, '%20')),
|
||||||
|
|
||||||
|
|
|
||||||
30
dev/External/SquireUI.js
vendored
30
dev/External/SquireUI.js
vendored
|
|
@ -109,13 +109,13 @@ class SquireUI
|
||||||
cmd: s => squire.setStyle({ fontFamily: s.value })
|
cmd: s => squire.setStyle({ fontFamily: s.value })
|
||||||
},
|
},
|
||||||
fontSize: {
|
fontSize: {
|
||||||
select: ['11px','13px','16px','20px','24px','30px'],
|
select: [[i18n('GLOBAL/DEFAULT'),''],'11px','13px','16px','20px','24px','30px'],
|
||||||
defaultValueIndex: 2,
|
defaultValueIndex: 0,
|
||||||
cmd: s => squire.setStyle({ fontSize: s.value })
|
cmd: s => squire.setStyle({ fontSize: s.value })
|
||||||
// TODO: maybe consider using https://developer.mozilla.org/en-US/docs/Web/CSS/font-size#values
|
// TODO: maybe consider using https://developer.mozilla.org/en-US/docs/Web/CSS/font-size#values
|
||||||
// example:
|
// example:
|
||||||
// select: ['xx-small', 'x-small',' small',' medium', 'large', 'x-large', 'xx-large', 'xxx-large'],
|
// select: ['','xx-small', 'x-small',' small',' medium', 'large', 'x-large', 'xx-large', 'xxx-large'],
|
||||||
// defaultValueIndex: 3,
|
// defaultValueIndex: 0,
|
||||||
},
|
},
|
||||||
// dir: {
|
// dir: {
|
||||||
// select: [
|
// select: [
|
||||||
|
|
@ -155,7 +155,7 @@ class SquireUI
|
||||||
html: 'B',
|
html: 'B',
|
||||||
cmd: () => this.doAction('bold'),
|
cmd: () => this.doAction('bold'),
|
||||||
key: 'B',
|
key: 'B',
|
||||||
matches: 'B,STRONT'
|
matches: 'B,STRONG'
|
||||||
},
|
},
|
||||||
italic: {
|
italic: {
|
||||||
html: 'I',
|
html: 'I',
|
||||||
|
|
@ -355,6 +355,7 @@ class SquireUI
|
||||||
input.append(option);
|
input.append(option);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
input.add(new Option(i18n('GLOBAL/DEFAULT'), ''));
|
||||||
Object.entries(cfg.select).forEach(([label, options]) => {
|
Object.entries(cfg.select).forEach(([label, options]) => {
|
||||||
let group = createElement('optgroup');
|
let group = createElement('optgroup');
|
||||||
group.label = label;
|
group.label = label;
|
||||||
|
|
@ -363,7 +364,7 @@ class SquireUI
|
||||||
option.style[action] = value;
|
option.style[action] = value;
|
||||||
group.append(option);
|
group.append(option);
|
||||||
});
|
});
|
||||||
input.append(group);
|
input.add(group);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
ev = 'input';
|
ev = 'input';
|
||||||
|
|
@ -477,15 +478,22 @@ class SquireUI
|
||||||
|
|
||||||
// -----
|
// -----
|
||||||
|
|
||||||
squire.addEventListener('pathChange', e => {
|
squire.addEventListener('pathChange', () => {
|
||||||
|
|
||||||
const squireRoot = squire.getRoot();
|
const squireRoot = squire.getRoot();
|
||||||
let elm = e.detail.element;
|
let range = squire.getSelection(),
|
||||||
|
collapsed = range.collapsed,
|
||||||
|
elm = collapsed ? range.endContainer : range?.commonAncestorContainer;
|
||||||
|
if (elm && !(elm instanceof Element)) {
|
||||||
|
elm = elm.parentElement;
|
||||||
|
}
|
||||||
forEachObjectValue(actions, entries => {
|
forEachObjectValue(actions, entries => {
|
||||||
forEachObjectValue(entries, cfg => {
|
forEachObjectValue(entries, cfg => {
|
||||||
// cfg.matches && cfg.input.classList.toggle('active', elm && elm.matches(cfg.matches));
|
// Check if selection has a matching parent or contains a matching element
|
||||||
cfg.matches && cfg.input.classList.toggle('active', elm && elm.closestWithin(cfg.matches, squireRoot));
|
cfg.matches && cfg.input.classList.toggle('active', !!(elm && (
|
||||||
|
(!collapsed && [...elm.querySelectorAll(cfg.matches)].some(node => range.intersectsNode(node)))
|
||||||
|
|| elm.closestWithin(cfg.matches, squireRoot)
|
||||||
|
)));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -245,7 +245,7 @@ export class ContactModel extends AbstractModel {
|
||||||
this.namePrefix(),
|
this.namePrefix(),
|
||||||
this.nameSuffix()
|
this.nameSuffix()
|
||||||
]/*, params, group*/);
|
]/*, params, group*/);
|
||||||
// jCard.parseFullName({set:true});
|
jCard.parseFullName({set:true});
|
||||||
|
|
||||||
['nickname', 'note', 'title'].forEach(field =>
|
['nickname', 'note', 'title'].forEach(field =>
|
||||||
this[field]() ? jCard.set(field, this[field]()/*, params, group*/) : jCard.remove(field)
|
this[field]() ? jCard.set(field, this[field]()/*, params, group*/) : jCard.remove(field)
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ export class AdminSettingsConfig /*extends AbstractViewSettings*/ {
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.config = ko.observableArray();
|
this.config = ko.observableArray();
|
||||||
this.search = ko.observableArray();
|
this.search = ko.observable('');
|
||||||
this.saved = ko.observable(false).extend({ falseTimeout: 5000 });
|
this.saved = ko.observable(false).extend({ falseTimeout: 5000 });
|
||||||
|
|
||||||
this.search.subscribe(value => {
|
this.search.subscribe(value => {
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,8 @@ export class AdminSettingsPackages extends AbstractViewSettings {
|
||||||
this.addSettings(['pluginsEnable']);
|
this.addSettings(['pluginsEnable']);
|
||||||
|
|
||||||
addObservablesTo(this, {
|
addObservablesTo(this, {
|
||||||
packagesError: ''
|
packagesError: '',
|
||||||
|
search: ''
|
||||||
});
|
});
|
||||||
|
|
||||||
this.packages = PackageAdminStore;
|
this.packages = PackageAdminStore;
|
||||||
|
|
@ -30,6 +31,19 @@ export class AdminSettingsPackages extends AbstractViewSettings {
|
||||||
|
|
||||||
visibility: () => (PackageAdminStore.loading() ? 'visible' : 'hidden')
|
visibility: () => (PackageAdminStore.loading() ? 'visible' : 'hidden')
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.search.subscribe(value => {
|
||||||
|
const v = value.toLowerCase(),
|
||||||
|
qsa = (node, selector, fn) => node.querySelectorAll(selector).forEach(fn),
|
||||||
|
match = node => node.textContent.toLowerCase().includes(v);
|
||||||
|
if (v.length) {
|
||||||
|
qsa(this.viewModelDom, 'td:first-child', td => {
|
||||||
|
td.parentNode.hidden = !match(td);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
qsa(this.viewModelDom, 'tr[hidden]', n => n.hidden = false);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
onShow() {
|
onShow() {
|
||||||
|
|
|
||||||
|
|
@ -62,7 +62,7 @@ export class UserSettingsGeneral extends AbstractViewSettings {
|
||||||
'viewHTML', 'viewImages', 'viewImagesWhitelist', 'removeColors', 'allowStyles', 'allowDraftAutosave',
|
'viewHTML', 'viewImages', 'viewImagesWhitelist', 'removeColors', 'allowStyles', 'allowDraftAutosave',
|
||||||
'hideDeleted', 'listInlineAttachments', 'simpleAttachmentsList', 'collapseBlockquotes',
|
'hideDeleted', 'listInlineAttachments', 'simpleAttachmentsList', 'collapseBlockquotes',
|
||||||
'useCheckboxesInList', 'listGrouped', 'replySameFolder', 'allowSpellcheck',
|
'useCheckboxesInList', 'listGrouped', 'replySameFolder', 'allowSpellcheck',
|
||||||
'messageReadAuto', 'showNextMessage', 'messageNewWindow'
|
'messageReadAuto', 'showNextMessage', 'messageNewWindow', 'markdown'
|
||||||
].forEach(name => this[name] = SettingsUserStore[name]);
|
].forEach(name => this[name] = SettingsUserStore[name]);
|
||||||
|
|
||||||
this.allowLanguagesOnSettings = !!SettingsGet('allowLanguagesOnSettings');
|
this.allowLanguagesOnSettings = !!SettingsGet('allowLanguagesOnSettings');
|
||||||
|
|
@ -123,7 +123,7 @@ export class UserSettingsGeneral extends AbstractViewSettings {
|
||||||
'ViewHTML', 'ViewImages', 'ViewImagesWhitelist', 'RemoveColors', 'AllowStyles', 'AllowDraftAutosave',
|
'ViewHTML', 'ViewImages', 'ViewImagesWhitelist', 'RemoveColors', 'AllowStyles', 'AllowDraftAutosave',
|
||||||
'HideDeleted', 'ListInlineAttachments', 'simpleAttachmentsList', 'CollapseBlockquotes',
|
'HideDeleted', 'ListInlineAttachments', 'simpleAttachmentsList', 'CollapseBlockquotes',
|
||||||
'UseCheckboxesInList', 'listGrouped', 'ReplySameFolder', 'allowSpellcheck',
|
'UseCheckboxesInList', 'listGrouped', 'ReplySameFolder', 'allowSpellcheck',
|
||||||
'messageReadAuto', 'showNextMessage', 'messageNewWindow',
|
'messageReadAuto', 'showNextMessage', 'messageNewWindow', 'markdown',
|
||||||
'DesktopNotifications', 'SoundNotification']);
|
'DesktopNotifications', 'SoundNotification']);
|
||||||
|
|
||||||
const fReloadLanguageHelper = (saveSettingsStep) => () => {
|
const fReloadLanguageHelper = (saveSettingsStep) => () => {
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,6 @@ import {
|
||||||
} from 'Sieve/Commands/Actions';
|
} from 'Sieve/Commands/Actions';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ConditionalCommand,
|
|
||||||
ElsIfCommand,
|
ElsIfCommand,
|
||||||
ElseCommand,
|
ElseCommand,
|
||||||
IfCommand,
|
IfCommand,
|
||||||
|
|
@ -49,6 +48,7 @@ import { NotifyCommand, ValidNotifyMethodTest, NotifyMethodCapabilityTest } from
|
||||||
import { IHaveTest, ErrorCommand } from 'Sieve/Extensions/rfc5463';
|
import { IHaveTest, ErrorCommand } from 'Sieve/Extensions/rfc5463';
|
||||||
import { MailboxExistsTest, MetadataTest, MetadataExistsTest } from 'Sieve/Extensions/rfc5490';
|
import { MailboxExistsTest, MetadataTest, MetadataExistsTest } from 'Sieve/Extensions/rfc5490';
|
||||||
import { ForEveryPartCommand, BreakCommand, ReplaceCommand, EncloseCommand, ExtractTextCommand } from 'Sieve/Extensions/rfc5703';
|
import { ForEveryPartCommand, BreakCommand, ReplaceCommand, EncloseCommand, ExtractTextCommand } from 'Sieve/Extensions/rfc5703';
|
||||||
|
import { ValidExtListTest } from 'Sieve/Extensions/rfc6134';
|
||||||
import { IncludeCommand, ReturnCommand, GlobalCommand } from 'Sieve/Extensions/rfc6609';
|
import { IncludeCommand, ReturnCommand, GlobalCommand } from 'Sieve/Extensions/rfc6609';
|
||||||
|
|
||||||
export const
|
export const
|
||||||
|
|
@ -67,7 +67,6 @@ export const
|
||||||
IfCommand,
|
IfCommand,
|
||||||
ElsIfCommand,
|
ElsIfCommand,
|
||||||
ElseCommand,
|
ElseCommand,
|
||||||
ConditionalCommand,
|
|
||||||
RequireCommand,
|
RequireCommand,
|
||||||
StopCommand,
|
StopCommand,
|
||||||
// Action commands
|
// Action commands
|
||||||
|
|
@ -129,6 +128,8 @@ export const
|
||||||
ReplaceCommand,
|
ReplaceCommand,
|
||||||
EncloseCommand,
|
EncloseCommand,
|
||||||
ExtractTextCommand,
|
ExtractTextCommand,
|
||||||
|
// rfc6134
|
||||||
|
ValidExtListTest,
|
||||||
// rfc6609
|
// rfc6609
|
||||||
IncludeCommand,
|
IncludeCommand,
|
||||||
ReturnCommand,
|
ReturnCommand,
|
||||||
|
|
@ -146,6 +147,17 @@ export const
|
||||||
return commands;
|
return commands;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
unavailableCommands = () => {
|
||||||
|
let commands = {};
|
||||||
|
AllCommands.forEach(cmd => {
|
||||||
|
const obj = new cmd, requires = obj.require;
|
||||||
|
if (requires && !(Array.isArray(requires) ? requires : [requires]).every(string => capa.includes(string))) {
|
||||||
|
commands[obj.identifier] = cmd;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return commands;
|
||||||
|
},
|
||||||
|
|
||||||
availableActions = () => {
|
availableActions = () => {
|
||||||
let actions = {}, id;
|
let actions = {}, id;
|
||||||
AllCommands.forEach(cmd => {
|
AllCommands.forEach(cmd => {
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,10 @@ export class FileIntoCommand extends ActionCommand
|
||||||
super();
|
super();
|
||||||
// QuotedString / MultiLine
|
// QuotedString / MultiLine
|
||||||
this._mailbox = new GrammarQuotedString();
|
this._mailbox = new GrammarQuotedString();
|
||||||
|
// https://datatracker.ietf.org/doc/html/rfc3894
|
||||||
|
this.copy = false;
|
||||||
|
// https://datatracker.ietf.org/doc/html/rfc5490#section-3.2
|
||||||
|
this.create = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
get require() { return 'fileinto'; }
|
get require() { return 'fileinto'; }
|
||||||
|
|
@ -28,9 +32,7 @@ export class FileIntoCommand extends ActionCommand
|
||||||
toString()
|
toString()
|
||||||
{
|
{
|
||||||
return 'fileinto'
|
return 'fileinto'
|
||||||
// https://datatracker.ietf.org/doc/html/rfc3894
|
|
||||||
+ ((this.copy && capa.includes('copy')) ? ' :copy' : '')
|
+ ((this.copy && capa.includes('copy')) ? ' :copy' : '')
|
||||||
// https://datatracker.ietf.org/doc/html/rfc5490#section-3.2
|
|
||||||
+ ((this.create && capa.includes('mailbox')) ? ' :create' : '')
|
+ ((this.create && capa.includes('mailbox')) ? ' :create' : '')
|
||||||
+ ' ' + this._mailbox
|
+ ' ' + this._mailbox
|
||||||
+ ';';
|
+ ';';
|
||||||
|
|
@ -64,15 +66,17 @@ export class RedirectCommand extends ActionCommand
|
||||||
super();
|
super();
|
||||||
// QuotedString / MultiLine
|
// QuotedString / MultiLine
|
||||||
this._address = new GrammarQuotedString();
|
this._address = new GrammarQuotedString();
|
||||||
|
// https://datatracker.ietf.org/doc/html/rfc3894
|
||||||
|
this.copy = false;
|
||||||
|
// https://datatracker.ietf.org/doc/html/rfc6134#section-2.3
|
||||||
|
this.list = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
toString()
|
toString()
|
||||||
{
|
{
|
||||||
|
|
||||||
return 'redirect'
|
return 'redirect'
|
||||||
// https://datatracker.ietf.org/doc/html/rfc6134#section-2.3
|
|
||||||
// + ((this.list && capa.includes('extlists')) ? ' :list ' + this.list : '')
|
// + ((this.list && capa.includes('extlists')) ? ' :list ' + this.list : '')
|
||||||
// https://datatracker.ietf.org/doc/html/rfc3894
|
|
||||||
+ ((this.copy && capa.includes('copy')) ? ' :copy' : '')
|
+ ((this.copy && capa.includes('copy')) ? ' :copy' : '')
|
||||||
+ ' ' + this._address
|
+ ' ' + this._address
|
||||||
+ ';';
|
+ ';';
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ControlCommand,
|
ControlCommand,
|
||||||
|
GrammarCommands,
|
||||||
GrammarStringList,
|
GrammarStringList,
|
||||||
GrammarQuotedString
|
GrammarQuotedString
|
||||||
} from 'Sieve/Grammar';
|
} from 'Sieve/Grammar';
|
||||||
|
|
@ -16,37 +17,55 @@ import {
|
||||||
* elsif <test2: test> <block2: block>
|
* elsif <test2: test> <block2: block>
|
||||||
* else <block3: block>
|
* else <block3: block>
|
||||||
*/
|
*/
|
||||||
export class ConditionalCommand extends ControlCommand
|
export /*abstract*/ class ConditionalCommand extends ControlCommand
|
||||||
{
|
{
|
||||||
constructor(identifier)
|
constructor()
|
||||||
{
|
{
|
||||||
super(identifier);
|
|
||||||
this.test = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
toString()
|
|
||||||
{
|
|
||||||
return this.identifier + ' ' + this.test + ' ' + this.commands;
|
|
||||||
}
|
|
||||||
/*
|
/*
|
||||||
public function pushArguments(array $args): void
|
if (this.constructor == ConditionalCommand) {
|
||||||
{
|
throw Error("Abstract class can't be instantiated.");
|
||||||
args.forEach((arg, i) => {
|
}
|
||||||
if (i && ':' === args[i-1][0]) {
|
|
||||||
this[args[i-1].replace(':','_')].value = arg.value;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
print_r($args);
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
*/
|
*/
|
||||||
|
super();
|
||||||
|
this.commands = new GrammarCommands;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class IfCommand extends ConditionalCommand
|
export class IfCommand extends ConditionalCommand
|
||||||
{
|
{
|
||||||
|
constructor()
|
||||||
|
{
|
||||||
|
super();
|
||||||
|
this._test = null; // must be descendent instanceof TestCommand
|
||||||
|
}
|
||||||
|
|
||||||
|
get test()
|
||||||
|
{
|
||||||
|
return this._test;
|
||||||
|
}
|
||||||
|
|
||||||
|
set test(value)
|
||||||
|
{
|
||||||
|
/*
|
||||||
|
if (!value instanceof TestCommand) {
|
||||||
|
throw Error("test must be descendent instanceof TestCommand.");
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
this._test = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
toString()
|
||||||
|
{
|
||||||
|
/*
|
||||||
|
if (!this._test instanceof TestCommand) {
|
||||||
|
throw Error("test must be descendent instanceof TestCommand.");
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
return this.identifier + ' ' + this._test + ' ' + this.commands;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ElsIfCommand extends ConditionalCommand
|
export class ElsIfCommand extends IfCommand
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@ export class AddressTest extends TestCommand
|
||||||
this.header_list = new GrammarStringList;
|
this.header_list = new GrammarStringList;
|
||||||
this.key_list = new GrammarStringList;
|
this.key_list = new GrammarStringList;
|
||||||
// rfc5260#section-6
|
// rfc5260#section-6
|
||||||
this.index = new GrammarNumber;
|
this.index = new GrammarNumber('');
|
||||||
this.last = false;
|
this.last = false;
|
||||||
// rfc5703#section-6
|
// rfc5703#section-6
|
||||||
// this.mime
|
// this.mime
|
||||||
|
|
@ -68,9 +68,10 @@ export class AddressTest extends TestCommand
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
+ (this.last ? ' :last' : (this.index.value ? ' :index ' + this.index : ''))
|
+ (this.last ? ' :last' : (this.index.value ? ' :index ' + this.index : ''))
|
||||||
+ (this.comparator ? ' :comparator ' + this.comparator : '')
|
+ (this._comparator ? ' :comparator ' + this._comparator : '')
|
||||||
+ ' ' + this.address_part
|
+ ' ' + this.address_part
|
||||||
+ ' ' + this.match_type
|
+ (this._match_type ? ' ' + this._match_type : '')
|
||||||
|
+ (this.relational_match ? ' ' + this.relational_match : '')
|
||||||
+ ' ' + this.header_list
|
+ ' ' + this.header_list
|
||||||
+ ' ' + this.key_list;
|
+ ' ' + this.key_list;
|
||||||
}
|
}
|
||||||
|
|
@ -147,9 +148,10 @@ export class EnvelopeTest extends TestCommand
|
||||||
toString()
|
toString()
|
||||||
{
|
{
|
||||||
return 'envelope'
|
return 'envelope'
|
||||||
+ (this.comparator ? ' :comparator ' + this.comparator : '')
|
+ (this._comparator ? ' :comparator ' + this._comparator : '')
|
||||||
+ ' ' + this.address_part
|
+ ' ' + this.address_part
|
||||||
+ ' ' + this.match_type
|
+ (this._match_type ? ' ' + this._match_type : '')
|
||||||
|
+ (this.relational_match ? ' ' + this.relational_match : '')
|
||||||
+ ' ' + this.envelope_part
|
+ ' ' + this.envelope_part
|
||||||
+ ' ' + this.key_list;
|
+ ' ' + this.key_list;
|
||||||
}
|
}
|
||||||
|
|
@ -234,7 +236,7 @@ export class HeaderTest extends TestCommand
|
||||||
this.header_names = new GrammarStringList;
|
this.header_names = new GrammarStringList;
|
||||||
this.key_list = new GrammarStringList;
|
this.key_list = new GrammarStringList;
|
||||||
// rfc5260#section-6
|
// rfc5260#section-6
|
||||||
this.index = new GrammarNumber;
|
this.index = new GrammarNumber('');
|
||||||
this.last = false;
|
this.last = false;
|
||||||
// rfc5703#section-6
|
// rfc5703#section-6
|
||||||
this.mime = false;
|
this.mime = false;
|
||||||
|
|
@ -279,8 +281,9 @@ export class HeaderTest extends TestCommand
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
+ (this.last ? ' :last' : (this.index.value ? ' :index ' + this.index : ''))
|
+ (this.last ? ' :last' : (this.index.value ? ' :index ' + this.index : ''))
|
||||||
+ (this.comparator ? ' :comparator ' + this.comparator : '')
|
+ (this._comparator ? ' :comparator ' + this._comparator : '')
|
||||||
+ ' ' + this.match_type
|
+ (this._match_type ? ' ' + this._match_type : '')
|
||||||
|
+ (this.relational_match ? ' ' + this.relational_match : '')
|
||||||
+ ' ' + this.header_names
|
+ ' ' + this.header_names
|
||||||
+ ' ' + this.key_list;
|
+ ' ' + this.key_list;
|
||||||
}
|
}
|
||||||
|
|
@ -316,11 +319,6 @@ export class NotTest extends TestCommand
|
||||||
{
|
{
|
||||||
return 'not ' + this.test;
|
return 'not ' + this.test;
|
||||||
}
|
}
|
||||||
|
|
||||||
pushArguments()
|
|
||||||
{
|
|
||||||
throw 'No arguments';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -22,8 +22,9 @@ export class BodyTest extends TestCommand
|
||||||
toString()
|
toString()
|
||||||
{
|
{
|
||||||
return 'body'
|
return 'body'
|
||||||
+ (this.comparator ? ' :comparator ' + this.comparator : '')
|
+ (this._comparator ? ' :comparator ' + this._comparator : '')
|
||||||
+ ' ' + this.match_type
|
+ (this._match_type ? ' ' + this._match_type : '')
|
||||||
|
+ (this.relational_match ? ' ' + this.relational_match : '')
|
||||||
+ ' ' + this.body_transform
|
+ ' ' + this.body_transform
|
||||||
+ ' ' + this.key_list;
|
+ ' ' + this.key_list;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,24 +13,28 @@ export class EnvironmentTest extends TestCommand
|
||||||
constructor()
|
constructor()
|
||||||
{
|
{
|
||||||
super();
|
super();
|
||||||
this.name = new GrammarQuotedString;
|
this._name = new GrammarQuotedString;
|
||||||
this.key_list = new GrammarStringList;
|
this.key_list = new GrammarStringList;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get name() { return this._name.value; }
|
||||||
|
set name(v) { this._name.value = v; }
|
||||||
|
|
||||||
get require() { return 'environment'; }
|
get require() { return 'environment'; }
|
||||||
|
|
||||||
toString()
|
toString()
|
||||||
{
|
{
|
||||||
return 'environment'
|
return 'environment'
|
||||||
+ (this.comparator ? ' :comparator ' + this.comparator : '')
|
+ (this._comparator ? ' :comparator ' + this._comparator : '')
|
||||||
+ ' ' + this.match_type
|
+ (this._match_type ? ' ' + this._match_type : '')
|
||||||
+ ' ' + this.name
|
+ (this.relational_match ? ' ' + this.relational_match : '')
|
||||||
|
+ ' ' + this._name
|
||||||
+ ' ' + this.key_list;
|
+ ' ' + this.key_list;
|
||||||
}
|
}
|
||||||
|
|
||||||
pushArguments(args)
|
pushArguments(args)
|
||||||
{
|
{
|
||||||
this.key_list = args.pop();
|
this.key_list = args.pop();
|
||||||
this.name = args.pop();
|
this._name = args.pop();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,12 @@ export class SetCommand extends ActionCommand
|
||||||
|
|
||||||
get require() { return 'variables'; }
|
get require() { return 'variables'; }
|
||||||
|
|
||||||
|
get name() { return this._name.value; }
|
||||||
|
set name(str) { this._name.value = str; }
|
||||||
|
|
||||||
|
get value() { return this._value.value; }
|
||||||
|
set value(str) { this._value.value = str; }
|
||||||
|
|
||||||
toString()
|
toString()
|
||||||
{
|
{
|
||||||
return 'set'
|
return 'set'
|
||||||
|
|
@ -29,12 +35,6 @@ export class SetCommand extends ActionCommand
|
||||||
+ ' ' + this._value;
|
+ ' ' + this._value;
|
||||||
}
|
}
|
||||||
|
|
||||||
get name() { return this._name.value; }
|
|
||||||
set name(str) { this._name.value = str; }
|
|
||||||
|
|
||||||
get value() { return this._value.value; }
|
|
||||||
set value(str) { this._value.value = str; }
|
|
||||||
|
|
||||||
pushArguments(args)
|
pushArguments(args)
|
||||||
{
|
{
|
||||||
[':lower', ':upper', ':lowerfirst', ':upperfirst', ':quotewildcard', ':length'].forEach(modifier => {
|
[':lower', ':upper', ':lowerfirst', ':upperfirst', ':quotewildcard', ':length'].forEach(modifier => {
|
||||||
|
|
@ -57,8 +57,9 @@ export class StringTest extends TestCommand
|
||||||
toString()
|
toString()
|
||||||
{
|
{
|
||||||
return 'string'
|
return 'string'
|
||||||
+ ' ' + this.match_type
|
+ (this._match_type ? ' ' + this._match_type : '')
|
||||||
+ (this.comparator ? ' :comparator ' + this.comparator : '')
|
+ (this.relational_match ? ' ' + this.relational_match : '')
|
||||||
|
+ (this._comparator ? ' :comparator ' + this._comparator : '')
|
||||||
+ ' ' + this.source
|
+ ' ' + this.source
|
||||||
+ ' ' + this.key_list;
|
+ ' ' + this.key_list;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -30,33 +30,6 @@ export class VacationCommand extends ActionCommand
|
||||||
// get require() { return ['vacation','vacation-seconds']; }
|
// get require() { return ['vacation','vacation-seconds']; }
|
||||||
get require() { return 'vacation'; }
|
get require() { return 'vacation'; }
|
||||||
|
|
||||||
toString()
|
|
||||||
{
|
|
||||||
let result = 'vacation';
|
|
||||||
if (0 < this._seconds.value && capa.includes('vacation-seconds')) {
|
|
||||||
result += ' :seconds ' + this._seconds;
|
|
||||||
} else if (0 < this._days.value) {
|
|
||||||
result += ' :days ' + this._days;
|
|
||||||
}
|
|
||||||
if (this._subject.length) {
|
|
||||||
result += ' :subject ' + this._subject;
|
|
||||||
}
|
|
||||||
if (this._from.length) {
|
|
||||||
result += ' :from ' + this._from;
|
|
||||||
// result += ' :from ' + this.arguments[':from'];
|
|
||||||
}
|
|
||||||
if (this.addresses.length) {
|
|
||||||
result += ' :addresses ' + this.addresses;
|
|
||||||
}
|
|
||||||
if (this.mime) {
|
|
||||||
result += ' :mime';
|
|
||||||
}
|
|
||||||
if (this._handle.length) {
|
|
||||||
result += ' :handle ' + this._handle;
|
|
||||||
}
|
|
||||||
return result + ' ' + this._reason;
|
|
||||||
}
|
|
||||||
|
|
||||||
get days() { return this._days.value; }
|
get days() { return this._days.value; }
|
||||||
get seconds() { return this._seconds.value; }
|
get seconds() { return this._seconds.value; }
|
||||||
get subject() { return this._subject.value; }
|
get subject() { return this._subject.value; }
|
||||||
|
|
@ -71,6 +44,32 @@ export class VacationCommand extends ActionCommand
|
||||||
set handle(str) { this._handle.value = str; }
|
set handle(str) { this._handle.value = str; }
|
||||||
set reason(str) { this._reason.value = str; }
|
set reason(str) { this._reason.value = str; }
|
||||||
|
|
||||||
|
toString()
|
||||||
|
{
|
||||||
|
let result = 'vacation';
|
||||||
|
if (0 < this._seconds.value && capa.includes('vacation-seconds')) {
|
||||||
|
result += ' :seconds ' + this._seconds;
|
||||||
|
} else if (0 < this._days.value) {
|
||||||
|
result += ' :days ' + this._days;
|
||||||
|
}
|
||||||
|
if (this._subject.length) {
|
||||||
|
result += ' :subject ' + this._subject;
|
||||||
|
}
|
||||||
|
if (this._from.length) {
|
||||||
|
result += ' :from ' + this._from;
|
||||||
|
}
|
||||||
|
if (this.addresses.length) {
|
||||||
|
result += ' :addresses ' + this.addresses;
|
||||||
|
}
|
||||||
|
if (this.mime) {
|
||||||
|
result += ' :mime';
|
||||||
|
}
|
||||||
|
if (this._handle.length) {
|
||||||
|
result += ' :handle ' + this._handle;
|
||||||
|
}
|
||||||
|
return result + ' ' + this._reason;
|
||||||
|
}
|
||||||
|
|
||||||
pushArguments(args)
|
pushArguments(args)
|
||||||
{
|
{
|
||||||
this._reason.value = args.pop().value; // GrammarQuotedString
|
this._reason.value = args.pop().value; // GrammarQuotedString
|
||||||
|
|
@ -81,7 +80,8 @@ export class VacationCommand extends ActionCommand
|
||||||
this.addresses = arg; // GrammarStringList
|
this.addresses = arg; // GrammarStringList
|
||||||
} else if (i && ':' === args[i-1][0]) {
|
} else if (i && ':' === args[i-1][0]) {
|
||||||
// :days, :seconds, :subject, :from, :handle
|
// :days, :seconds, :subject, :from, :handle
|
||||||
this[args[i-1].replace(':','_')].value = arg.value;
|
let p = args[i-1].replace(':','_');
|
||||||
|
this[p] ? (this[p].value = arg.value) : console.log('Unknown VacationCommand :' + p);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -43,11 +43,12 @@ class FlagCommand extends ActionCommand
|
||||||
if (args[0] instanceof GrammarQuotedString) {
|
if (args[0] instanceof GrammarQuotedString) {
|
||||||
this._variablename = args[0];
|
this._variablename = args[0];
|
||||||
}
|
}
|
||||||
if (args[1] instanceof GrammarString) {
|
args[0] = args[1];
|
||||||
this.list_of_flags = args[1];
|
}
|
||||||
}
|
if (args[0] instanceof GrammarStringList) {
|
||||||
} else if (args[0] instanceof GrammarString) {
|
|
||||||
this.list_of_flags = args[0];
|
this.list_of_flags = args[0];
|
||||||
|
} else if (args[0]) {
|
||||||
|
this.list_of_flags.push(args[0]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -78,8 +79,9 @@ export class HasFlagTest extends TestCommand
|
||||||
toString()
|
toString()
|
||||||
{
|
{
|
||||||
return 'hasflag'
|
return 'hasflag'
|
||||||
+ ' ' + this.match_type
|
+ (this._match_type ? ' ' + this._match_type : '')
|
||||||
+ (this.comparator ? ' :comparator ' + this.comparator : '')
|
+ (this.relational_match ? ' ' + this.relational_match : '')
|
||||||
|
+ (this._comparator ? ' :comparator ' + this._comparator : '')
|
||||||
+ ' ' + this.variable_list
|
+ ' ' + this.variable_list
|
||||||
+ ' ' + this.list_of_flags;
|
+ ' ' + this.list_of_flags;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@
|
||||||
|
|
||||||
import {
|
import {
|
||||||
GrammarQuotedString,
|
GrammarQuotedString,
|
||||||
GrammarString,
|
|
||||||
TestCommand
|
TestCommand
|
||||||
} from 'Sieve/Grammar';
|
} from 'Sieve/Grammar';
|
||||||
|
|
||||||
|
|
@ -14,19 +13,23 @@ export class SpamTestTest extends TestCommand
|
||||||
{
|
{
|
||||||
super();
|
super();
|
||||||
this.percent = false, // 0 - 100 else 0 - 10
|
this.percent = false, // 0 - 100 else 0 - 10
|
||||||
this.value = new GrammarQuotedString;
|
this._value = new GrammarQuotedString;
|
||||||
}
|
}
|
||||||
|
|
||||||
// get require() { return this.percent ? 'spamtestplus' : 'spamtest'; }
|
// get require() { return this.percent ? 'spamtestplus' : 'spamtest'; }
|
||||||
get require() { return /:value|:count/.test(this.match_type) ? ['spamtestplus','relational'] : 'spamtestplus'; }
|
get require() { return /:value|:count/.test(this._match_type) ? ['spamtestplus','relational'] : 'spamtestplus'; }
|
||||||
|
|
||||||
|
get value() { return this._value.value; }
|
||||||
|
set value(v) { this._value.value = v; }
|
||||||
|
|
||||||
toString()
|
toString()
|
||||||
{
|
{
|
||||||
return 'spamtest'
|
return 'spamtest'
|
||||||
+ (this.percent ? ' :percent' : '')
|
+ (this.percent ? ' :percent' : '')
|
||||||
+ (this.comparator ? ' :comparator ' + this.comparator : '')
|
+ (this._comparator ? ' :comparator ' + this._comparator : '')
|
||||||
+ ' ' + this.match_type
|
+ (this._match_type ? ' ' + this._match_type : '')
|
||||||
+ ' ' + this.value;
|
+ (this.relational_match ? ' ' + this.relational_match : '')
|
||||||
|
+ ' ' + this._value;
|
||||||
}
|
}
|
||||||
|
|
||||||
pushArguments(args)
|
pushArguments(args)
|
||||||
|
|
@ -34,8 +37,8 @@ export class SpamTestTest extends TestCommand
|
||||||
args.forEach(arg => {
|
args.forEach(arg => {
|
||||||
if (':percent' === arg) {
|
if (':percent' === arg) {
|
||||||
this.percent = true;
|
this.percent = true;
|
||||||
} else if (arg instanceof GrammarString) {
|
} else if (arg instanceof GrammarQuotedString) {
|
||||||
this.value = arg;
|
this._value = arg;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -46,24 +49,28 @@ export class VirusTestTest extends TestCommand
|
||||||
constructor()
|
constructor()
|
||||||
{
|
{
|
||||||
super();
|
super();
|
||||||
this.value = new GrammarQuotedString; // 1 - 5
|
this._value = new GrammarQuotedString; // 1 - 5
|
||||||
}
|
}
|
||||||
|
|
||||||
get require() { return /:value|:count/.test(this.match_type) ? ['virustest','relational'] : 'virustest'; }
|
get require() { return ':value' == this._match_type ? ['virustest','relational'] : 'virustest'; }
|
||||||
|
|
||||||
|
get value() { return this._value.value; }
|
||||||
|
set value(v) { this._value.value = v; }
|
||||||
|
|
||||||
toString()
|
toString()
|
||||||
{
|
{
|
||||||
return 'virustest'
|
return 'virustest'
|
||||||
+ (this.comparator ? ' :comparator ' + this.comparator : '')
|
+ (this._comparator ? ' :comparator ' + this._comparator : '')
|
||||||
+ ' ' + this.match_type
|
+ (this._match_type ? ' ' + this._match_type : '')
|
||||||
+ ' ' + this.value;
|
+ (this.relational_match ? ' ' + this.relational_match : '')
|
||||||
|
+ ' ' + this._value;
|
||||||
}
|
}
|
||||||
|
|
||||||
pushArguments(args)
|
pushArguments(args)
|
||||||
{
|
{
|
||||||
args.forEach(arg => {
|
args.forEach(arg => {
|
||||||
if (arg instanceof GrammarString) {
|
if (arg instanceof GrammarQuotedString) {
|
||||||
this.value = arg;
|
this._value = arg;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,10 +14,10 @@ export class DateTest extends TestCommand
|
||||||
constructor()
|
constructor()
|
||||||
{
|
{
|
||||||
super();
|
super();
|
||||||
this.zone = new GrammarQuotedString;
|
this._zone = new GrammarQuotedString;
|
||||||
this.originalzone = false;
|
this.originalzone = false;
|
||||||
this.header_name = new GrammarQuotedString;
|
this._header_name = new GrammarQuotedString;
|
||||||
this.date_part = new GrammarQuotedString;
|
this._date_part = new GrammarQuotedString;
|
||||||
this.key_list = new GrammarStringList;
|
this.key_list = new GrammarStringList;
|
||||||
// rfc5260#section-6
|
// rfc5260#section-6
|
||||||
this.index = new GrammarNumber;
|
this.index = new GrammarNumber;
|
||||||
|
|
@ -27,30 +27,40 @@ export class DateTest extends TestCommand
|
||||||
// get require() { return ['date','index']; }
|
// get require() { return ['date','index']; }
|
||||||
get require() { return 'date'; }
|
get require() { return 'date'; }
|
||||||
|
|
||||||
|
get zone() { return this._zone.value; }
|
||||||
|
set zone(v) { this._zone.value = v; }
|
||||||
|
|
||||||
|
get header_name() { return this._header_name.value; }
|
||||||
|
set header_name(v) { this._header_name.value = v; }
|
||||||
|
|
||||||
|
get date_part() { return this._date_part.value; }
|
||||||
|
set date_part(v) { this._date_part.value = v; }
|
||||||
|
|
||||||
toString()
|
toString()
|
||||||
{
|
{
|
||||||
return 'date'
|
return 'date'
|
||||||
+ (this.last ? ' :last' : (this.index.value ? ' :index ' + this.index : ''))
|
+ (this.last ? ' :last' : (this.index.value ? ' :index ' + this.index : ''))
|
||||||
+ (this.originalzone ? ' :originalzone' : (this.zone.length ? ' :zone ' + this.zone : ''))
|
+ (this.originalzone ? ' :originalzone' : (this._zone.length ? ' :zone ' + this._zone : ''))
|
||||||
+ (this.comparator ? ' :comparator ' + this.comparator : '')
|
+ (this._comparator ? ' :comparator ' + this._comparator : '')
|
||||||
+ ' ' + this.match_type
|
+ (this._match_type ? ' ' + this._match_type : '')
|
||||||
+ ' ' + this.header_name
|
+ (this.relational_match ? ' ' + this.relational_match : '')
|
||||||
+ ' ' + this.date_part
|
+ ' ' + this._header_name
|
||||||
|
+ ' ' + this._date_part
|
||||||
+ ' ' + this.key_list;
|
+ ' ' + this.key_list;
|
||||||
}
|
}
|
||||||
|
|
||||||
pushArguments(args)
|
pushArguments(args)
|
||||||
{
|
{
|
||||||
this.key_list = args.pop();
|
this.key_list = args.pop();
|
||||||
this.date_part = args.pop();
|
this._date_part = args.pop();
|
||||||
this.header_name = args.pop();
|
this._header_name = args.pop();
|
||||||
args.forEach((arg, i) => {
|
args.forEach((arg, i) => {
|
||||||
if (':originalzone' === arg) {
|
if (':originalzone' === arg) {
|
||||||
this.originalzone = true;
|
this.originalzone = true;
|
||||||
} else if (':last' === arg) {
|
} else if (':last' === arg) {
|
||||||
this.last = true;
|
this.last = true;
|
||||||
} else if (i && ':zone' === args[i-1]) {
|
} else if (i && ':zone' === args[i-1]) {
|
||||||
this.zone.value = arg.value;
|
this._zone.value = arg.value;
|
||||||
} else if (i && ':index' === args[i-1]) {
|
} else if (i && ':index' === args[i-1]) {
|
||||||
this.index.value = arg.value;
|
this.index.value = arg.value;
|
||||||
}
|
}
|
||||||
|
|
@ -63,30 +73,37 @@ export class CurrentDateTest extends TestCommand
|
||||||
constructor()
|
constructor()
|
||||||
{
|
{
|
||||||
super();
|
super();
|
||||||
this.zone = new GrammarQuotedString;
|
this._zone = new GrammarQuotedString;
|
||||||
this.date_part = new GrammarQuotedString;
|
this._date_part = new GrammarQuotedString;
|
||||||
this.key_list = new GrammarStringList;
|
this.key_list = new GrammarStringList;
|
||||||
}
|
}
|
||||||
|
|
||||||
get require() { return 'date'; }
|
get require() { return 'date'; }
|
||||||
|
|
||||||
|
get zone() { return this._zone.value; }
|
||||||
|
set zone(v) { this._zone.value = v; }
|
||||||
|
|
||||||
|
get date_part() { return this._date_part.value; }
|
||||||
|
set date_part(v) { this._date_part.value = v; }
|
||||||
|
|
||||||
toString()
|
toString()
|
||||||
{
|
{
|
||||||
return 'currentdate'
|
return 'currentdate'
|
||||||
+ (this.zone.length ? ' :zone ' + this.zone : '')
|
+ (this._zone.length ? ' :zone ' + this._zone : '')
|
||||||
+ (this.comparator ? ' :comparator ' + this.comparator : '')
|
+ (this._comparator ? ' :comparator ' + this._comparator : '')
|
||||||
+ ' ' + this.match_type
|
+ (this._match_type ? ' ' + this._match_type : '')
|
||||||
+ ' ' + this.date_part
|
+ (this.relational_match ? ' ' + this.relational_match : '')
|
||||||
|
+ ' ' + this._date_part
|
||||||
+ ' ' + this.key_list;
|
+ ' ' + this.key_list;
|
||||||
}
|
}
|
||||||
|
|
||||||
pushArguments(args)
|
pushArguments(args)
|
||||||
{
|
{
|
||||||
this.key_list = args.pop();
|
this.key_list = args.pop();
|
||||||
this.date_part = args.pop();
|
this._date_part = args.pop();
|
||||||
args.forEach((arg, i) => {
|
args.forEach((arg, i) => {
|
||||||
if (i && ':zone' === args[i-1]) {
|
if (i && ':zone' === args[i-1]) {
|
||||||
this.zone.value = arg.value;
|
this._zone.value = arg.value;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,24 +16,30 @@ export class AddHeaderCommand extends ActionCommand
|
||||||
{
|
{
|
||||||
super();
|
super();
|
||||||
this.last = false;
|
this.last = false;
|
||||||
this.field_name = new GrammarQuotedString;
|
this._field_name = new GrammarQuotedString;
|
||||||
this.value = new GrammarQuotedString;
|
this._value = new GrammarQuotedString;
|
||||||
}
|
}
|
||||||
|
|
||||||
get require() { return 'editheader'; }
|
get require() { return 'editheader'; }
|
||||||
|
|
||||||
|
get field_name() { return this._field_name.value; }
|
||||||
|
set field_name(v) { this._field_name.value = v; }
|
||||||
|
|
||||||
|
get value() { return this._value.value; }
|
||||||
|
set value(v) { this._value.value = v; }
|
||||||
|
|
||||||
toString()
|
toString()
|
||||||
{
|
{
|
||||||
return this.identifier
|
return this.identifier
|
||||||
+ (this.last ? ' :last' : '')
|
+ (this.last ? ' :last' : '')
|
||||||
+ ' ' + this.field_name
|
+ ' ' + this._field_name
|
||||||
+ ' ' + this.value + ';';
|
+ ' ' + this._value + ';';
|
||||||
}
|
}
|
||||||
|
|
||||||
pushArguments(args)
|
pushArguments(args)
|
||||||
{
|
{
|
||||||
this.value = args.pop();
|
this._value = args.pop();
|
||||||
this.field_name = args.pop();
|
this._field_name = args.pop();
|
||||||
this.last = args.includes(':last');
|
this.last = args.includes(':last');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -47,19 +53,22 @@ export class DeleteHeaderCommand extends ActionCommand
|
||||||
this.last = false;
|
this.last = false;
|
||||||
this.comparator = '',
|
this.comparator = '',
|
||||||
this.match_type = ':is',
|
this.match_type = ':is',
|
||||||
this.field_name = new GrammarQuotedString;
|
this._field_name = new GrammarQuotedString;
|
||||||
this.value_patterns = new GrammarStringList;
|
this.value_patterns = new GrammarStringList;
|
||||||
}
|
}
|
||||||
|
|
||||||
get require() { return 'editheader'; }
|
get require() { return 'editheader'; }
|
||||||
|
|
||||||
|
get field_name() { return this._field_name.value; }
|
||||||
|
set field_name(v) { this._field_name.value = v; }
|
||||||
|
|
||||||
toString()
|
toString()
|
||||||
{
|
{
|
||||||
return this.identifier
|
return this.identifier
|
||||||
+ (this.last ? ' :last' : (this.index.value ? ' :index ' + this.index : ''))
|
+ (this.last ? ' :last' : (this.index.value ? ' :index ' + this.index : ''))
|
||||||
+ (this.comparator ? ' :comparator ' + this.comparator : '')
|
+ (this.comparator ? ' :comparator ' + this.comparator : '')
|
||||||
+ ' ' + this.match_type
|
+ ' ' + this.match_type
|
||||||
+ ' ' + this.field_name
|
+ ' ' + this._field_name
|
||||||
+ ' ' + this.value_patterns + ';';
|
+ ' ' + this.value_patterns + ';';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -76,10 +85,10 @@ export class DeleteHeaderCommand extends ActionCommand
|
||||||
});
|
});
|
||||||
|
|
||||||
if (l && args[l-1] instanceof GrammarString) {
|
if (l && args[l-1] instanceof GrammarString) {
|
||||||
this.field_name = args[l-1];
|
this._field_name = args[l-1];
|
||||||
this.value_patterns = args[l];
|
this.value_patterns = args[l];
|
||||||
} else {
|
} else {
|
||||||
this.field_name = args[l];
|
this._field_name = args[l];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,11 +8,11 @@ import {
|
||||||
GrammarString
|
GrammarString
|
||||||
} from 'Sieve/Grammar';
|
} from 'Sieve/Grammar';
|
||||||
|
|
||||||
class rfc5429Command extends ActionCommand
|
class /*abstract*/ rfc5429Command extends ActionCommand
|
||||||
{
|
{
|
||||||
constructor(identifier)
|
constructor()
|
||||||
{
|
{
|
||||||
super(identifier);
|
super();
|
||||||
this._reason = new GrammarQuotedString;
|
this._reason = new GrammarQuotedString;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -44,7 +44,6 @@ class rfc5429Command extends ActionCommand
|
||||||
*/
|
*/
|
||||||
export class ErejectCommand extends rfc5429Command
|
export class ErejectCommand extends rfc5429Command
|
||||||
{
|
{
|
||||||
constructor() { super('ereject'); }
|
|
||||||
get require() { return 'ereject'; }
|
get require() { return 'ereject'; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -53,6 +52,5 @@ export class ErejectCommand extends rfc5429Command
|
||||||
*/
|
*/
|
||||||
export class RejectCommand extends rfc5429Command
|
export class RejectCommand extends rfc5429Command
|
||||||
{
|
{
|
||||||
constructor() { super('reject'); }
|
|
||||||
get require() { return 'reject'; }
|
get require() { return 'reject'; }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,14 +26,16 @@ export class NotifyCommand extends ActionCommand
|
||||||
}
|
}
|
||||||
|
|
||||||
get method() { return this._method.value; }
|
get method() { return this._method.value; }
|
||||||
get from() { return this._from.value; }
|
set method(str) { this._method.value = str; }
|
||||||
get importance() { return this._importance.value; }
|
|
||||||
get message() { return this._message.value; }
|
|
||||||
|
|
||||||
set method(str) { this._method.value = str; }
|
get from() { return this._from.value; }
|
||||||
set from(str) { this._from.value = str; }
|
set from(str) { this._from.value = str; }
|
||||||
|
|
||||||
|
get importance() { return this._importance.value; }
|
||||||
set importance(int) { this._importance.value = int; }
|
set importance(int) { this._importance.value = int; }
|
||||||
set message(str) { this._message.value = str; }
|
|
||||||
|
get message() { return this._message.value; }
|
||||||
|
set message(str) { this._message.value = str; }
|
||||||
|
|
||||||
get require() { return 'enotify'; }
|
get require() { return 'enotify'; }
|
||||||
|
|
||||||
|
|
@ -63,7 +65,8 @@ export class NotifyCommand extends ActionCommand
|
||||||
this.options = arg; // GrammarStringList
|
this.options = arg; // GrammarStringList
|
||||||
} else if (i && ':' === args[i-1][0]) {
|
} else if (i && ':' === args[i-1][0]) {
|
||||||
// :from, :importance, :message
|
// :from, :importance, :message
|
||||||
this[args[i-1].replace(':','_')].value = arg.value;
|
let p = args[i-1].replace(':','_');
|
||||||
|
this[p] ? (this[p].value = arg.value) : console.log('Unknown VacationCommand :' + p);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -99,25 +102,32 @@ export class NotifyMethodCapabilityTest extends TestCommand
|
||||||
constructor()
|
constructor()
|
||||||
{
|
{
|
||||||
super();
|
super();
|
||||||
this.notification_uri = new GrammarQuotedString;
|
this._notification_uri = new GrammarQuotedString;
|
||||||
this.notification_capability = new GrammarQuotedString;
|
this._notification_capability = new GrammarQuotedString;
|
||||||
this.key_list = new GrammarStringList;
|
this.key_list = new GrammarStringList;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get notification_uri() { return this._notification_uri.value; }
|
||||||
|
set notification_uri(v) { this._notification_uri.value = v; }
|
||||||
|
|
||||||
|
get notification_capability() { return this._notification_capability.value; }
|
||||||
|
set notification_capability(v) { this._notification_capability.value = v; }
|
||||||
|
|
||||||
toString()
|
toString()
|
||||||
{
|
{
|
||||||
return 'valid_notify_method '
|
return 'valid_notify_method '
|
||||||
+ (this.comparator ? ' :comparator ' + this.comparator : '')
|
+ (this._comparator ? ' :comparator ' + this._comparator : '')
|
||||||
+ (this.match_type ? ' ' + this.match_type : '')
|
+ (this._match_type ? ' ' + this._match_type : '')
|
||||||
+ this.notification_uri
|
+ (this.relational_match ? ' ' + this.relational_match : '')
|
||||||
+ this.notification_capability
|
+ this._notification_uri
|
||||||
|
+ this._notification_capability
|
||||||
+ this.key_list;
|
+ this.key_list;
|
||||||
}
|
}
|
||||||
|
|
||||||
pushArguments(args)
|
pushArguments(args)
|
||||||
{
|
{
|
||||||
this.key_list = args.pop();
|
this.key_list = args.pop();
|
||||||
this.notification_capability = args.pop();
|
this._notification_capability = args.pop();
|
||||||
this.notification_uri = args.pop();
|
this._notification_uri = args.pop();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -41,18 +41,21 @@ export class ErrorCommand extends ControlCommand
|
||||||
constructor()
|
constructor()
|
||||||
{
|
{
|
||||||
super();
|
super();
|
||||||
this.message = new GrammarQuotedString;
|
this._message = new GrammarQuotedString;
|
||||||
}
|
}
|
||||||
|
|
||||||
get require() { return 'ihave'; }
|
get require() { return 'ihave'; }
|
||||||
|
|
||||||
|
get message() { return this._message.value; }
|
||||||
|
set message(v) { this._message.value = v; }
|
||||||
|
|
||||||
toString()
|
toString()
|
||||||
{
|
{
|
||||||
return 'error ' + this.message + ';';
|
return 'error ' + this._message + ';';
|
||||||
}
|
}
|
||||||
|
|
||||||
pushArguments(args)
|
pushArguments(args)
|
||||||
{
|
{
|
||||||
this.message = args.pop();
|
this._message = args.pop();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -42,28 +42,35 @@ export class MetadataTest extends TestCommand
|
||||||
constructor()
|
constructor()
|
||||||
{
|
{
|
||||||
super();
|
super();
|
||||||
this.mailbox = new GrammarQuotedString;
|
this._mailbox = new GrammarQuotedString;
|
||||||
this.annotation_name = new GrammarQuotedString;
|
this._annotation_name = new GrammarQuotedString;
|
||||||
this.key_list = new GrammarStringList;
|
this.key_list = new GrammarStringList;
|
||||||
}
|
}
|
||||||
|
|
||||||
get require() { return 'mboxmetadata'; }
|
get require() { return 'mboxmetadata'; }
|
||||||
|
|
||||||
|
get mailbox() { return this._mailbox.value; }
|
||||||
|
set mailbox(v) { this._mailbox.value = v; }
|
||||||
|
|
||||||
|
get annotation_name() { return this._annotation_name.value; }
|
||||||
|
set annotation_name(v) { this._annotation_name.value = v; }
|
||||||
|
|
||||||
toString()
|
toString()
|
||||||
{
|
{
|
||||||
return 'metadata '
|
return 'metadata '
|
||||||
+ ' ' + this.match_type
|
+ ' ' + this._match_type
|
||||||
+ (this.comparator ? ' :comparator ' + this.comparator : '')
|
+ (this.relational_match ? ' ' + this.relational_match : '')
|
||||||
+ ' ' + this.mailbox
|
+ (this._comparator ? ' :comparator ' + this._comparator : '')
|
||||||
+ ' ' + this.annotation_name
|
+ ' ' + this._mailbox
|
||||||
|
+ ' ' + this._annotation_name
|
||||||
+ ' ' + this.key_list;
|
+ ' ' + this.key_list;
|
||||||
}
|
}
|
||||||
|
|
||||||
pushArguments(args)
|
pushArguments(args)
|
||||||
{
|
{
|
||||||
this.key_list = args.pop();
|
this.key_list = args.pop();
|
||||||
this.annotation_name = args.pop();
|
this._annotation_name = args.pop();
|
||||||
this.mailbox = args.pop();
|
this._mailbox = args.pop();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -75,23 +82,26 @@ export class MetadataExistsTest extends TestCommand
|
||||||
constructor()
|
constructor()
|
||||||
{
|
{
|
||||||
super();
|
super();
|
||||||
this.mailbox = new GrammarQuotedString;
|
this._mailbox = new GrammarQuotedString;
|
||||||
this.annotation_names = new GrammarStringList;
|
this.annotation_names = new GrammarStringList;
|
||||||
}
|
}
|
||||||
|
|
||||||
get require() { return 'mboxmetadata'; }
|
get require() { return 'mboxmetadata'; }
|
||||||
|
|
||||||
|
get mailbox() { return this._mailbox.value; }
|
||||||
|
set mailbox(v) { this._mailbox.value = v; }
|
||||||
|
|
||||||
toString()
|
toString()
|
||||||
{
|
{
|
||||||
return 'metadataexists '
|
return 'metadataexists '
|
||||||
+ ' ' + this.mailbox
|
+ ' ' + this._mailbox
|
||||||
+ ' ' + this.annotation_names;
|
+ ' ' + this.annotation_names;
|
||||||
}
|
}
|
||||||
|
|
||||||
pushArguments(args)
|
pushArguments(args)
|
||||||
{
|
{
|
||||||
this.annotation_names = args.pop();
|
this.annotation_names = args.pop();
|
||||||
this.mailbox = args.pop();
|
this._mailbox = args.pop();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -103,25 +113,29 @@ export class ServerMetadataTest extends TestCommand
|
||||||
constructor()
|
constructor()
|
||||||
{
|
{
|
||||||
super();
|
super();
|
||||||
this.annotation_name = new GrammarQuotedString;
|
this._annotation_name = new GrammarQuotedString;
|
||||||
this.key_list = new GrammarStringList;
|
this.key_list = new GrammarStringList;
|
||||||
}
|
}
|
||||||
|
|
||||||
get require() { return 'servermetadata'; }
|
get require() { return 'servermetadata'; }
|
||||||
|
|
||||||
|
get annotation_name() { return this._annotation_name.value; }
|
||||||
|
set annotation_name(v) { this._annotation_name.value = v; }
|
||||||
|
|
||||||
toString()
|
toString()
|
||||||
{
|
{
|
||||||
return 'servermetadata '
|
return 'servermetadata '
|
||||||
+ ' ' + this.match_type
|
+ ' ' + this._match_type
|
||||||
+ (this.comparator ? ' :comparator ' + this.comparator : '')
|
+ (this.relational_match ? ' ' + this.relational_match : '')
|
||||||
+ ' ' + this.annotation_name
|
+ (this._comparator ? ' :comparator ' + this._comparator : '')
|
||||||
|
+ ' ' + this._annotation_name
|
||||||
+ ' ' + this.key_list;
|
+ ' ' + this.key_list;
|
||||||
}
|
}
|
||||||
|
|
||||||
pushArguments(args)
|
pushArguments(args)
|
||||||
{
|
{
|
||||||
this.key_list = args.pop();
|
this.key_list = args.pop();
|
||||||
this.annotation_name = args.pop();
|
this._annotation_name = args.pop();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
import {
|
import {
|
||||||
ActionCommand,
|
ActionCommand,
|
||||||
ControlCommand,
|
ControlCommand,
|
||||||
|
GrammarCommands,
|
||||||
GrammarNumber,
|
GrammarNumber,
|
||||||
GrammarQuotedString,
|
GrammarQuotedString,
|
||||||
GrammarString,
|
GrammarString,
|
||||||
|
|
@ -19,7 +20,8 @@ export class ForEveryPartCommand extends ControlCommand
|
||||||
constructor()
|
constructor()
|
||||||
{
|
{
|
||||||
super();
|
super();
|
||||||
this._name = new GrammarString;
|
this.name = new GrammarString;
|
||||||
|
this.commands = new GrammarCommands;
|
||||||
}
|
}
|
||||||
|
|
||||||
get require() { return 'foreverypart'; }
|
get require() { return 'foreverypart'; }
|
||||||
|
|
@ -27,8 +29,8 @@ export class ForEveryPartCommand extends ControlCommand
|
||||||
toString()
|
toString()
|
||||||
{
|
{
|
||||||
let result = 'foreverypart';
|
let result = 'foreverypart';
|
||||||
if (this._subject.length) {
|
if (this.name.length) {
|
||||||
result += ' :name ' + this._name;
|
result += ' :name ' + this.name;
|
||||||
}
|
}
|
||||||
return result + ' ' + this.commands;
|
return result + ' ' + this.commands;
|
||||||
}
|
}
|
||||||
|
|
@ -37,19 +39,22 @@ export class ForEveryPartCommand extends ControlCommand
|
||||||
{
|
{
|
||||||
args.forEach((arg, i) => {
|
args.forEach((arg, i) => {
|
||||||
if (':name' === arg) {
|
if (':name' === arg) {
|
||||||
this._name.value = args[i+1].value;
|
this.name.value = args[i+1].value;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Must be inside foreverypart
|
||||||
|
*/
|
||||||
export class BreakCommand extends ForEveryPartCommand
|
export class BreakCommand extends ForEveryPartCommand
|
||||||
{
|
{
|
||||||
toString()
|
toString()
|
||||||
{
|
{
|
||||||
let result = 'break';
|
let result = 'break';
|
||||||
if (this._subject.length) {
|
if (this.name.length) {
|
||||||
result += ' :name ' + this._name;
|
result += ' :name ' + this.name;
|
||||||
}
|
}
|
||||||
return result + ';';
|
return result + ';';
|
||||||
}
|
}
|
||||||
|
|
@ -63,14 +68,23 @@ export class ReplaceCommand extends ActionCommand
|
||||||
constructor()
|
constructor()
|
||||||
{
|
{
|
||||||
super();
|
super();
|
||||||
this.mime = false;
|
this.mime = false;
|
||||||
this._subject = new GrammarQuotedString;
|
this._subject = new GrammarQuotedString;
|
||||||
this._from = new GrammarQuotedString;
|
this._from = new GrammarQuotedString;
|
||||||
this.replacement = new GrammarQuotedString;
|
this._replacement = new GrammarQuotedString;
|
||||||
}
|
}
|
||||||
|
|
||||||
get require() { return 'replace'; }
|
get require() { return 'replace'; }
|
||||||
|
|
||||||
|
get subject() { return this._subject.value; }
|
||||||
|
set subject(str) { this._subject.value = str; }
|
||||||
|
|
||||||
|
get from() { return this._from.value; }
|
||||||
|
set from(str) { this._from.value = str; }
|
||||||
|
|
||||||
|
get replacement() { return this._replacement.value; }
|
||||||
|
set replacement(str) { this._replacement.value = str; }
|
||||||
|
|
||||||
toString()
|
toString()
|
||||||
{
|
{
|
||||||
let result = 'replace';
|
let result = 'replace';
|
||||||
|
|
@ -82,20 +96,20 @@ export class ReplaceCommand extends ActionCommand
|
||||||
}
|
}
|
||||||
if (this._from.length) {
|
if (this._from.length) {
|
||||||
result += ' :from ' + this._from;
|
result += ' :from ' + this._from;
|
||||||
// result += ' :from ' + this.arguments[':from'];
|
|
||||||
}
|
}
|
||||||
return result + this.replacement + ';';
|
return result + this._replacement + ';';
|
||||||
}
|
}
|
||||||
|
|
||||||
pushArguments(args)
|
pushArguments(args)
|
||||||
{
|
{
|
||||||
this.replacement = args.pop();
|
this._replacement = args.pop();
|
||||||
args.forEach((arg, i) => {
|
args.forEach((arg, i) => {
|
||||||
if (':mime' === arg) {
|
if (':mime' === arg) {
|
||||||
this.mime = true;
|
this.mime = true;
|
||||||
} else if (i && ':' === args[i-1][0]) {
|
} else if (i && ':' === args[i-1][0]) {
|
||||||
// :subject, :from
|
// :subject, :from
|
||||||
this[args[i-1].replace(':','_')].value = arg.value;
|
let p = args[i-1].replace(':','_');
|
||||||
|
this[p] ? (this[p].value = arg.value) : console.log('Unknown VacationCommand :' + p);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -115,6 +129,9 @@ export class EncloseCommand extends ActionCommand
|
||||||
|
|
||||||
get require() { return 'enclose'; }
|
get require() { return 'enclose'; }
|
||||||
|
|
||||||
|
get subject() { return this._subject.value; }
|
||||||
|
set subject(v) { this._subject.value = v; }
|
||||||
|
|
||||||
toString()
|
toString()
|
||||||
{
|
{
|
||||||
let result = 'enclose';
|
let result = 'enclose';
|
||||||
|
|
@ -132,7 +149,8 @@ export class EncloseCommand extends ActionCommand
|
||||||
args.forEach((arg, i) => {
|
args.forEach((arg, i) => {
|
||||||
if (i && ':' === args[i-1][0]) {
|
if (i && ':' === args[i-1][0]) {
|
||||||
// :subject, :headers
|
// :subject, :headers
|
||||||
this[args[i-1].replace(':','_')].value = arg.value;
|
let p = args[i-1].replace(':','_');
|
||||||
|
this[p] ? (this[p].value = arg.value) : console.log('Unknown VacationCommand :' + p);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -140,6 +158,7 @@ export class EncloseCommand extends ActionCommand
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* https://datatracker.ietf.org/doc/html/rfc5703#section-7
|
* https://datatracker.ietf.org/doc/html/rfc5703#section-7
|
||||||
|
* Should be inside foreverypart, else empty and flagged as a compilation error
|
||||||
*/
|
*/
|
||||||
export class ExtractTextCommand extends ActionCommand
|
export class ExtractTextCommand extends ActionCommand
|
||||||
{
|
{
|
||||||
|
|
@ -148,9 +167,12 @@ export class ExtractTextCommand extends ActionCommand
|
||||||
super();
|
super();
|
||||||
this.modifiers = [];
|
this.modifiers = [];
|
||||||
this._first = new GrammarNumber;
|
this._first = new GrammarNumber;
|
||||||
this.varname = new GrammarQuotedString;
|
this._varname = new GrammarQuotedString;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get varname() { return this._varname.value; }
|
||||||
|
set varname(v) { this._varname.value = v; }
|
||||||
|
|
||||||
get require() { return 'extracttext'; }
|
get require() { return 'extracttext'; }
|
||||||
|
|
||||||
toString()
|
toString()
|
||||||
|
|
@ -160,12 +182,12 @@ export class ExtractTextCommand extends ActionCommand
|
||||||
if (0 < this._first.value) {
|
if (0 < this._first.value) {
|
||||||
result += ' :first ' + this._first;
|
result += ' :first ' + this._first;
|
||||||
}
|
}
|
||||||
return result + ' ' + this.varname + ';';
|
return result + ' ' + this._varname + ';';
|
||||||
}
|
}
|
||||||
|
|
||||||
pushArguments(args)
|
pushArguments(args)
|
||||||
{
|
{
|
||||||
this.varname = args.pop();
|
this._varname = args.pop();
|
||||||
[':lower', ':upper', ':lowerfirst', ':upperfirst', ':quotewildcard', ':length'].forEach(modifier => {
|
[':lower', ':upper', ':lowerfirst', ':upperfirst', ':quotewildcard', ':length'].forEach(modifier => {
|
||||||
args.includes(modifier) && this.modifiers.push(modifier);
|
args.includes(modifier) && this.modifiers.push(modifier);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ export class ValidExtListTest extends TestCommand
|
||||||
{
|
{
|
||||||
constructor()
|
constructor()
|
||||||
{
|
{
|
||||||
super();
|
super('valid_ext_list');
|
||||||
this.ext_list_names = new GrammarStringList;
|
this.ext_list_names = new GrammarStringList;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,18 +16,21 @@ export class IncludeCommand extends ControlCommand
|
||||||
this.global = false; // ':personal' / ':global';
|
this.global = false; // ':personal' / ':global';
|
||||||
this.once = false;
|
this.once = false;
|
||||||
this.optional = false;
|
this.optional = false;
|
||||||
this.value = new GrammarQuotedString;
|
this._value = new GrammarQuotedString;
|
||||||
}
|
}
|
||||||
|
|
||||||
get require() { return 'include'; }
|
get require() { return 'include'; }
|
||||||
|
|
||||||
|
get value() { return this._value.value; }
|
||||||
|
set value(v) { this._value.value = v; }
|
||||||
|
|
||||||
toString()
|
toString()
|
||||||
{
|
{
|
||||||
return 'include'
|
return 'include'
|
||||||
+ (this.global ? ' :global' : '')
|
+ (this.global ? ' :global' : '')
|
||||||
+ (this.once ? ' :once' : '')
|
+ (this.once ? ' :once' : '')
|
||||||
+ (this.optional ? ' :optional' : '')
|
+ (this.optional ? ' :optional' : '')
|
||||||
+ ' ' + this.value + ';';
|
+ ' ' + this._value + ';';
|
||||||
}
|
}
|
||||||
|
|
||||||
pushArguments(args)
|
pushArguments(args)
|
||||||
|
|
@ -36,7 +39,7 @@ export class IncludeCommand extends ControlCommand
|
||||||
if (':global' === arg || ':once' === arg || ':optional' === arg) {
|
if (':global' === arg || ':once' === arg || ':optional' === arg) {
|
||||||
this[arg.slice(1)] = true;
|
this[arg.slice(1)] = true;
|
||||||
} else if (arg instanceof GrammarQuotedString) {
|
} else if (arg instanceof GrammarQuotedString) {
|
||||||
this.value = arg;
|
this._value = arg;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ import {
|
||||||
MULTILINE_DOTSTART
|
MULTILINE_DOTSTART
|
||||||
} from 'Sieve/RegEx';
|
} from 'Sieve/RegEx';
|
||||||
|
|
||||||
import { arrayToString, getMatchTypes } from 'Sieve/Utils';
|
import { arrayToString, getMatchTypes, getComparators } from 'Sieve/Utils';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* abstract
|
* abstract
|
||||||
|
|
@ -18,7 +18,7 @@ export class GrammarString /*extends String*/
|
||||||
{
|
{
|
||||||
constructor(value = '')
|
constructor(value = '')
|
||||||
{
|
{
|
||||||
this._value = value;
|
this._value = value.toString ? value.toString() : value;
|
||||||
}
|
}
|
||||||
|
|
||||||
toString()
|
toString()
|
||||||
|
|
@ -45,41 +45,41 @@ export class GrammarString /*extends String*/
|
||||||
/**
|
/**
|
||||||
* abstract
|
* abstract
|
||||||
*/
|
*/
|
||||||
export class GrammarComment extends GrammarString
|
export /*abstract*/ class GrammarComment extends GrammarString
|
||||||
{
|
{
|
||||||
|
/*
|
||||||
|
constructor()
|
||||||
|
{
|
||||||
|
if (this.constructor == GrammarComment) {
|
||||||
|
throw Error("Abstract class can't be instantiated.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* https://tools.ietf.org/html/rfc5228#section-2.9
|
* https://tools.ietf.org/html/rfc5228#section-2.9
|
||||||
*/
|
*/
|
||||||
export class GrammarCommand
|
const cmdNameSuffix = /(test|command|action)$/;
|
||||||
|
export /*abstract*/ class GrammarCommand
|
||||||
{
|
{
|
||||||
constructor(identifier)
|
constructor(identifier)
|
||||||
{
|
{
|
||||||
this.identifier = identifier || this.constructor.name.toLowerCase().replace(/(test|command|action)$/, '');
|
/*
|
||||||
this.arguments = [];
|
if (this.constructor == GrammarCommand) {
|
||||||
this.commands = new GrammarCommands;
|
throw Error("Abstract class can't be instantiated.");
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
this.identifier = identifier || this.constructor.name.toLowerCase().replace(cmdNameSuffix, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
toString()
|
toString()
|
||||||
{
|
{
|
||||||
let result = this.identifier;
|
let result = this.identifier;
|
||||||
if (this.arguments.length) {
|
if (this.arguments?.length) {
|
||||||
result += ' ' + arrayToString(this.arguments, ' ');
|
result += ' ' + arrayToString(this.arguments, ' ');
|
||||||
}
|
}
|
||||||
return result + (
|
return result + ';';
|
||||||
this.commands.length ? ' ' + this.commands : ';'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
getComparators()
|
|
||||||
{
|
|
||||||
return ['i;ascii-casemap'];
|
|
||||||
}
|
|
||||||
|
|
||||||
getMatchTypes()
|
|
||||||
{
|
|
||||||
return [':is', ':contains', ':matches'];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pushArguments(args)
|
pushArguments(args)
|
||||||
|
|
@ -108,78 +108,107 @@ export class GrammarCommands extends Array
|
||||||
/**
|
/**
|
||||||
* https://tools.ietf.org/html/rfc5228#section-3
|
* https://tools.ietf.org/html/rfc5228#section-3
|
||||||
*/
|
*/
|
||||||
export class ControlCommand extends GrammarCommand
|
export /*abstract*/ class ControlCommand extends GrammarCommand
|
||||||
{
|
{
|
||||||
|
/*
|
||||||
constructor(identifier)
|
constructor(identifier)
|
||||||
{
|
{
|
||||||
super(identifier);
|
if (this.constructor == ControlCommand) {
|
||||||
this.commands = new GrammarCommands;
|
throw Error("Abstract class can't be instantiated.");
|
||||||
}
|
|
||||||
|
|
||||||
toString()
|
|
||||||
{
|
|
||||||
let result = this.identifier;
|
|
||||||
if (this.arguments.length) {
|
|
||||||
result += ' ' + arrayToString(this.arguments, ' ');
|
|
||||||
}
|
}
|
||||||
return result + (
|
super(identifier);
|
||||||
this.commands.length ? ' ' + this.commands : ';'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
getComparators()
|
|
||||||
{
|
|
||||||
return ['i;ascii-casemap'];
|
|
||||||
}
|
|
||||||
|
|
||||||
getMatchTypes()
|
|
||||||
{
|
|
||||||
return [':is', ':contains', ':matches'];
|
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* https://tools.ietf.org/html/rfc5228#section-4
|
* https://tools.ietf.org/html/rfc5228#section-4
|
||||||
*/
|
*/
|
||||||
export class ActionCommand extends GrammarCommand
|
export /*abstract*/ class ActionCommand extends GrammarCommand
|
||||||
{
|
{
|
||||||
|
/*
|
||||||
constructor(identifier)
|
constructor(identifier)
|
||||||
{
|
{
|
||||||
|
if (this.constructor == ActionCommand) {
|
||||||
|
throw Error("Abstract class can't be instantiated.");
|
||||||
|
}
|
||||||
super(identifier);
|
super(identifier);
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
toString()
|
|
||||||
{
|
|
||||||
let result = this.identifier;
|
|
||||||
if (this.arguments.length) {
|
|
||||||
result += ' ' + arrayToString(this.arguments, ' ');
|
|
||||||
}
|
|
||||||
return result + ';'
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* https://tools.ietf.org/html/rfc5228#section-5
|
* https://tools.ietf.org/html/rfc5228#section-5
|
||||||
*/
|
*/
|
||||||
export class TestCommand extends GrammarCommand
|
export /*abstract*/ class TestCommand extends GrammarCommand
|
||||||
{
|
{
|
||||||
constructor(identifier)
|
constructor(identifier)
|
||||||
{
|
{
|
||||||
|
/*
|
||||||
|
if (this.constructor == TestCommand) {
|
||||||
|
throw Error("Abstract class can't be instantiated.");
|
||||||
|
}
|
||||||
|
*/
|
||||||
super(identifier);
|
super(identifier);
|
||||||
// Almost every test has a comparator and match_type, so define them here
|
// Almost every test has a comparator and match_type, so define them here
|
||||||
this.comparator = '';
|
this._comparator = '';
|
||||||
this.match_type = ':is';
|
this._match_type = '';
|
||||||
|
this.relational_match = ''; // GrammarQuotedString DQUOTE ( "gt" / "ge" / "lt" / "le" / "eq" / "ne" ) DQUOTE
|
||||||
|
}
|
||||||
|
|
||||||
|
get require() { return /:value|:count/.test(this._match_type) ? 'relational' : ''; }
|
||||||
|
|
||||||
|
get match_type()
|
||||||
|
{
|
||||||
|
return this._match_type;
|
||||||
|
}
|
||||||
|
set match_type(value)
|
||||||
|
{
|
||||||
|
// default?
|
||||||
|
if (':is' == value) {
|
||||||
|
value = '';
|
||||||
|
}
|
||||||
|
if (value.length && !getMatchTypes(0).includes(value)) {
|
||||||
|
throw 'Unsupported match-type ' + value;
|
||||||
|
}
|
||||||
|
if (':list' == value) {
|
||||||
|
this._comparator = '';
|
||||||
|
}
|
||||||
|
if (':count' != value && ':value' != value) {
|
||||||
|
this.relational_match = '';
|
||||||
|
}
|
||||||
|
this._match_type = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
get comparator()
|
||||||
|
{
|
||||||
|
return this._comparator;
|
||||||
|
}
|
||||||
|
set comparator(value)
|
||||||
|
{
|
||||||
|
if (!(value instanceof GrammarQuotedString)) {
|
||||||
|
value = new GrammarQuotedString(value);
|
||||||
|
}
|
||||||
|
// default?
|
||||||
|
if (value.length && 'i;ascii-casemap' != value.value) {
|
||||||
|
if (':list' == this._match_type) {
|
||||||
|
throw 'Comparator not allowed when using :list';
|
||||||
|
}
|
||||||
|
if (!getComparators().includes(value.value)) {
|
||||||
|
throw 'Unsupported comparator ' + value;
|
||||||
|
}
|
||||||
|
this._comparator = value;
|
||||||
|
} else {
|
||||||
|
this._comparator = '';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
toString()
|
toString()
|
||||||
{
|
{
|
||||||
// https://datatracker.ietf.org/doc/html/rfc6134#section-2.3
|
|
||||||
if (!getMatchTypes().includes(this.match_type)) {
|
|
||||||
throw 'Unsupported match-type ' + this.match_type;
|
|
||||||
}
|
|
||||||
return (this.identifier
|
return (this.identifier
|
||||||
+ (this.comparator ? ' :comparator ' + this.comparator : '')
|
+ (this._comparator ? ' :comparator ' + this._comparator : '')
|
||||||
+ (this.match_type ? ' ' + this.match_type : '')
|
+ (this._match_type ? ' ' + this._match_type : '')
|
||||||
|
+ (this.relational_match ? ' ' + this.relational_match : '')
|
||||||
+ ' ' + arrayToString(this.arguments, ' ')).trim();
|
+ ' ' + arrayToString(this.arguments, ' ')).trim();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -251,6 +280,7 @@ export class GrammarStringList extends Array
|
||||||
{
|
{
|
||||||
toString()
|
toString()
|
||||||
{
|
{
|
||||||
|
// if there is only a single string, the brackets are optional
|
||||||
if (1 < this.length) {
|
if (1 < this.length) {
|
||||||
return '[' + this.join(',') + ']';
|
return '[' + this.join(',') + ']';
|
||||||
}
|
}
|
||||||
|
|
@ -259,7 +289,7 @@ export class GrammarStringList extends Array
|
||||||
|
|
||||||
push(value)
|
push(value)
|
||||||
{
|
{
|
||||||
if (!(value instanceof GrammarString)) {
|
if (!(value instanceof GrammarQuotedString)) {
|
||||||
value = new GrammarQuotedString(value);
|
value = new GrammarQuotedString(value);
|
||||||
}
|
}
|
||||||
super.push(value);
|
super.push(value);
|
||||||
|
|
@ -286,10 +316,14 @@ GrammarStringList.fromString = list => {
|
||||||
|
|
||||||
export class GrammarQuotedString extends GrammarString
|
export class GrammarQuotedString extends GrammarString
|
||||||
{
|
{
|
||||||
|
constructor(value = '')
|
||||||
|
{
|
||||||
|
super(value instanceof GrammarQuotedString ? value.value : value);
|
||||||
|
}
|
||||||
|
|
||||||
toString()
|
toString()
|
||||||
{
|
{
|
||||||
return '"' + this._value.replace(/[\\"]/g, '\\$&') + '"';
|
return '"' + this._value.replace(/[\\"]/g, '\\$&') + '"';
|
||||||
// return '"' + super.toString().replace(/[\\"]/g, '\\$&') + '"';
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -324,3 +358,23 @@ GrammarMultiLine.fromString = string => {
|
||||||
}
|
}
|
||||||
return new GrammarMultiLine();
|
return new GrammarMultiLine();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class UnknownCommand extends GrammarCommand
|
||||||
|
{
|
||||||
|
constructor(identifier)
|
||||||
|
{
|
||||||
|
super(identifier);
|
||||||
|
this.commands = new GrammarCommands;
|
||||||
|
}
|
||||||
|
|
||||||
|
toString()
|
||||||
|
{
|
||||||
|
let result = this.identifier;
|
||||||
|
if (this.arguments?.length) {
|
||||||
|
result += ' ' + arrayToString(this.arguments, ' ');
|
||||||
|
}
|
||||||
|
return result + (
|
||||||
|
this.commands?.length ? ' ' + this.commands : ';'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ import {
|
||||||
import {
|
import {
|
||||||
GrammarBracketComment,
|
GrammarBracketComment,
|
||||||
GrammarCommand,
|
GrammarCommand,
|
||||||
|
GrammarComment,
|
||||||
GrammarHashComment,
|
GrammarHashComment,
|
||||||
GrammarMultiLine,
|
GrammarMultiLine,
|
||||||
GrammarNumber,
|
GrammarNumber,
|
||||||
|
|
@ -27,8 +28,8 @@ import {
|
||||||
GrammarTestList
|
GrammarTestList
|
||||||
} from 'Sieve/Grammar';
|
} from 'Sieve/Grammar';
|
||||||
|
|
||||||
import { availableCommands } from 'Sieve/Commands';
|
import { availableCommands, unavailableCommands } from 'Sieve/Commands';
|
||||||
import { ConditionalCommand, RequireCommand } from 'Sieve/Commands/Controls';
|
import { ConditionalCommand, IfCommand, RequireCommand } from 'Sieve/Commands/Controls';
|
||||||
import { NotTest } from 'Sieve/Commands/Tests';
|
import { NotTest } from 'Sieve/Commands/Tests';
|
||||||
|
|
||||||
const
|
const
|
||||||
|
|
@ -92,6 +93,7 @@ export const parseScript = (script, name = 'script.sieve') => {
|
||||||
|
|
||||||
// Only activate available commands
|
// Only activate available commands
|
||||||
const Commands = availableCommands();
|
const Commands = availableCommands();
|
||||||
|
const disabledCommands = unavailableCommands();
|
||||||
|
|
||||||
let match,
|
let match,
|
||||||
line = 1,
|
line = 1,
|
||||||
|
|
@ -117,18 +119,20 @@ export const parseScript = (script, name = 'script.sieve') => {
|
||||||
let prev_arg = args[args.length-1];
|
let prev_arg = args[args.length-1];
|
||||||
if (getMatchTypes(0).includes(arg)) {
|
if (getMatchTypes(0).includes(arg)) {
|
||||||
command.match_type = arg;
|
command.match_type = arg;
|
||||||
} else if (':value' === prev_arg || ':count' === prev_arg) {
|
} else if (getMatchTypes(0).includes(prev_arg)) {
|
||||||
// Sieve relational [RFC5231] match types
|
|
||||||
/^(gt|ge|lt|le|eq|ne)$/.test(arg.value) || error('Invalid relational match-type ' + arg);
|
|
||||||
command.match_type = prev_arg + ' ' + arg;
|
|
||||||
--args.length;
|
--args.length;
|
||||||
// requires.push('relational');
|
if (':value' === prev_arg || ':count' === prev_arg) {
|
||||||
|
// Sieve relational [RFC5231] match types
|
||||||
|
/^"(gt|ge|lt|le|eq|ne)"$/.test(arg) || error('Invalid relational match-type ' + arg);
|
||||||
|
command.relational_match = arg;
|
||||||
|
// requires.push('relational');
|
||||||
|
return;
|
||||||
|
}
|
||||||
} else if (':comparator' === prev_arg) {
|
} else if (':comparator' === prev_arg) {
|
||||||
command.comparator = arg;
|
command.comparator = arg;
|
||||||
--args.length;
|
--args.length;
|
||||||
} else {
|
|
||||||
args.push(arg);
|
|
||||||
}
|
}
|
||||||
|
args.push(arg);
|
||||||
},
|
},
|
||||||
pushArgs = () => {
|
pushArgs = () => {
|
||||||
if (args.length) {
|
if (args.length) {
|
||||||
|
|
@ -154,27 +158,30 @@ export const parseScript = (script, name = 'script.sieve') => {
|
||||||
pushArgs();
|
pushArgs();
|
||||||
value = value.toLowerCase();
|
value = value.toLowerCase();
|
||||||
let new_command;
|
let new_command;
|
||||||
if ('if' === value) {
|
if (Commands[value]) {
|
||||||
new_command = new ConditionalCommand(value);
|
if ('elsif' === value || 'else' === value) {
|
||||||
} else if ('elsif' === value || 'else' === value) {
|
let valid = false, cmd = (command ? command?.commands : tree), i = cmd?.length;
|
||||||
// (prev_command instanceof ConditionalCommand) || error('Not after IF condition');
|
while (i) {
|
||||||
new_command = new ConditionalCommand(value);
|
cmd[--i];
|
||||||
} else if (Commands[value]) {
|
if (cmd[i] instanceof IfCommand) {
|
||||||
|
valid = true;
|
||||||
|
break;
|
||||||
|
} else if (typeof cmd[i] !== 'string' && !(cmd[i] instanceof GrammarComment)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
valid || error('Not after IF/ELSIF condition');
|
||||||
|
}
|
||||||
if ('allof' === value || 'anyof' === value) {
|
if ('allof' === value || 'anyof' === value) {
|
||||||
// (command instanceof ConditionalCommand || command instanceof NotTest) || error('Test-list not in conditional');
|
// (command instanceof ConditionalCommand || command instanceof NotTest) || error('Test-list not in conditional');
|
||||||
}
|
}
|
||||||
new_command = new Commands[value]();
|
new_command = new Commands[value]();
|
||||||
|
} else if (disabledCommands[value]) {
|
||||||
|
console.error('Unsupported command: ' + value);
|
||||||
|
new_command = new disabledCommands[value]();
|
||||||
} else {
|
} else {
|
||||||
if (command && (
|
console.error('Unknown command: ' + value);
|
||||||
command instanceof ConditionalCommand
|
new_command = new GrammarCommand(value);
|
||||||
|| command instanceof NotTest
|
|
||||||
|| command.tests instanceof GrammarTestList)) {
|
|
||||||
console.error('Unknown test: ' + value);
|
|
||||||
new_command = new TestCommand(value);
|
|
||||||
} else {
|
|
||||||
console.error('Unknown command: ' + value);
|
|
||||||
new_command = new GrammarCommand(value);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (new_command instanceof TestCommand) {
|
if (new_command instanceof TestCommand) {
|
||||||
|
|
@ -192,7 +199,7 @@ export const parseScript = (script, name = 'script.sieve') => {
|
||||||
if (command.commands) {
|
if (command.commands) {
|
||||||
command.commands.push(new_command);
|
command.commands.push(new_command);
|
||||||
} else {
|
} else {
|
||||||
error('commands not allowed in "' + command.identifier + '" command');
|
error('command "' + new_command.identifier + '" not allowed in "' + command.identifier + '" command');
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
tree.push(new_command);
|
tree.push(new_command);
|
||||||
|
|
@ -219,7 +226,8 @@ export const parseScript = (script, name = 'script.sieve') => {
|
||||||
pushArg(GrammarMultiLine.fromString(value));
|
pushArg(GrammarMultiLine.fromString(value));
|
||||||
break;
|
break;
|
||||||
case T_QUOTED_STRING:
|
case T_QUOTED_STRING:
|
||||||
pushArg(new GrammarQuotedString(value.slice(1,-1)));
|
try { value = JSON.parse(value); } catch(e) { console.error(e, value); }
|
||||||
|
pushArg(new GrammarQuotedString(value));
|
||||||
break;
|
break;
|
||||||
case T_NUMBER:
|
case T_NUMBER:
|
||||||
pushArg(new GrammarNumber(value));
|
pushArg(new GrammarNumber(value));
|
||||||
|
|
@ -268,7 +276,6 @@ export const parseScript = (script, name = 'script.sieve') => {
|
||||||
break;
|
break;
|
||||||
case T_BLOCK_END:
|
case T_BLOCK_END:
|
||||||
(command instanceof ConditionalCommand) || error(TokenError[type]);
|
(command instanceof ConditionalCommand) || error(TokenError[type]);
|
||||||
// prev_command = command;
|
|
||||||
command = levels.up();
|
command = levels.up();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
|
@ -293,5 +300,6 @@ export const parseScript = (script, name = 'script.sieve') => {
|
||||||
}
|
}
|
||||||
|
|
||||||
tree.requires = requires;
|
tree.requires = requires;
|
||||||
|
tree.toString = () => tree.join('\r\n');
|
||||||
return tree;
|
return tree;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ export const
|
||||||
koComputable = fn => ko.computed(fn, {'pure':true}),
|
koComputable = fn => ko.computed(fn, {'pure':true}),
|
||||||
|
|
||||||
arrayToString = (arr, separator) =>
|
arrayToString = (arr, separator) =>
|
||||||
arr.map(item => item.toString?.() || item).join(separator),
|
(arr || []).map(item => item.toString?.() || item).join(separator),
|
||||||
/*
|
/*
|
||||||
getNotificationMessage = code => {
|
getNotificationMessage = code => {
|
||||||
let key = getKeyByValue(Notifications, code);
|
let key = getKeyByValue(Notifications, code);
|
||||||
|
|
@ -58,11 +58,33 @@ export const
|
||||||
serverErrorDesc(text);
|
serverErrorDesc(text);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getComparators = (validOnly = 0) => {
|
||||||
|
let result = [
|
||||||
|
// Default
|
||||||
|
'i;ascii-casemap',
|
||||||
|
];
|
||||||
|
if (capa.includes('relational') || !validOnly) {
|
||||||
|
result.push('i;octet');
|
||||||
|
}
|
||||||
|
if (capa.includes('comparator-i;ascii-numeric') || !validOnly) {
|
||||||
|
result.push('i;ascii-numeric');
|
||||||
|
}
|
||||||
|
if (capa.includes('comparator-i;unicode-casemap') || !validOnly) {
|
||||||
|
result.push('i;unicode-casemap');
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
|
||||||
getMatchTypes = (validOnly = 1) => {
|
getMatchTypes = (validOnly = 1) => {
|
||||||
let result = [':is',':contains',':matches'];
|
let result = [':is',':contains',':matches'];
|
||||||
// https://datatracker.ietf.org/doc/html/rfc6134#section-2.3
|
// https://datatracker.ietf.org/doc/html/rfc6134#section-2.3
|
||||||
|
// Only available for tests with a key_list property
|
||||||
if (capa.includes('extlists') || !validOnly) {
|
if (capa.includes('extlists') || !validOnly) {
|
||||||
result.push(':list');
|
result.push(':list');
|
||||||
}
|
}
|
||||||
|
if (capa.includes('relational') || !validOnly) {
|
||||||
|
result.push(':value');
|
||||||
|
result.push(':count');
|
||||||
|
}
|
||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,7 @@ export const SettingsUserStore = new class {
|
||||||
layout: 1,
|
layout: 1,
|
||||||
editorDefaultType: 'Html',
|
editorDefaultType: 'Html',
|
||||||
editorWysiwyg: 'Squire',
|
editorWysiwyg: 'Squire',
|
||||||
|
markdown: 0,
|
||||||
msgDefaultAction: 1
|
msgDefaultAction: 1
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -113,7 +114,8 @@ export const SettingsUserStore = new class {
|
||||||
'requireTLS',
|
'requireTLS',
|
||||||
'pgpSign',
|
'pgpSign',
|
||||||
'pgpEncrypt',
|
'pgpEncrypt',
|
||||||
'allowSpellcheck'
|
'allowSpellcheck',
|
||||||
|
'markdown'
|
||||||
/*
|
/*
|
||||||
'MessagesPerPage',
|
'MessagesPerPage',
|
||||||
'MessageReadDelay',
|
'MessageReadDelay',
|
||||||
|
|
|
||||||
|
|
@ -34,4 +34,9 @@
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tab-content {
|
||||||
|
grid-column-end: 3;
|
||||||
|
border: 1px solid var(--border-color, #ddd);
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,11 @@ export class PluginPopupView extends AbstractViewPopup {
|
||||||
saveError: '',
|
saveError: '',
|
||||||
id: '',
|
id: '',
|
||||||
name: '',
|
name: '',
|
||||||
readme: ''
|
readme: '',
|
||||||
|
author: '',
|
||||||
|
url: '',
|
||||||
|
version: '',
|
||||||
|
released: ''
|
||||||
});
|
});
|
||||||
|
|
||||||
this.config = ko.observableArray();
|
this.config = ko.observableArray();
|
||||||
|
|
@ -73,12 +77,20 @@ export class PluginPopupView extends AbstractViewPopup {
|
||||||
this.id('');
|
this.id('');
|
||||||
this.name('');
|
this.name('');
|
||||||
this.readme('');
|
this.readme('');
|
||||||
|
this.author('');
|
||||||
|
this.url('');
|
||||||
|
this.version('');
|
||||||
|
this.released('');
|
||||||
this.config([]);
|
this.config([]);
|
||||||
|
|
||||||
if (oPlugin) {
|
if (oPlugin) {
|
||||||
this.id(oPlugin.id);
|
this.id(oPlugin.id);
|
||||||
this.name(oPlugin.name);
|
this.name(oPlugin.name);
|
||||||
this.readme(oPlugin.readme);
|
this.readme(oPlugin.readme);
|
||||||
|
this.author(oPlugin.author);
|
||||||
|
this.url(oPlugin.url);
|
||||||
|
this.version(oPlugin.version);
|
||||||
|
this.released(oPlugin.released);
|
||||||
|
|
||||||
const config = oPlugin.config;
|
const config = oPlugin.config;
|
||||||
if (arrayLength(config)) {
|
if (arrayLength(config)) {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
This app packages SnappyMail <upstream>2.37.3</upstream>.
|
This app packages SnappyMail <upstream>2.38.0</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.37.3
|
VERSION=2.38.0
|
||||||
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,7 +3,7 @@
|
||||||
<id>snappymail</id>
|
<id>snappymail</id>
|
||||||
<name>SnappyMail</name>
|
<name>SnappyMail</name>
|
||||||
<summary>SnappyMail Webmail</summary>
|
<summary>SnappyMail Webmail</summary>
|
||||||
<version>2.37.3</version>
|
<version>2.38.0</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[**Lightweight & fast email client.**
|
<description><![CDATA[**Lightweight & fast email client.**
|
||||||
|
|
|
||||||
|
|
@ -90,26 +90,23 @@ class SnappyMailHelper
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
if ($doLogin && $aCredentials[1] && $aCredentials[2]) {
|
if ($doLogin && $aCredentials[1] && $aCredentials[2]) {
|
||||||
|
$isOIDC = \str_starts_with($aCredentials[2], 'oidc_login|');
|
||||||
try {
|
try {
|
||||||
$ocSession = \OC::$server->getSession();
|
$ocSession = \OC::$server->getSession();
|
||||||
if ($ocSession->get('is_oidc')) {
|
$oAccount = $oActions->LoginProcess($aCredentials[1], $aCredentials[2]);
|
||||||
$pwd = new \SnappyMail\SensitiveString($aCredentials[1]);
|
if (!$isOIDC && $oAccount
|
||||||
$oAccount = $oActions->LoginProcess($aCredentials[1], $pwd);
|
&& $oConfig->Get('login', 'sign_me_auto', \RainLoop\Enumerations\SignMeType::DefaultOff) === \RainLoop\Enumerations\SignMeType::DefaultOn
|
||||||
if ($oAccount) {
|
) {
|
||||||
$oActions->SetSignMeToken($oAccount);
|
$oActions->SetSignMeToken($oAccount);
|
||||||
}
|
|
||||||
} else {
|
|
||||||
$oAccount = $oActions->LoginProcess($aCredentials[1], $aCredentials[2]);
|
|
||||||
if ($oAccount && $oConfig->Get('login', 'sign_me_auto', \RainLoop\Enumerations\SignMeType::DefaultOff) === \RainLoop\Enumerations\SignMeType::DefaultOn) {
|
|
||||||
$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();
|
if (!$isOIDC) {
|
||||||
\OC::$server->getSession()['snappymail-passphrase'] = '';
|
$sUID = \OC::$server->getUserSession()->getUser()->getUID();
|
||||||
\OC::$server->getConfig()->setUserValue($sUID, 'snappymail', 'passphrase', '');
|
\OC::$server->getSession()['snappymail-passphrase'] = '';
|
||||||
\SnappyMail\Log::error('Nextcloud', $e->getMessage());
|
\OC::$server->getConfig()->setUserValue($sUID, 'snappymail', 'passphrase', '');
|
||||||
|
\SnappyMail\Log::error('Nextcloud', $e->getMessage());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -126,6 +123,32 @@ class SnappyMailHelper
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if OpenID Connect (OIDC) is enabled and used for login
|
||||||
|
// https://apps.nextcloud.com/apps/oidc_login
|
||||||
|
public static function isOIDCLogin() : bool
|
||||||
|
{
|
||||||
|
$config = \OC::$server->getConfig();
|
||||||
|
if ($config->getAppValue('snappymail', 'snappymail-autologin-oidc', false)) {
|
||||||
|
// Check if the OIDC Login app is enabled
|
||||||
|
if (\OC::$server->getAppManager()->isEnabledForUser('oidc_login')) {
|
||||||
|
// Check if session is an OIDC Login
|
||||||
|
$ocSession = \OC::$server->getSession();
|
||||||
|
if ($ocSession->get('is_oidc')) {
|
||||||
|
// IToken->getPassword() ???
|
||||||
|
if ($ocSession->get('oidc_access_token')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
\SnappyMail\Log::debug('Nextcloud', 'OIDC access_token missing');
|
||||||
|
} else {
|
||||||
|
\SnappyMail\Log::debug('Nextcloud', 'No OIDC login');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
\SnappyMail\Log::debug('Nextcloud', 'OIDC login disabled');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
private static function getLoginCredentials() : array
|
private static function getLoginCredentials() : array
|
||||||
{
|
{
|
||||||
$sUID = \OC::$server->getUserSession()->getUser()->getUID();
|
$sUID = \OC::$server->getUserSession()->getUser()->getUID();
|
||||||
|
|
@ -151,18 +174,9 @@ class SnappyMailHelper
|
||||||
if ($ocSession['snappymail-nc-uid'] == $sUID) {
|
if ($ocSession['snappymail-nc-uid'] == $sUID) {
|
||||||
|
|
||||||
// If OpenID Connect (OIDC) is enabled and used for login, use this.
|
// If OpenID Connect (OIDC) is enabled and used for login, use this.
|
||||||
// https://apps.nextcloud.com/apps/oidc_login
|
if (static::isOIDCLogin()) {
|
||||||
if ($config->getAppValue('snappymail', 'snappymail-autologin-oidc', false)) {
|
$sEmail = $config->getUserValue($sUID, 'settings', 'email');
|
||||||
if ($ocSession->get('is_oidc')) {
|
return [$sUID, $sEmail, "oidc_login|{$sUID}"];
|
||||||
// IToken->getPassword() ???
|
|
||||||
if ($sAccessToken = $ocSession->get('oidc_access_token')) {
|
|
||||||
$sEmail = $config->getUserValue($sUID, 'settings', 'email');
|
|
||||||
return [$sUID, $sEmail, $sAccessToken];
|
|
||||||
}
|
|
||||||
\SnappyMail\Log::debug('Nextcloud', 'OIDC access_token missing');
|
|
||||||
} else {
|
|
||||||
\SnappyMail\Log::debug('Nextcloud', 'No OIDC login');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only use the user's password in the current session if they have
|
// Only use the user's password in the current session if they have
|
||||||
|
|
|
||||||
|
|
@ -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.37.3" );
|
return ( "2.38.0" );
|
||||||
}
|
}
|
||||||
|
|
||||||
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.37.3",
|
"version": "2.38.0",
|
||||||
"homepage": "https://snappymail.eu",
|
"homepage": "https://snappymail.eu",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "DJ Maze",
|
"name": "DJ Maze",
|
||||||
|
|
|
||||||
|
|
@ -124,11 +124,20 @@ $Plugin->addHook('hook.name', 'functionName');
|
||||||
params:
|
params:
|
||||||
string &$sEmail
|
string &$sEmail
|
||||||
|
|
||||||
|
Happens in resolveLoginCredentials($sEmail) BEFORE resolving domain name.
|
||||||
|
This is the pure text from the login screen (DoLogin) or the SSO feature (ServiceSso) received by LoginProcess().
|
||||||
|
- DoLogin() -> LoginProcess() -> resolveLoginCredentials($sEmail)
|
||||||
|
- ServiceSso() -> LoginProcess() -> resolveLoginCredentials($sEmail)
|
||||||
|
So $sEmail can just have the value `test` without a domain.
|
||||||
|
|
||||||
### login.credentials.step-2
|
### login.credentials.step-2
|
||||||
params:
|
params:
|
||||||
string &$sEmail
|
string &$sEmail
|
||||||
string &$sPassword
|
string &$sPassword
|
||||||
|
|
||||||
|
Happens in resolveLoginCredentials($sEmail) AFTER resolving domain name.
|
||||||
|
So $sEmail always has a domain (for example `test` is now `test@example.com`).
|
||||||
|
|
||||||
### login.credentials
|
### login.credentials
|
||||||
params:
|
params:
|
||||||
string &$sEmail
|
string &$sEmail
|
||||||
|
|
@ -136,6 +145,10 @@ $Plugin->addHook('hook.name', 'functionName');
|
||||||
string &$sPassword
|
string &$sPassword
|
||||||
string &$sSmtpUser
|
string &$sSmtpUser
|
||||||
|
|
||||||
|
$sEmail is the domain imap->fixUsername() without shortening.
|
||||||
|
$sImapUser is the domain imap->fixUsername() for login into IMAP.
|
||||||
|
$sSmtpUser is the domain smtp->fixUsername() for login into SMTP.
|
||||||
|
|
||||||
### login.success
|
### login.success
|
||||||
params:
|
params:
|
||||||
\RainLoop\Model\MainAccount $oAccount
|
\RainLoop\Model\MainAccount $oAccount
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,10 @@ class Memcache implements \MailSo\Cache\DriverInterface
|
||||||
|
|
||||||
public function Set(string $sKey, string $sValue) : bool
|
public function Set(string $sKey, string $sValue) : bool
|
||||||
{
|
{
|
||||||
return $this->oMem ? $this->oMem->set($this->generateCachedKey($sKey), $sValue, 0, $this->iExpire) : false;
|
if ($this->oMem instanceof \Memcache) {
|
||||||
|
return $this->oMem->set($this->generateCachedKey($sKey), $sValue, 0, $this->iExpire);
|
||||||
|
}
|
||||||
|
return $this->oMem ? $this->oMem->set($this->generateCachedKey($sKey), $sValue, $this->iExpire) : false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function Exists(string $sKey) : bool
|
public function Exists(string $sKey) : bool
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,8 @@ class CacheMemcachePlugin extends \RainLoop\Plugins\AbstractPlugin
|
||||||
{
|
{
|
||||||
const
|
const
|
||||||
NAME = 'Cache Memcache',
|
NAME = 'Cache Memcache',
|
||||||
VERSION = '2.36',
|
VERSION = '2.37',
|
||||||
RELEASE = '2024-03-22',
|
RELEASE = '2024-09-15',
|
||||||
REQUIRED = '2.36.0',
|
REQUIRED = '2.36.0',
|
||||||
CATEGORY = 'Cache',
|
CATEGORY = 'Cache',
|
||||||
DESCRIPTION = 'Cache handler using PHP Memcache or PHP Memcached';
|
DESCRIPTION = 'Cache handler using PHP Memcache or PHP Memcached';
|
||||||
|
|
|
||||||
|
|
@ -472,7 +472,7 @@ class Client implements ClientInterface, IteratorAggregate
|
||||||
*
|
*
|
||||||
* @return Pipeline|array
|
* @return Pipeline|array
|
||||||
*/
|
*/
|
||||||
protected function createPipeline(array $options = null, $callable = null)
|
protected function createPipeline(?array $options = null, $callable = null)
|
||||||
{
|
{
|
||||||
if (isset($options['atomic']) && $options['atomic']) {
|
if (isset($options['atomic']) && $options['atomic']) {
|
||||||
$class = Atomic::class;
|
$class = Atomic::class;
|
||||||
|
|
@ -525,7 +525,7 @@ class Client implements ClientInterface, IteratorAggregate
|
||||||
*
|
*
|
||||||
* @return MultiExecTransaction|array
|
* @return MultiExecTransaction|array
|
||||||
*/
|
*/
|
||||||
protected function createTransaction(array $options = null, $callable = null)
|
protected function createTransaction(?array $options = null, $callable = null)
|
||||||
{
|
{
|
||||||
$transaction = new MultiExecTransaction($this, $options);
|
$transaction = new MultiExecTransaction($this, $options);
|
||||||
|
|
||||||
|
|
@ -557,7 +557,7 @@ class Client implements ClientInterface, IteratorAggregate
|
||||||
*
|
*
|
||||||
* @return PubSubConsumer|null
|
* @return PubSubConsumer|null
|
||||||
*/
|
*/
|
||||||
protected function createPubSub(array $options = null, $callable = null)
|
protected function createPubSub(?array $options = null, $callable = null)
|
||||||
{
|
{
|
||||||
if ($this->connection instanceof RelayConnection) {
|
if ($this->connection instanceof RelayConnection) {
|
||||||
$pubsub = new RelayPubSubConsumer($this, $options);
|
$pubsub = new RelayPubSubConsumer($this, $options);
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ class Consumer extends AbstractConsumer
|
||||||
* @param ClientInterface $client Client instance used by the consumer.
|
* @param ClientInterface $client Client instance used by the consumer.
|
||||||
* @param array $options Options for the consumer initialization.
|
* @param array $options Options for the consumer initialization.
|
||||||
*/
|
*/
|
||||||
public function __construct(ClientInterface $client, array $options = null)
|
public function __construct(ClientInterface $client, ?array $options = null)
|
||||||
{
|
{
|
||||||
$this->checkCapabilities($client);
|
$this->checkCapabilities($client);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,7 @@ class MultiExec implements ClientContextInterface
|
||||||
* @param ClientInterface $client Client instance used by the transaction.
|
* @param ClientInterface $client Client instance used by the transaction.
|
||||||
* @param array $options Initialization options.
|
* @param array $options Initialization options.
|
||||||
*/
|
*/
|
||||||
public function __construct(ClientInterface $client, array $options = null)
|
public function __construct(ClientInterface $client, ?array $options = null)
|
||||||
{
|
{
|
||||||
$this->assertClient($client);
|
$this->assertClient($client);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,9 @@ class CustomLoginMappingPlugin extends \RainLoop\Plugins\AbstractPlugin
|
||||||
{
|
{
|
||||||
const
|
const
|
||||||
NAME = 'Custom Login Mapping',
|
NAME = 'Custom Login Mapping',
|
||||||
VERSION = '2.1',
|
VERSION = '2.3',
|
||||||
RELEASE = '2021-04-21',
|
RELEASE = '2024-09-21',
|
||||||
REQUIRED = '2.5.0',
|
REQUIRED = '2.36.1',
|
||||||
CATEGORY = 'Login',
|
CATEGORY = 'Login',
|
||||||
DESCRIPTION = 'Enables custom usernames by email address.';
|
DESCRIPTION = 'Enables custom usernames by email address.';
|
||||||
|
|
||||||
|
|
@ -17,23 +17,29 @@ class CustomLoginMappingPlugin extends \RainLoop\Plugins\AbstractPlugin
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param string $sEmail
|
* @param string $sEmail
|
||||||
* @param string $sLogin
|
* @param string $sImapUser
|
||||||
* @param string $sPassword
|
* @param string $sPassword
|
||||||
|
* @param string $sSmtpUser
|
||||||
*
|
*
|
||||||
* @throws \RainLoop\Exceptions\ClientException
|
* @throws \RainLoop\Exceptions\ClientException
|
||||||
*/
|
*/
|
||||||
public function FilterLoginCredentials(&$sEmail, &$sLogin, &$sPassword)
|
public function FilterLoginCredentials(string &$sEmail, string &$sImapUser, string &$sPassword, string &$sSmtpUser)
|
||||||
{
|
{
|
||||||
$sMapping = \trim($this->Config()->Get('plugin', 'mapping', ''));
|
$sMapping = \trim($this->Config()->Get('plugin', 'mapping', ''));
|
||||||
if (!empty($sMapping)) {
|
if (!empty($sMapping)) {
|
||||||
$aLines = \explode("\n", \preg_replace('/[\r\n\t\s]+/', "\n", $sMapping));
|
$aLines = \explode("\n", \preg_replace('/[\r\n\t\s]+/', "\n", $sMapping));
|
||||||
foreach ($aLines as $sLine) {
|
foreach ($aLines as $sLine) {
|
||||||
if (false !== \strpos($sLine, ':')) {
|
if (false !== \strpos($sLine, ':')) {
|
||||||
$aData = \explode(':', $sLine, 2);
|
$aData = \explode(':', $sLine, 3);
|
||||||
if (\is_array($aData) && !empty($aData[0]) && isset($aData[1])) {
|
if (\is_array($aData) && !empty($aData[0]) && isset($aData[1])) {
|
||||||
$aData = \array_map('trim', $aData);
|
$aData = \array_map('trim', $aData);
|
||||||
if ($sEmail === $aData[0] && \strlen($aData[1])) {
|
if ($sEmail === $aData[0]) {
|
||||||
$sLogin = $aData[1];
|
if (\strlen($aData[1])) {
|
||||||
|
$sImapUser = $aData[1];
|
||||||
|
}
|
||||||
|
if (isset($aData[2]) && \strlen($aData[2])) {
|
||||||
|
$sSmtpUser = $aData[2];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -46,8 +52,8 @@ class CustomLoginMappingPlugin extends \RainLoop\Plugins\AbstractPlugin
|
||||||
return array(
|
return array(
|
||||||
\RainLoop\Plugins\Property::NewInstance('mapping')->SetLabel('Mapping')
|
\RainLoop\Plugins\Property::NewInstance('mapping')->SetLabel('Mapping')
|
||||||
->SetType(\RainLoop\Enumerations\PluginPropertyType::STRING_TEXT)
|
->SetType(\RainLoop\Enumerations\PluginPropertyType::STRING_TEXT)
|
||||||
->SetDescription('email:login mapping')
|
->SetDescription('email:imap-login:smtp-login mapping')
|
||||||
->SetDefaultValue("user@domain.com:user.bob\nadmin@domain.com:user.john")
|
->SetDefaultValue("user@domain.com:imapuser.bob:smtpuser.bob\nadmin@domain.com:imapuser.john")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
20
plugins/ics-viewer/LICENSE
Normal file
20
plugins/ics-viewer/LICENSE
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2022 SnappyMail
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
this software and associated documentation files (the "Software"), to deal in
|
||||||
|
the Software without restriction, including without limitation the rights to
|
||||||
|
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
9526
plugins/ics-viewer/ical.js
Normal file
9526
plugins/ics-viewer/ical.js
Normal file
File diff suppressed because it is too large
Load diff
22
plugins/ics-viewer/index.php
Normal file
22
plugins/ics-viewer/index.php
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
class ICSViewerPlugin extends \RainLoop\Plugins\AbstractPlugin
|
||||||
|
{
|
||||||
|
const
|
||||||
|
NAME = 'ICS Viewer',
|
||||||
|
AUTHOR = 'PhieF',
|
||||||
|
VERSION = '1.0',
|
||||||
|
RELEASE = '2024-09-17',
|
||||||
|
CATEGORY = 'Messages',
|
||||||
|
DESCRIPTION = 'Display ICS attachment using ical lib, or JSON-LD details, based on viewICS',
|
||||||
|
REQUIRED = '2.34.0';
|
||||||
|
|
||||||
|
public function Init() : void
|
||||||
|
{
|
||||||
|
// $this->UseLangs(true);
|
||||||
|
$this->addJs('message.js');
|
||||||
|
$this->addJs('windowsZones.js');
|
||||||
|
// Load https://github.com/kewisch/ical.js/releases
|
||||||
|
$this->addJs('ical.js');
|
||||||
|
}
|
||||||
|
}
|
||||||
136
plugins/ics-viewer/message.js
Normal file
136
plugins/ics-viewer/message.js
Normal file
|
|
@ -0,0 +1,136 @@
|
||||||
|
(rl => {
|
||||||
|
const templateId = 'MailMessageView';
|
||||||
|
|
||||||
|
addEventListener('rl-view-model.create', e => {
|
||||||
|
if (templateId === e.detail.viewModelTemplateID) {
|
||||||
|
|
||||||
|
const
|
||||||
|
template = document.getElementById(templateId),
|
||||||
|
view = e.detail,
|
||||||
|
attachmentsPlace = template.content.querySelector('.attachmentsPlace'),
|
||||||
|
dateRegEx = /(TZID=(?<tz>[^:]+):)?(?<year>[0-9]{4})(?<month>[0-9]{2})(?<day>[0-9]{2})T(?<hour>[0-9]{2})(?<minute>[0-9]{2})(?<second>[0-9]{2})(?<utc>Z?)/,
|
||||||
|
parseDate = str => {
|
||||||
|
let parts = dateRegEx.exec(str)?.groups,
|
||||||
|
options = {dateStyle: 'long', timeStyle: 'short'},
|
||||||
|
date = (parts ? new Date(
|
||||||
|
parseInt(parts.year, 10),
|
||||||
|
parseInt(parts.month, 10) - 1,
|
||||||
|
parseInt(parts.day, 10),
|
||||||
|
parseInt(parts.hour, 10),
|
||||||
|
parseInt(parts.minute, 10),
|
||||||
|
parseInt(parts.second, 10)
|
||||||
|
) : new Date(str));
|
||||||
|
parts?.tz && (options.timeZone = windowsVTIMEZONEs[parts.tz] || parts.tz);
|
||||||
|
try {
|
||||||
|
return date.format(options);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
if (options.timeZone) {
|
||||||
|
options.timeZone = undefined;
|
||||||
|
return date.format(options);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
attachmentsPlace.after(Element.fromHTML(`
|
||||||
|
<details data-bind="if: ICSViewer, visible: ICSViewer">
|
||||||
|
<summary data-icon="📅" data-bind="text: ICSViewer().SUMMARY"></summary>
|
||||||
|
<table><tbody style="white-space:pre">
|
||||||
|
<tr data-bind="visible: ICSViewer().ORGANIZER_TXT"><td>Organizer: </td><td><a data-bind="text: ICSViewer().ORGANIZER_TXT, attr: { href: ICSViewer().ORGANIZER_MAIL }"></a></td></tr>
|
||||||
|
<tr><td>Start: </td><td data-bind="text: ICSViewer().DTSTART"></td></tr>
|
||||||
|
<tr><td>End: </td><td data-bind="text: ICSViewer().DTEND"></td></tr>
|
||||||
|
<tr data-bind="visible: ICSViewer().LOCATION"><td>Location: </td><td data-bind="text: ICSViewer().LOCATION"></td></tr>
|
||||||
|
<!-- <tr><td>Transparency</td><td data-bind="text: ICSViewer().TRANSP"></td></tr>-->
|
||||||
|
<tr><td>Attendees: </td><td data-bind="foreach: ICSViewer().ATTENDEE"><span data-bind="text: $data.replace(/;/g,';\\n')"></span> </td>
|
||||||
|
|
||||||
|
</tbody></table>
|
||||||
|
</details>`));
|
||||||
|
|
||||||
|
view.ICSViewer = ko.observable(null);
|
||||||
|
|
||||||
|
view.saveICS = () => {
|
||||||
|
let VEVENT = view.VEVENT();
|
||||||
|
if (VEVENT) {
|
||||||
|
if (rl.nextcloud && VEVENT.rawText) {
|
||||||
|
rl.nextcloud.selectCalendar()
|
||||||
|
.then(href => href && rl.nextcloud.calendarPut(href, VEVENT));
|
||||||
|
} else {
|
||||||
|
// TODO
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TODO
|
||||||
|
*/
|
||||||
|
view.message.subscribe(msg => {
|
||||||
|
view.ICSViewer(null);
|
||||||
|
if (msg) {
|
||||||
|
// JSON-LD after parsing HTML
|
||||||
|
// See http://schema.org/
|
||||||
|
msg.linkedData.subscribe(data => {
|
||||||
|
if (!view.ICSViewer()) {
|
||||||
|
data.forEach(item => {
|
||||||
|
if (item["ical:summary"]) {
|
||||||
|
let VEVENT = {
|
||||||
|
SUMMARY: item["ical:summary"],
|
||||||
|
DTSTART: parseDate(item["ical:dtstart"]),
|
||||||
|
// DTEND: parseDate(item["ical:dtend"]),
|
||||||
|
// TRANSP: item["ical:transp"],
|
||||||
|
// LOCATION: item["ical:location"],
|
||||||
|
ATTENDEE: []
|
||||||
|
}
|
||||||
|
view.ICSViewer(VEVENT);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// ICS attachment
|
||||||
|
// let ics = msg.attachments.find(attachment => 'application/ics' == attachment.mimeType);
|
||||||
|
|
||||||
|
let ics = msg.attachments.find(attachment => 'text/calendar' == attachment.mimeType);
|
||||||
|
if (ics && ics.download) {
|
||||||
|
|
||||||
|
// fetch it and parse the VEVENT
|
||||||
|
rl.fetch(ics.linkDownload())
|
||||||
|
.then(response => (response.status < 400) ? response.text() : Promise.reject(new Error({ response })))
|
||||||
|
.then(text => {
|
||||||
|
let jcalData = ICAL.parse(text)
|
||||||
|
var comp = new ICAL.Component(jcalData);
|
||||||
|
var vevent = comp.getFirstSubcomponent("vevent");
|
||||||
|
var event = new ICAL.Event(vevent);
|
||||||
|
let VEVENT = {};
|
||||||
|
if (event.organizer && event.organizer.startsWith("mailto:")) {
|
||||||
|
VEVENT.ORGANIZER_TXT = event.organizer.substr(7)
|
||||||
|
VEVENT.ORGANIZER_MAIL = event.organizer
|
||||||
|
} else
|
||||||
|
VEVENT.ORGANIZER_TXT = event.organizer
|
||||||
|
VEVENT.SUMMARY = event.summary;
|
||||||
|
VEVENT.DTSTART = parseDate(vevent.getFirstPropertyValue("dtstart"));
|
||||||
|
VEVENT.DTEND = parseDate(vevent.getFirstPropertyValue("dtend"));
|
||||||
|
VEVENT.LOCATION = event.location;
|
||||||
|
VEVENT.ATTENDEE = []
|
||||||
|
for (let attendee of event.attendees) {
|
||||||
|
VEVENT.ATTENDEE.push(attendee.getFirstParameter("cn"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (VEVENT) {
|
||||||
|
VEVENT.rawText = text;
|
||||||
|
VEVENT.isCancelled = () => VEVENT.STATUS?.includes('CANCELLED');
|
||||||
|
VEVENT.isConfirmed = () => VEVENT.STATUS?.includes('CONFIRMED');
|
||||||
|
VEVENT.shouldReply = () => VEVENT.METHOD?.includes('REPLY');
|
||||||
|
console.dir({
|
||||||
|
isCancelled: VEVENT.isCancelled(),
|
||||||
|
shouldReply: VEVENT.shouldReply()
|
||||||
|
});
|
||||||
|
view.ICSViewer(VEVENT);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
})(window.rl);
|
||||||
44
plugins/ics-viewer/style.css
Normal file
44
plugins/ics-viewer/style.css
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
|
||||||
|
/**
|
||||||
|
* .SML-@type where @type is the value of the JSON-LD "@type":
|
||||||
|
* See http://schema.org/
|
||||||
|
*/
|
||||||
|
|
||||||
|
.SML-FlightReservation {
|
||||||
|
}
|
||||||
|
|
||||||
|
.SML-Airline {
|
||||||
|
}
|
||||||
|
|
||||||
|
.SML-Airport {
|
||||||
|
}
|
||||||
|
|
||||||
|
.SML-Flight {
|
||||||
|
}
|
||||||
|
|
||||||
|
.SML-FoodEstablishmentReservation {
|
||||||
|
}
|
||||||
|
|
||||||
|
.SML-FoodEstablishment {
|
||||||
|
}
|
||||||
|
|
||||||
|
.SML-ParcelDelivery {
|
||||||
|
}
|
||||||
|
|
||||||
|
.SML-Order {
|
||||||
|
}
|
||||||
|
|
||||||
|
.SML-Organization {
|
||||||
|
}
|
||||||
|
|
||||||
|
.SML-Product {
|
||||||
|
}
|
||||||
|
|
||||||
|
.SML-PostalAddress {
|
||||||
|
}
|
||||||
|
|
||||||
|
.SML-Person {
|
||||||
|
}
|
||||||
|
|
||||||
|
.SML-PromotionCard {
|
||||||
|
}
|
||||||
|
|
@ -8,10 +8,10 @@ class LDAPLoginMappingPlugin extends AbstractPlugin
|
||||||
{
|
{
|
||||||
const
|
const
|
||||||
NAME = 'LDAP login mapping',
|
NAME = 'LDAP login mapping',
|
||||||
VERSION = '2.2',
|
VERSION = '2.3',
|
||||||
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 = '2024-03-12',
|
RELEASE = '2024-09-20',
|
||||||
REQUIRED = '2.35.3',
|
REQUIRED = '2.36.1',
|
||||||
CATEGORY = 'Login',
|
CATEGORY = 'Login',
|
||||||
DESCRIPTION = 'Enable custom mapping using ldap field';
|
DESCRIPTION = 'Enable custom mapping using ldap field';
|
||||||
/**
|
/**
|
||||||
|
|
@ -79,12 +79,13 @@ class LDAPLoginMappingPlugin extends AbstractPlugin
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param string $sEmail
|
* @param string $sEmail
|
||||||
* @param string $sLogin
|
* @param string $sImapUser
|
||||||
* @param string $sPassword
|
* @param string $sPassword
|
||||||
|
* @param string $sSmtpUser
|
||||||
*
|
*
|
||||||
* @throws \RainLoop\Exceptions\ClientException
|
* @throws \RainLoop\Exceptions\ClientException
|
||||||
*/
|
*/
|
||||||
public function FilterLoginСredentials(&$sEmail, &$sLogin, &$sPassword)
|
public function FilterLoginСredentials(string &$sEmail, string &$sImapUser, string &$sPassword, string &$sSmtpUser)
|
||||||
{
|
{
|
||||||
$this->oLogger = \RainLoop\Api::Logger();
|
$this->oLogger = \RainLoop\Api::Logger();
|
||||||
|
|
||||||
|
|
@ -102,10 +103,10 @@ class LDAPLoginMappingPlugin extends AbstractPlugin
|
||||||
$sIP = $_SERVER['REMOTE_ADDR'];
|
$sIP = $_SERVER['REMOTE_ADDR'];
|
||||||
$sResult = $this->ldapSearch($sEmail);
|
$sResult = $this->ldapSearch($sEmail);
|
||||||
if ( is_array($sResult) ) {
|
if ( is_array($sResult) ) {
|
||||||
$sLogin = $sResult['login'];
|
$sImapUser = $sResult['login'];
|
||||||
$sEmail = $sResult['email'];
|
$sEmail = $sResult['email'];
|
||||||
}
|
}
|
||||||
syslog(LOG_WARNING, "plugins/ldap-login-mapping/index.php:FilterLoginСredentials() auth try: $sIP/$sEmail, resolved as $sLogin/$sEmail");
|
syslog(LOG_WARNING, "plugins/ldap-login-mapping/index.php:FilterLoginСredentials() auth try: $sIP/$sEmail, resolved as $sImapUser/$sEmail");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,10 +12,10 @@ class LdapMailAccountsPlugin extends AbstractPlugin
|
||||||
{
|
{
|
||||||
const
|
const
|
||||||
NAME = 'LDAP Mail Accounts',
|
NAME = 'LDAP Mail Accounts',
|
||||||
VERSION = '2.2.0',
|
VERSION = '2.2.1',
|
||||||
AUTHOR = 'cm-schl',
|
AUTHOR = 'cm-schl',
|
||||||
URL = 'https://github.com/cm-sch',
|
URL = 'https://github.com/cm-sch',
|
||||||
RELEASE = '2024-05-28',
|
RELEASE = '2024-09-20',
|
||||||
REQUIRED = '2.36.1',
|
REQUIRED = '2.36.1',
|
||||||
CATEGORY = 'Accounts',
|
CATEGORY = 'Accounts',
|
||||||
DESCRIPTION = 'Add additional mail accounts the SnappyMail user has access to by a LDAP query. Basing on the work of FWest98 (https://github.com/FWest98).';
|
DESCRIPTION = 'Add additional mail accounts the SnappyMail user has access to by a LDAP query. Basing on the work of FWest98 (https://github.com/FWest98).';
|
||||||
|
|
@ -44,7 +44,7 @@ class LdapMailAccountsPlugin extends AbstractPlugin
|
||||||
* @param string &$sPassword
|
* @param string &$sPassword
|
||||||
* @param string &$sSmtpUser
|
* @param string &$sSmtpUser
|
||||||
*/
|
*/
|
||||||
public function overwriteMainAccountEmail(&$sEmail, &$sImapUser, &$sPassword, &$sSmtpUser)
|
public function overwriteMainAccountEmail(string &$sEmail, string &$sImapUser, string &$sPassword, string &$sSmtpUser)
|
||||||
{
|
{
|
||||||
$this->Manager()->Actions()->Logger()->Write("Login DATA: login: $sImapUser email: $sEmail", \LOG_WARNING, "LDAP MAIL ACCOUNTS PLUGIN");
|
$this->Manager()->Actions()->Logger()->Write("Login DATA: login: $sImapUser email: $sEmail", \LOG_WARNING, "LDAP MAIL ACCOUNTS PLUGIN");
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -401,7 +401,7 @@ class Client
|
||||||
* @param int $form_content_type HTTP form content type to use
|
* @param int $form_content_type HTTP form content type to use
|
||||||
* @return array
|
* @return array
|
||||||
*/
|
*/
|
||||||
private function executeRequest($url, $parameters = array(), $http_method = self::HTTP_METHOD_GET, array $http_headers = null, $form_content_type = self::HTTP_FORM_CONTENT_TYPE_MULTIPART)
|
private function executeRequest($url, $parameters = array(), $http_method = self::HTTP_METHOD_GET, ?array $http_headers = null, $form_content_type = self::HTTP_FORM_CONTENT_TYPE_MULTIPART)
|
||||||
{
|
{
|
||||||
$curl_options = array(
|
$curl_options = array(
|
||||||
CURLOPT_RETURNTRANSFER => true,
|
CURLOPT_RETURNTRANSFER => true,
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,9 @@ class LoginOverridePlugin extends \RainLoop\Plugins\AbstractPlugin
|
||||||
{
|
{
|
||||||
const
|
const
|
||||||
NAME = 'Login Override',
|
NAME = 'Login Override',
|
||||||
VERSION = '2.3',
|
VERSION = '2.4',
|
||||||
RELEASE = '2024-03-12',
|
RELEASE = '2024-09-20',
|
||||||
REQUIRED = '2.35.3',
|
REQUIRED = '2.36.1',
|
||||||
CATEGORY = 'Filters',
|
CATEGORY = 'Filters',
|
||||||
DESCRIPTION = 'Override IMAP/SMTP login credentials for specific users.';
|
DESCRIPTION = 'Override IMAP/SMTP login credentials for specific users.';
|
||||||
|
|
||||||
|
|
@ -17,7 +17,7 @@ class LoginOverridePlugin extends \RainLoop\Plugins\AbstractPlugin
|
||||||
$this->addHook('smtp.before-login', 'MapSmtpCredentials');
|
$this->addHook('smtp.before-login', 'MapSmtpCredentials');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function MapEmailAddress(string &$sEmail, string &$sLogin, string &$sPassword)
|
public function MapEmailAddress(string &$sEmail, string &$sImapUser, string &$sPassword, string &$sSmtpUser)
|
||||||
{
|
{
|
||||||
$sMapping = \trim($this->Config()->Get('plugin', 'email_mapping', ''));
|
$sMapping = \trim($this->Config()->Get('plugin', 'email_mapping', ''));
|
||||||
if (!empty($sMapping)) {
|
if (!empty($sMapping)) {
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,8 @@ class LoginRemotePlugin extends \RainLoop\Plugins\AbstractPlugin
|
||||||
NAME = 'Login Remote',
|
NAME = 'Login Remote',
|
||||||
AUTHOR = 'SnappyMail',
|
AUTHOR = 'SnappyMail',
|
||||||
URL = 'https://snappymail.eu/',
|
URL = 'https://snappymail.eu/',
|
||||||
VERSION = '1.4',
|
VERSION = '1.5',
|
||||||
RELEASE = '2024-03-27',
|
RELEASE = '2024-09-20',
|
||||||
REQUIRED = '2.36.1',
|
REQUIRED = '2.36.1',
|
||||||
CATEGORY = 'Login',
|
CATEGORY = 'Login',
|
||||||
LICENSE = 'MIT',
|
LICENSE = 'MIT',
|
||||||
|
|
@ -56,7 +56,7 @@ class LoginRemotePlugin extends \RainLoop\Plugins\AbstractPlugin
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function FilterLoginCredentials(&$sEmail, &$sImapUser, &$sPassword, &$sSmtpUser)
|
public function FilterLoginCredentials(string &$sEmail, string &$sImapUser, string &$sPassword, string &$sSmtpUser)
|
||||||
{
|
{
|
||||||
// cPanel https://github.com/the-djmaze/snappymail/issues/697
|
// cPanel https://github.com/the-djmaze/snappymail/issues/697
|
||||||
// && !empty($_ENV['CPANEL'])
|
// && !empty($_ENV['CPANEL'])
|
||||||
|
|
|
||||||
|
|
@ -6,9 +6,9 @@ class LoginVirtuserPlugin extends \RainLoop\Plugins\AbstractPlugin
|
||||||
NAME = 'Virtuser Login',
|
NAME = 'Virtuser Login',
|
||||||
AUTHOR = 'v20z',
|
AUTHOR = 'v20z',
|
||||||
URL = 'https://github.com/the-djmaze/snappymail/issues/1691',
|
URL = 'https://github.com/the-djmaze/snappymail/issues/1691',
|
||||||
VERSION = '0.1',
|
VERSION = '0.2',
|
||||||
RELEASE = '2024-06-01',
|
RELEASE = '2024-09-20',
|
||||||
REQUIRED = '2.5.0',
|
REQUIRED = '2.36.1',
|
||||||
CATEGORY = 'Login',
|
CATEGORY = 'Login',
|
||||||
DESCRIPTION = 'File based Email-to-User lookup.';
|
DESCRIPTION = 'File based Email-to-User lookup.';
|
||||||
|
|
||||||
|
|
@ -19,12 +19,13 @@ class LoginVirtuserPlugin extends \RainLoop\Plugins\AbstractPlugin
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param string $sEmail
|
* @param string $sEmail
|
||||||
* @param string $sLogin
|
* @param string $sImapUser
|
||||||
* @param string $sPassword
|
* @param string $sPassword
|
||||||
|
* @param string $sSmtpUser
|
||||||
*
|
*
|
||||||
* @throws \RainLoop\Exceptions\ClientException
|
* @throws \RainLoop\Exceptions\ClientException
|
||||||
*/
|
*/
|
||||||
public function ParseVirtuserFiles(&$sEmail, &$sLogin, &$sPassword)
|
public function ParseVirtuserFiles(string &$sEmail, string &$sImapUser, string &$sPassword, string &$sSmtpUser)
|
||||||
{
|
{
|
||||||
$sFiles = \trim($this->Config()->Get('plugin', 'virtuser_files', ''));
|
$sFiles = \trim($this->Config()->Get('plugin', 'virtuser_files', ''));
|
||||||
if (!empty($sFiles))
|
if (!empty($sFiles))
|
||||||
|
|
@ -46,7 +47,7 @@ class LoginVirtuserPlugin extends \RainLoop\Plugins\AbstractPlugin
|
||||||
$aData = preg_split( '/[[:blank:]]+/', $sLine, 3, PREG_SPLIT_NO_EMPTY);
|
$aData = preg_split( '/[[:blank:]]+/', $sLine, 3, PREG_SPLIT_NO_EMPTY);
|
||||||
if (is_array($aData) && !empty($aData[0]) && isset($aData[1])) {
|
if (is_array($aData) && !empty($aData[0]) && isset($aData[1])) {
|
||||||
if ($sEmail === $aData[0] && 0 < strlen($aData[1])) {
|
if ($sEmail === $aData[0] && 0 < strlen($aData[1])) {
|
||||||
$sLogin = $aData[1];
|
$sImapUser = $aData[1];
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,11 +4,11 @@ class NextcloudPlugin extends \RainLoop\Plugins\AbstractPlugin
|
||||||
{
|
{
|
||||||
const
|
const
|
||||||
NAME = 'Nextcloud',
|
NAME = 'Nextcloud',
|
||||||
VERSION = '2.37.0',
|
VERSION = '2.38.0',
|
||||||
RELEASE = '2024-08-11',
|
RELEASE = '2024-09-16',
|
||||||
CATEGORY = 'Integrations',
|
CATEGORY = 'Integrations',
|
||||||
DESCRIPTION = 'Integrate with Nextcloud v20+',
|
DESCRIPTION = 'Integrate with Nextcloud v20+',
|
||||||
REQUIRED = '2.36.2';
|
REQUIRED = '2.38.0';
|
||||||
|
|
||||||
private const IGNORE_SYSTEM_ADDRESSBOOK_KEY = 'ignoreSystemAddressbook';
|
private const IGNORE_SYSTEM_ADDRESSBOOK_KEY = 'ignoreSystemAddressbook';
|
||||||
private const IGNORE_SYSTEM_ADDRESSBOOK_DEFAULT_VALUE = true;
|
private const IGNORE_SYSTEM_ADDRESSBOOK_DEFAULT_VALUE = true;
|
||||||
|
|
@ -55,6 +55,7 @@ class NextcloudPlugin extends \RainLoop\Plugins\AbstractPlugin
|
||||||
|
|
||||||
$this->addTemplate('templates/PopupsNextcloudFiles.html');
|
$this->addTemplate('templates/PopupsNextcloudFiles.html');
|
||||||
$this->addTemplate('templates/PopupsNextcloudCalendars.html');
|
$this->addTemplate('templates/PopupsNextcloudCalendars.html');
|
||||||
|
$this->addTemplate('templates/PopupsNextcloudInvites.html');
|
||||||
|
|
||||||
// $this->addHook('login.credentials.step-2', 'loginCredentials2');
|
// $this->addHook('login.credentials.step-2', 'loginCredentials2');
|
||||||
// $this->addHook('login.credentials', 'loginCredentials');
|
// $this->addHook('login.credentials', 'loginCredentials');
|
||||||
|
|
@ -163,32 +164,17 @@ class NextcloudPlugin extends \RainLoop\Plugins\AbstractPlugin
|
||||||
|
|
||||||
public function beforeLogin(\RainLoop\Model\Account $oAccount, \MailSo\Net\NetClient $oClient, \MailSo\Net\ConnectSettings $oSettings) : void
|
public function beforeLogin(\RainLoop\Model\Account $oAccount, \MailSo\Net\NetClient $oClient, \MailSo\Net\ConnectSettings $oSettings) : void
|
||||||
{
|
{
|
||||||
// https://apps.nextcloud.com/apps/oidc_login
|
|
||||||
$config = \OC::$server->getConfig();
|
|
||||||
$oUser = \OC::$server->getUserSession()->getUser();
|
|
||||||
$sUID = $oUser->getUID();
|
|
||||||
|
|
||||||
$sEmail = $config->getUserValue($sUID, 'snappymail', 'snappymail-email');
|
|
||||||
$sPassword = $config->getUserValue($sUID, 'snappymail', 'passphrase')
|
|
||||||
?: $config->getUserValue($sUID, 'snappymail', 'snappymail-password');
|
|
||||||
$bAccountDefinedExplicitly = ($sEmail && $sPassword) && $sEmail === $oSettings->username;
|
|
||||||
|
|
||||||
$sNcEmail = $oUser->getEMailAddress() ?: $oUser->getPrimaryEMailAddress();
|
|
||||||
|
|
||||||
// Only login with OIDC access token if
|
// Only login with OIDC access token if
|
||||||
// it is enabled in config, the user is currently logged in with OIDC,
|
// it is enabled in config, the user is currently logged in with OIDC,
|
||||||
// the current snappymail account is the OIDC account and no account defined explicitly
|
// the current snappymail account is the OIDC account and no account defined explicitly
|
||||||
if (\OC::$server->getConfig()->getAppValue('snappymail', 'snappymail-autologin-oidc', false)
|
if ($oAccount instanceof \RainLoop\Model\MainAccount
|
||||||
&& \OC::$server->getSession()->get('is_oidc')
|
&& \OCA\SnappyMail\Util\SnappyMailHelper::isOIDCLogin()
|
||||||
&& $sNcEmail === $oSettings->username
|
|
||||||
&& !$bAccountDefinedExplicitly
|
|
||||||
// && $oClient->supportsAuthType('OAUTHBEARER') // v2.28
|
// && $oClient->supportsAuthType('OAUTHBEARER') // v2.28
|
||||||
|
&& \str_starts_with($oSettings->passphrase, 'oidc_login|')
|
||||||
) {
|
) {
|
||||||
$sAccessToken = \OC::$server->getSession()->get('oidc_access_token');
|
// $oSettings->passphrase = \OC::$server->getSession()->get('snappymail-passphrase');
|
||||||
if ($sAccessToken) {
|
$oSettings->passphrase = \OC::$server->getSession()->get('oidc_access_token');
|
||||||
$oSettings->passphrase = $sAccessToken;
|
\array_unshift($oSettings->SASLMechanisms, 'OAUTHBEARER');
|
||||||
\array_unshift($oSettings->SASLMechanisms, 'OAUTHBEARER');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,9 +21,18 @@
|
||||||
|
|
||||||
// https://github.com/nextcloud/calendar/issues/4684
|
// https://github.com/nextcloud/calendar/issues/4684
|
||||||
if (cfg.CalDAV) {
|
if (cfg.CalDAV) {
|
||||||
attachmentsControls.append(Element.fromHTML(`<span data-bind="visible: nextcloudICS" data-icon="📅">
|
attachmentsControls.append(Element.fromHTML(`<span data-bind="visible: nextcloudICSShow" data-icon="📅">
|
||||||
<span class="g-ui-link" data-bind="click: nextcloudSaveICS" data-i18n="NEXTCLOUD/SAVE_ICS"></span>
|
<span class="g-ui-link" data-bind="click: nextcloudSaveICS" data-i18n="NEXTCLOUD/SAVE_ICS"></span>
|
||||||
</span>`));
|
</span>`));
|
||||||
|
attachmentsControls.append(Element.fromHTML(`<span data-bind="visible: nextcloudICSOldInvitation" data-icon="📅">
|
||||||
|
<span data-i18n="NEXTCLOUD/OLD_INVITATION"></span>
|
||||||
|
</span>`))
|
||||||
|
attachmentsControls.append(Element.fromHTML(`<span data-bind="visible: nextcloudICSNewInvitation" data-icon="📅">
|
||||||
|
<span class="g-ui-link" data-bind="click: nextcloudSaveICS" data-i18n="NEXTCLOUD/UPDATE_ON_MY_CALENDAR"></span>
|
||||||
|
</span>`))
|
||||||
|
attachmentsControls.append(Element.fromHTML(`<span data-bind="visible: nextcloudICSLastInvitation" data-icon="📅">
|
||||||
|
<span data-i18n="NEXTCLOUD/LAST_INVITATION"></span>
|
||||||
|
</span>`))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/*
|
/*
|
||||||
|
|
@ -107,18 +116,169 @@
|
||||||
};
|
};
|
||||||
|
|
||||||
view.nextcloudICS = ko.observable(null);
|
view.nextcloudICS = ko.observable(null);
|
||||||
|
view.nextcloudICSOldInvitation = ko.observable(null);
|
||||||
|
view.nextcloudICSNewInvitation = ko.observable(null);
|
||||||
|
view.nextcloudICSLastInvitation = ko.observable(null);
|
||||||
|
|
||||||
view.nextcloudSaveICS = () => {
|
view.nextcloudICSShow = ko.observable(null);
|
||||||
|
|
||||||
|
view.nextcloudICSCalendar = ko.observable(null);
|
||||||
|
view.filteredEventsUrls = ko.observable([]);
|
||||||
|
|
||||||
|
view.nextcloudSaveICS = async () => {
|
||||||
let VEVENT = view.nextcloudICS();
|
let VEVENT = view.nextcloudICS();
|
||||||
VEVENT && rl.nextcloud.selectCalendar()
|
VEVENT = await view.handleUpdatedRecurrentEvents(VEVENT)
|
||||||
.then(href => href && rl.nextcloud.calendarPut(href, VEVENT));
|
VEVENT && rl.nextcloud.selectCalendar(VEVENT)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
view.handleUpdatedRecurrentEvents = async (VEVENT) => {
|
||||||
|
|
||||||
|
const uid = VEVENT.UID
|
||||||
|
|
||||||
|
let makePUTRequest = false
|
||||||
|
|
||||||
|
const filteredEventUrls = view.filteredEventUrls
|
||||||
|
|
||||||
|
if (filteredEventUrls.length > 1) {
|
||||||
|
// console.warn('filteredEventUrls.length > 1')
|
||||||
|
}
|
||||||
|
else if (filteredEventUrls.length == 1) {
|
||||||
|
const eventUrl = filteredEventUrls[0]
|
||||||
|
const eventText = await fetchEvent(eventUrl)
|
||||||
|
|
||||||
|
// don't do anything for equal cards
|
||||||
|
if (VEVENT.rawText == eventText) {
|
||||||
|
return VEVENT
|
||||||
|
}
|
||||||
|
|
||||||
|
const newVeventsText = extractVEVENTs(VEVENT.rawText)
|
||||||
|
const oldVeventsText = extractVEVENTs(eventText)
|
||||||
|
|
||||||
|
// if there's only one event in the old card, it can't be an edit of the exception card
|
||||||
|
if (oldVeventsText.length == 1) {
|
||||||
|
const newRecurrenceId = extractProperties('RECURRENCE-ID', newVeventsText[0])
|
||||||
|
|
||||||
|
// if there's a property RECURRENCE-ID in the new card, it's an recurrence exception event
|
||||||
|
if (newRecurrenceId.length > 0) {
|
||||||
|
const updatedVEVENT = {
|
||||||
|
'SUMMARY' : VEVENT.SUMMARY,
|
||||||
|
'UID' : VEVENT.UID,
|
||||||
|
'rawText' : mergeEventTexts(eventText, newVeventsText[0])
|
||||||
|
}
|
||||||
|
VEVENT = updatedVEVENT
|
||||||
|
makePUTRequest = true
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return VEVENT
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// if there's more than one event in the old card, it's possible to be an inclusion of a new exception card
|
||||||
|
// or update of old exception card
|
||||||
|
else {
|
||||||
|
let recurrenceIdMatch = false
|
||||||
|
|
||||||
|
let isUpdate = false
|
||||||
|
const updateData = {
|
||||||
|
'oldEventIndex': null,
|
||||||
|
'newEventIndex': null,
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if it's an update
|
||||||
|
for (let i = 0; i < oldVeventsText.length; i++) {
|
||||||
|
let oldVeventText = oldVeventsText[i]
|
||||||
|
|
||||||
|
for (let j = 0; j < newVeventsText.length; j++) {
|
||||||
|
let newVeventText = newVeventsText[j]
|
||||||
|
|
||||||
|
let oldRecurrenceId = extractProperties('RECURRENCE-ID', oldVeventText)
|
||||||
|
let newRecurrenceId = extractProperties('RECURRENCE-ID', newVeventText)
|
||||||
|
let oldSequence = extractProperties('SEQUENCE', oldVeventText)
|
||||||
|
let newSequence = extractProperties('SEQUENCE', newVeventText)
|
||||||
|
|
||||||
|
if (oldRecurrenceId.length == 0 || newRecurrenceId.length == 0 || oldSequence.length == 0 || newSequence.length == 0) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (oldRecurrenceId[0] == newRecurrenceId[0]) {
|
||||||
|
if (newSequence[0] > oldSequence[0]) {
|
||||||
|
isUpdate = true
|
||||||
|
|
||||||
|
updateData.oldEventIndex = i
|
||||||
|
updateData.newEventIndex = j
|
||||||
|
|
||||||
|
i = oldVeventsText.length
|
||||||
|
j = newVeventsText.length
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// if it's an update...
|
||||||
|
if (isUpdate) {
|
||||||
|
// substitute old event text for new event text
|
||||||
|
const oldEventStart = eventText.indexOf(oldVeventsText[updateData.oldEventIndex])
|
||||||
|
const oldEventEnd = oldEventStart + oldVeventsText[updateData.oldEventIndex].length
|
||||||
|
|
||||||
|
const newEvent = eventText.substring(0, oldEventStart) + newVeventsText[updateData.newEventIndex] + eventText.substring(oldEventEnd)
|
||||||
|
|
||||||
|
const updatedVEVENT = {
|
||||||
|
'SUMMARY' : VEVENT.SUMMARY,
|
||||||
|
'UID': VEVENT.UID,
|
||||||
|
'rawText' : newEvent
|
||||||
|
}
|
||||||
|
VEVENT = updatedVEVENT
|
||||||
|
makePUTRequest = true
|
||||||
|
}
|
||||||
|
// if it's not an update, it's an inclusion, as there's no match of RECURRENCE-ID
|
||||||
|
else {
|
||||||
|
const updatedVEVENT = {
|
||||||
|
'SUMMARY' : VEVENT.SUMMARY,
|
||||||
|
'UID' : VEVENT.UID,
|
||||||
|
'rawText' : mergeEventTexts(eventText, newVeventsText[0])
|
||||||
|
}
|
||||||
|
VEVENT = updatedVEVENT
|
||||||
|
makePUTRequest = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (makePUTRequest) {
|
||||||
|
let href = "/" + (filteredEventUrls[0].split('/').slice(5, -1)[0])
|
||||||
|
|
||||||
|
rl.nextcloud.calendarPut(href, VEVENT, (response) => {
|
||||||
|
if (response.status != 201 && response.status != 204) {
|
||||||
|
InvitesPopupView.showModal([
|
||||||
|
rl.i18n('NEXTCLOUD/EVENT_UPDATE_FAILURE_TITLE'),
|
||||||
|
rl.i18n('NEXTCLOUD/EVENT_UPDATE_FAILURE_BODY', {eventName: VEVENT.SUMMARY})
|
||||||
|
])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
InvitesPopupView.showModal([
|
||||||
|
rl.i18n('NEXTCLOUD/EVENT_UPDATED_TITLE'),
|
||||||
|
rl.i18n('NEXTCLOUD/EVENT_UPDATED_BODY', {eventName: VEVENT.SUMMARY})
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return VEVENT
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TODO
|
* TODO
|
||||||
*/
|
*/
|
||||||
view.message.subscribe(msg => {
|
view.message.subscribe(msg => {
|
||||||
view.nextcloudICS(null);
|
view.nextcloudICS(null);
|
||||||
|
view.nextcloudICSOldInvitation(null);
|
||||||
|
view.nextcloudICSNewInvitation(null);
|
||||||
|
view.nextcloudICSLastInvitation(null);
|
||||||
|
view.nextcloudICSShow(view.nextcloudICS())
|
||||||
|
|
||||||
|
|
||||||
if (msg && cfg.CalDAV) {
|
if (msg && cfg.CalDAV) {
|
||||||
// let ics = msg.attachments.find(attachment => 'application/ics' == attachment.mimeType);
|
// let ics = msg.attachments.find(attachment => 'application/ics' == attachment.mimeType);
|
||||||
let ics = msg.attachments.find(attachment => 'text/calendar' == attachment.mimeType);
|
let ics = msg.attachments.find(attachment => 'text/calendar' == attachment.mimeType);
|
||||||
|
|
@ -126,7 +286,7 @@
|
||||||
// fetch it and parse the VEVENT
|
// fetch it and parse the VEVENT
|
||||||
rl.fetch(ics.linkDownload())
|
rl.fetch(ics.linkDownload())
|
||||||
.then(response => (response.status < 400) ? response.text() : Promise.reject(new Error({ response })))
|
.then(response => (response.status < 400) ? response.text() : Promise.reject(new Error({ response })))
|
||||||
.then(text => {
|
.then(async (text) => {
|
||||||
let VEVENT,
|
let VEVENT,
|
||||||
VALARM,
|
VALARM,
|
||||||
multiple = ['ATTACH','ATTENDEE','CATEGORIES','COMMENT','CONTACT','EXDATE',
|
multiple = ['ATTACH','ATTENDEE','CATEGORIES','COMMENT','CONTACT','EXDATE',
|
||||||
|
|
@ -177,6 +337,158 @@
|
||||||
shouldReply: VEVENT.shouldReply()
|
shouldReply: VEVENT.shouldReply()
|
||||||
});
|
});
|
||||||
view.nextcloudICS(VEVENT);
|
view.nextcloudICS(VEVENT);
|
||||||
|
view.nextcloudICSShow(true);
|
||||||
|
|
||||||
|
|
||||||
|
// try to get calendars, save
|
||||||
|
const calendarUrls = await fetchCalendarUrls()
|
||||||
|
|
||||||
|
const filteredEventUrls = []
|
||||||
|
for (let i = 0; i < calendarUrls.length; i++) {
|
||||||
|
let calendarUrl = calendarUrls[i]
|
||||||
|
|
||||||
|
const skipCalendars = ['/inbox/', '/outbox/', '/trashbin/']
|
||||||
|
let skip = false
|
||||||
|
for (let j = 0; j < skipCalendars.length; j++) {
|
||||||
|
if (calendarUrl.includes(skipCalendars[j])) {
|
||||||
|
skip = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (skip) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// try to get event
|
||||||
|
const eventUrls = await fetchEventUrl(calendarUrl, VEVENT.UID)
|
||||||
|
|
||||||
|
if (eventUrls.length == 0) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
eventUrls.forEach((url) => {
|
||||||
|
filteredEventUrls.push(url)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
view.filteredEventUrls = filteredEventUrls
|
||||||
|
|
||||||
|
// if there's none, save in view.nextcloudICS
|
||||||
|
if (filteredEventUrls.length == 0) {
|
||||||
|
view.nextcloudICS(VEVENT);
|
||||||
|
view.nextcloudICSShow(true)
|
||||||
|
}
|
||||||
|
// if there's some...
|
||||||
|
else {
|
||||||
|
const savedEvent = await fetchEvent(filteredEventUrls[0])
|
||||||
|
|
||||||
|
const newVeventsText = extractVEVENTs(VEVENT.rawText)
|
||||||
|
const oldVeventsText = extractVEVENTs(savedEvent)
|
||||||
|
|
||||||
|
// if there's more than one event in the old card, it's possible to be inclusion of new exception card
|
||||||
|
// or updated of old exception card
|
||||||
|
if (oldVeventsText.length > 1) {
|
||||||
|
let recurrenceIdMatch = false
|
||||||
|
|
||||||
|
let oldNewestCreated = null
|
||||||
|
let newNewestCreated = null
|
||||||
|
|
||||||
|
// check if it's an update
|
||||||
|
for (let i = 0; i < oldVeventsText.length; i++) {
|
||||||
|
let oldVeventText = oldVeventsText[i]
|
||||||
|
|
||||||
|
for (let j = 0; j < newVeventsText.length; j++) {
|
||||||
|
let newVeventText = newVeventsText[j]
|
||||||
|
|
||||||
|
let oldRecurrenceId = extractProperties('RECURRENCE-ID', oldVeventText)
|
||||||
|
let newRecurrenceId = extractProperties('RECURRENCE-ID', newVeventText)
|
||||||
|
let oldSequence = extractProperties('SEQUENCE', oldVeventText)
|
||||||
|
let newSequence = extractProperties('SEQUENCE', newVeventText)
|
||||||
|
|
||||||
|
if (newRecurrenceId.length == 0 && oldRecurrenceId.length == 1) {
|
||||||
|
view.nextcloudICSShow(false)
|
||||||
|
view.nextcloudICSOldInvitation(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (oldRecurrenceId.length > 0 && newRecurrenceId.length > 0 && oldSequence.length > 0 && newSequence.length > 0) {
|
||||||
|
if (oldRecurrenceId[0] == newRecurrenceId[0]) {
|
||||||
|
if (newSequence[0] < oldSequence[0]) {
|
||||||
|
view.nextcloudICSOldInvitation(true)
|
||||||
|
view.nextcloudICSShow(false)
|
||||||
|
}
|
||||||
|
else if (newSequence[0] == oldSequence[0]) {
|
||||||
|
view.nextcloudICSLastInvitation(true)
|
||||||
|
view.nextcloudICSShow(false)
|
||||||
|
}
|
||||||
|
else if (newSequence[0] > oldSequence[0]) {
|
||||||
|
view.nextcloudICSNewInvitation(true)
|
||||||
|
view.nextcloudICSShow(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// exit for loops
|
||||||
|
j = newVeventsText.length
|
||||||
|
i = oldVeventsText.length
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let oldCreated = extractProperties('CREATED', oldVeventText)
|
||||||
|
let newCreated = extractProperties('CREATED', newVeventText)
|
||||||
|
|
||||||
|
if (oldCreated.length == 0 || newCreated.length == 0) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const formattedOldDate = oldCreated[0].replace(/(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})Z/, '$1-$2-$3T$4:$5:$6Z');
|
||||||
|
const formattedNewDate = newCreated[0].replace(/(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})Z/, '$1-$2-$3T$4:$5:$6Z');
|
||||||
|
|
||||||
|
const oldDate = new Date(formattedOldDate)
|
||||||
|
const newDate = new Date(formattedNewDate)
|
||||||
|
|
||||||
|
if (oldNewestCreated == null || oldDate > oldNewestCreated) {
|
||||||
|
oldNewestCreated = oldDate
|
||||||
|
}
|
||||||
|
if (newNewestCreated == null || newDate > newNewestCreated) {
|
||||||
|
newNewestCreated = newDate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newNewestCreated != null && oldNewestCreated != null) {
|
||||||
|
if (newNewestCreated < oldNewestCreated) {
|
||||||
|
view.nextcloudICSOldInvitation(true)
|
||||||
|
view.nextcloudICSShow(false)
|
||||||
|
}
|
||||||
|
else if (newNewestCreated == oldNewestCreated) {
|
||||||
|
view.nextcloudICSLastInvitation(true)
|
||||||
|
view.nextcloudICSShow(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
const oldLastModified = extractProperties('LAST-MODIFIED', oldVeventsText[0])
|
||||||
|
const newLastModified = extractProperties('LAST-MODIFIED', newVeventsText[0])
|
||||||
|
|
||||||
|
if (oldLastModified.length == 1 && newLastModified.length == 1) {
|
||||||
|
const formattedOldDate = oldLastModified[0].replace(/(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})Z/, '$1-$2-$3T$4:$5:$6Z');
|
||||||
|
const formattedNewDate = newLastModified[0].replace(/(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})Z/, '$1-$2-$3T$4:$5:$6Z');
|
||||||
|
|
||||||
|
const oldDate = new Date(formattedOldDate)
|
||||||
|
const newDate = new Date(formattedNewDate)
|
||||||
|
|
||||||
|
if (newDate > oldDate) {
|
||||||
|
view.nextcloudICSNewInvitation(true)
|
||||||
|
view.nextcloudICSShow(false)
|
||||||
|
}
|
||||||
|
else if (newDate < oldDate) {
|
||||||
|
view.nextcloudICSOldInvitation(true)
|
||||||
|
view.nextcloudICSShow(false)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
view.nextcloudICSLastInvitation(true)
|
||||||
|
view.nextcloudICSShow(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -186,3 +498,148 @@
|
||||||
});
|
});
|
||||||
|
|
||||||
})(window.rl);
|
})(window.rl);
|
||||||
|
|
||||||
|
|
||||||
|
async function fetchCalendarUrls () {
|
||||||
|
const username = OC.currentUser
|
||||||
|
const requestToken = OC.requestToken
|
||||||
|
|
||||||
|
const url = '/remote.php/dav/calendars/' + username
|
||||||
|
const response = await fetch(url, {
|
||||||
|
'method': 'PROPFIND',
|
||||||
|
'headers': {
|
||||||
|
'Depth': '1',
|
||||||
|
'Content-Type': 'application/xml',
|
||||||
|
'requesttoken' : requestToken
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Error fetching calendars', response)
|
||||||
|
}
|
||||||
|
|
||||||
|
const responseText = await response.text()
|
||||||
|
|
||||||
|
const parser = new DOMParser()
|
||||||
|
const xmlDoc = parser.parseFromString(responseText, 'application/xml')
|
||||||
|
|
||||||
|
const calendarUrls = []
|
||||||
|
|
||||||
|
const hrefElements = xmlDoc.getElementsByTagName('d:href')
|
||||||
|
for (let i = 1; i < hrefElements.length; i++) {
|
||||||
|
let calendarUrl = hrefElements[i].textContent.trim()
|
||||||
|
calendarUrls.push(calendarUrl)
|
||||||
|
}
|
||||||
|
|
||||||
|
return calendarUrls
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async function fetchEventUrl (calendarUrl, uid) {
|
||||||
|
const requestToken = OC.requestToken
|
||||||
|
|
||||||
|
const xmlRequestBody = `<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<C:calendar-query xmlns:C="urn:ietf:params:xml:ns:caldav">
|
||||||
|
<C:prop>
|
||||||
|
<C:calendar-data/>
|
||||||
|
</C:prop>
|
||||||
|
<C:filter>
|
||||||
|
<C:comp-filter name="VCALENDAR">
|
||||||
|
<C:comp-filter name="VEVENT">
|
||||||
|
<C:prop-filter name="UID">
|
||||||
|
<C:text-match collation="i;unicode-casemap" match-type="equals">${uid}</C:text-match>
|
||||||
|
</C:prop-filter>
|
||||||
|
</C:comp-filter>
|
||||||
|
</C:comp-filter>
|
||||||
|
</C:filter>
|
||||||
|
</C:calendar-query>`
|
||||||
|
|
||||||
|
const response = await fetch(calendarUrl, {
|
||||||
|
'method': 'REPORT',
|
||||||
|
'headers': {
|
||||||
|
'Content-Type': 'application/xml',
|
||||||
|
'Depth': 1,
|
||||||
|
'requesttoken': requestToken
|
||||||
|
},
|
||||||
|
'body': xmlRequestBody
|
||||||
|
})
|
||||||
|
|
||||||
|
const responseText = await response.text()
|
||||||
|
|
||||||
|
const parser = new DOMParser()
|
||||||
|
const xmlDoc = parser.parseFromString(responseText, 'application/xml')
|
||||||
|
|
||||||
|
const hrefElements = xmlDoc.getElementsByTagName('d:href')
|
||||||
|
const hrefValues = Array.from(hrefElements).map(element => element.textContent)
|
||||||
|
|
||||||
|
return hrefValues
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async function fetchEvent (eventUrl) {
|
||||||
|
const requestToken = OC.requestToken
|
||||||
|
const response = await fetch(eventUrl, {
|
||||||
|
'method': 'GET',
|
||||||
|
'headers': {
|
||||||
|
'requesttoken': requestToken
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const responseText = await response.text()
|
||||||
|
|
||||||
|
return responseText
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function extractVEVENTs(text) {
|
||||||
|
let lastEndIndex = 0
|
||||||
|
const vEvents = []
|
||||||
|
|
||||||
|
let textToSearch = ""
|
||||||
|
let beginIndex = 0
|
||||||
|
let endIndex = 0
|
||||||
|
let foundVevent = false
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
textToSearch = text.substring(lastEndIndex)
|
||||||
|
|
||||||
|
beginIndex = textToSearch.indexOf('BEGIN:VEVENT')
|
||||||
|
if (beginIndex == -1) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
endIndex = textToSearch.substring(beginIndex).indexOf('END:VEVENT')
|
||||||
|
if (endIndex == -1) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
endIndex += beginIndex
|
||||||
|
|
||||||
|
lastEndIndex = lastEndIndex + endIndex + 'END:VEVENT'.length
|
||||||
|
|
||||||
|
foundVevent = textToSearch.substring(beginIndex + 'BEGIN:VEVENT'.length, endIndex)
|
||||||
|
|
||||||
|
vEvents.push(foundVevent)
|
||||||
|
}
|
||||||
|
|
||||||
|
return vEvents
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function extractProperties (property, text) {
|
||||||
|
const matches = text.match(`${property}.*`)
|
||||||
|
|
||||||
|
if (matches == null) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
const separatedMatches = matches.map((match) => {
|
||||||
|
return match.substring(property.length + 1)
|
||||||
|
})
|
||||||
|
|
||||||
|
return separatedMatches
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function mergeEventTexts (oldEventText, newEventText) {
|
||||||
|
const appendIndex = oldEventText.indexOf('END:VEVENT') + 'END:VEVENT'.length
|
||||||
|
const updatedEventText = oldEventText.substring(0, appendIndex) + "\nBEGIN:VEVENT" + newEventText + "END:VEVENT" + oldEventText.substring(appendIndex)
|
||||||
|
return updatedEventText
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -320,12 +320,28 @@ class NextcloudCalendarsPopupView extends rl.pluginPopupView {
|
||||||
}
|
}
|
||||||
|
|
||||||
onBuild(dom) {
|
onBuild(dom) {
|
||||||
|
let modalObj = this
|
||||||
this.tree = dom.querySelector('#sm-nc-calendars');
|
this.tree = dom.querySelector('#sm-nc-calendars');
|
||||||
this.tree.addEventListener('click', event => {
|
this.tree.addEventListener('click', event => {
|
||||||
let el = event.target;
|
let el = event.target;
|
||||||
if (el.matches('button')) {
|
if (el.matches('button')) {
|
||||||
this.select = el.href;
|
this.select = el.href;
|
||||||
this.close();
|
let VEVENT = this.VEVENT
|
||||||
|
|
||||||
|
rl.nextcloud.calendarPut(this.select, this.VEVENT, (response) => {
|
||||||
|
if (response.status != 201 && response.status != 204) {
|
||||||
|
InvitesPopupView.showModal([
|
||||||
|
rl.i18n('MESSAGE/EVENT_ADDITION_FAILURE_TITLE'),
|
||||||
|
rl.i18n('MESSAGE/EVENT_ADDITION_FAILURE_BODY', {eventName: VEVENT.SUMMARY}),
|
||||||
|
() => {modalObj.close()}
|
||||||
|
])
|
||||||
|
}
|
||||||
|
InvitesPopupView.showModal([
|
||||||
|
rl.i18n('MESSAGE/EVENT_ADDED_TITLE'),
|
||||||
|
rl.i18n('MESSAGE/EVENT_ADDED_BODY', {eventName: VEVENT.SUMMARY}),
|
||||||
|
() => {modalObj.close()}
|
||||||
|
])
|
||||||
|
})
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -375,9 +391,10 @@ class NextcloudCalendarsPopupView extends rl.pluginPopupView {
|
||||||
treeElement.appendChild(li);
|
treeElement.appendChild(li);
|
||||||
}
|
}
|
||||||
// Happens after showModal()
|
// Happens after showModal()
|
||||||
beforeShow(fResolve) {
|
beforeShow(fResolve, VEVENT) {
|
||||||
this.select = '';
|
this.select = '';
|
||||||
this.fResolve = fResolve;
|
this.fResolve = fResolve;
|
||||||
|
this.VEVENT = VEVENT
|
||||||
this.tree.innerHTML = '';
|
this.tree.innerHTML = '';
|
||||||
davFetch('calendars', '/', {
|
davFetch('calendars', '/', {
|
||||||
method: 'PROPFIND',
|
method: 'PROPFIND',
|
||||||
|
|
@ -432,14 +449,15 @@ close() {}
|
||||||
}
|
}
|
||||||
|
|
||||||
rl.nextcloud = {
|
rl.nextcloud = {
|
||||||
selectCalendar: () =>
|
selectCalendar: (VEVENT) =>
|
||||||
new Promise(resolve => {
|
new Promise(resolve => {
|
||||||
NextcloudCalendarsPopupView.showModal([
|
NextcloudCalendarsPopupView.showModal([
|
||||||
href => resolve(href),
|
href => resolve(href),
|
||||||
|
VEVENT
|
||||||
]);
|
]);
|
||||||
}),
|
}),
|
||||||
|
|
||||||
calendarPut: (path, event) => {
|
calendarPut: (path, event, callback) => {
|
||||||
davFetch('calendars', path + '/' + event.UID + '.ics', {
|
davFetch('calendars', path + '/' + event.UID + '.ics', {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: {
|
headers: {
|
||||||
|
|
@ -463,6 +481,8 @@ rl.nextcloud = {
|
||||||
// response.text().then(text => console.error({status:response.status, body:text}));
|
// response.text().then(text => console.error({status:response.status, body:text}));
|
||||||
Promise.reject(new Error({ response }));
|
Promise.reject(new Error({ response }));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
callback && callback(response)
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -500,3 +520,27 @@ function getElementsInNamespaces(xmlDocument, tagName) {
|
||||||
}
|
}
|
||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class InvitesPopupView extends rl.pluginPopupView {
|
||||||
|
constructor() {
|
||||||
|
super('NextcloudInvites')
|
||||||
|
}
|
||||||
|
|
||||||
|
onBuild(dom) {
|
||||||
|
this.title = dom.querySelector('#sm-invites-popup-title')
|
||||||
|
this.body = dom.querySelector('#sm-invites-popup-body')
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeShow(title, body, onHide_) {
|
||||||
|
this.title.innerHTML = title
|
||||||
|
this.body.innerHTML = body
|
||||||
|
this.onHide_ = onHide_
|
||||||
|
}
|
||||||
|
|
||||||
|
onHide() {
|
||||||
|
if (this.onHide_) {
|
||||||
|
this.onHide_()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,14 +2,21 @@
|
||||||
"NEXTCLOUD": {
|
"NEXTCLOUD": {
|
||||||
"SAVE_ATTACHMENTS": "Save in Nextcloud",
|
"SAVE_ATTACHMENTS": "Save in Nextcloud",
|
||||||
"SAVE_EML": "Save as .eml in Nextcloud",
|
"SAVE_EML": "Save as .eml in Nextcloud",
|
||||||
"SAVE_ICS": "Add to calendar",
|
|
||||||
"SELECT_FOLDER": "Select folder",
|
"SELECT_FOLDER": "Select folder",
|
||||||
"SELECT_FILES": "Select file(s)",
|
"SELECT_FILES": "Select file(s)",
|
||||||
"ATTACH_FILES": "Attach Nextcloud files",
|
"ATTACH_FILES": "Attach Nextcloud files",
|
||||||
"SELECT_CALENDAR": "Select calendar",
|
|
||||||
"FILE_ATTACH": "attach",
|
"FILE_ATTACH": "attach",
|
||||||
"FILE_INTERNAL": "internal",
|
"FILE_INTERNAL": "internal",
|
||||||
"FILE_PUBLIC": "public",
|
"FILE_PUBLIC": "public",
|
||||||
"ADDRESS_BOOK": "Address Book"
|
"ADDRESS_BOOK": "Address Book",
|
||||||
|
"SELECT_CALENDAR": "Select calendar",
|
||||||
|
"SAVE_ICS": "Add to calendar",
|
||||||
|
"OLD_INVITATION": "Old invitation",
|
||||||
|
"UPDATE_ON_MY_CALENDAR": "Update invitation",
|
||||||
|
"LAST_INVITATION": "Last invitation",
|
||||||
|
"EVENT_UPDATE_FAILURE_TITLE": "Update failed",
|
||||||
|
"EVENT_UPDATE_FAILURE_BODY": "Why, i have no clue",
|
||||||
|
"EVENT_UPDATED_TITLE": "Updated",
|
||||||
|
"EVENT_UPDATED_BODY": "Why, i have no clue"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
8
plugins/nextcloud/templates/PopupsNextcloudInvites.html
Normal file
8
plugins/nextcloud/templates/PopupsNextcloudInvites.html
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
<header>
|
||||||
|
<a class="close" href="#" data-bind="click: close">×</a>
|
||||||
|
<h3 id="sm-invites-popup-title"></h3>
|
||||||
|
</header>
|
||||||
|
<div class="modal-body form-horizontal" id="sm-invites-popup-body">
|
||||||
|
</div>
|
||||||
|
<footer>
|
||||||
|
</footer>
|
||||||
|
|
@ -6,8 +6,8 @@ class ProxyAuthPlugin extends \RainLoop\Plugins\AbstractPlugin
|
||||||
NAME = 'Proxy Auth',
|
NAME = 'Proxy Auth',
|
||||||
AUTHOR = 'Philipp',
|
AUTHOR = 'Philipp',
|
||||||
URL = 'https://www.mundhenk.org/',
|
URL = 'https://www.mundhenk.org/',
|
||||||
VERSION = '0.4',
|
VERSION = '0.5',
|
||||||
RELEASE = '2024-05-26',
|
RELEASE = '2024-09-20',
|
||||||
REQUIRED = '2.36.1',
|
REQUIRED = '2.36.1',
|
||||||
CATEGORY = 'Login',
|
CATEGORY = 'Login',
|
||||||
LICENSE = 'MIT',
|
LICENSE = 'MIT',
|
||||||
|
|
@ -41,7 +41,7 @@ class ProxyAuthPlugin extends \RainLoop\Plugins\AbstractPlugin
|
||||||
return ( ( $ip_decimal & $netmask_decimal ) == ( $range_decimal & $netmask_decimal ) );
|
return ( ( $ip_decimal & $netmask_decimal ) == ( $range_decimal & $netmask_decimal ) );
|
||||||
}
|
}
|
||||||
|
|
||||||
public function MapEmailAddress(string &$sEmail, string &$sLogin, string &$sPassword)
|
public function MapEmailAddress(string &$sEmail, string &$sImapUser, string &$sPassword, string &$sSmtpUser)
|
||||||
{
|
{
|
||||||
$oActions = \RainLoop\Api::Actions();
|
$oActions = \RainLoop\Api::Actions();
|
||||||
$oLogger = $oActions->Logger();
|
$oLogger = $oActions->Logger();
|
||||||
|
|
|
||||||
869
plugins/view-ics/ical.js
Normal file
869
plugins/view-ics/ical.js
Normal file
|
|
@ -0,0 +1,869 @@
|
||||||
|
// https://www.rfc-editor.org/rfc/rfc5545
|
||||||
|
|
||||||
|
(rl => {
|
||||||
|
|
||||||
|
const
|
||||||
|
paramRegEx = /;([a-zA-Z0-9-]+)=([^";:,]*|"[^"]*")/g,
|
||||||
|
// eslint-disable-next-line max-len
|
||||||
|
dateRegEx = /(?<year>[0-9]{4})(?<month>[0-9]{2})(?<day>[0-9]{2})T(?<hour>[0-9]{2})(?<minute>[0-9]{2})(?<second>[0-9]{2})(?<utc>Z?)/,
|
||||||
|
parseDate = (str, tz) => {
|
||||||
|
let parts = dateRegEx.exec(str)?.groups,
|
||||||
|
options = {dateStyle: 'long', timeStyle: 'short'},
|
||||||
|
date = (parts ? new Date(
|
||||||
|
parseInt(parts.year, 10),
|
||||||
|
parseInt(parts.month, 10) - 1,
|
||||||
|
parseInt(parts.day, 10),
|
||||||
|
parseInt(parts.hour, 10),
|
||||||
|
parseInt(parts.minute, 10),
|
||||||
|
parseInt(parts.second, 10)
|
||||||
|
) : new Date(str));
|
||||||
|
tz && (options.timeZone = windowsVTIMEZONEs[tz] || tz);
|
||||||
|
try {
|
||||||
|
return date.format(options);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
if (options.timeZone) {
|
||||||
|
options.timeZone = undefined;
|
||||||
|
return date.format(options);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
class Property extends String
|
||||||
|
{
|
||||||
|
constructor(name, value, params) {
|
||||||
|
super(value);
|
||||||
|
this.name = name;
|
||||||
|
// this.value = value;
|
||||||
|
this.params = {};
|
||||||
|
if (params) {
|
||||||
|
for (const param of params.matchAll(paramRegEx)) {
|
||||||
|
if (param[2]) {
|
||||||
|
this.params[param[1]] = param[2].replace(/^"|"$/);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
includes(...params) {
|
||||||
|
return this.value.includes(...params);
|
||||||
|
}
|
||||||
|
replace(...params) {
|
||||||
|
return this.value.replace(...params);
|
||||||
|
}
|
||||||
|
toString() {
|
||||||
|
return this.value;
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
static fromString(data) {
|
||||||
|
// Parse component property 'name *(";" param ) ":" value'
|
||||||
|
const match = data.match(/^([a-zA-Z0-9-]+)((?:;[a-zA-Z0-9-]+=(?:[^";:,]*|"[^"]*"))*):(.+)$/);
|
||||||
|
return match && new Property(match[1], match[3], match[2]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rl.ICS = {
|
||||||
|
|
||||||
|
parseDate: parseDate,
|
||||||
|
|
||||||
|
parseEvent(text) {
|
||||||
|
let VEVENT,
|
||||||
|
VALARM,
|
||||||
|
multiple = ['ATTACH','ATTENDEE','CATEGORIES','COMMENT','CONTACT','EXDATE',
|
||||||
|
'EXRULE','RSTATUS','RELATED','RESOURCES','RDATE','RRULE'],
|
||||||
|
lines = text.split(/\r?\n/),
|
||||||
|
i = lines.length;
|
||||||
|
while (i--) {
|
||||||
|
let line = lines[i], prop;
|
||||||
|
if (VEVENT) {
|
||||||
|
while (line.startsWith(' ') && i--) {
|
||||||
|
line = lines[i] + line.slice(1);
|
||||||
|
}
|
||||||
|
if (line.startsWith('END:VALARM')) {
|
||||||
|
// Start parsing Alarm Component
|
||||||
|
VALARM = {};
|
||||||
|
continue;
|
||||||
|
} else if (line.startsWith('BEGIN:VALARM')) {
|
||||||
|
// End parsing Alarm Component
|
||||||
|
VEVENT.VALARM || (VEVENT.VALARM = []);
|
||||||
|
VEVENT.VALARM.push(VALARM);
|
||||||
|
VALARM = null;
|
||||||
|
continue;
|
||||||
|
} else if (line.startsWith('BEGIN:VEVENT')) {
|
||||||
|
// End parsing
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
prop = Property.fromString(line);
|
||||||
|
if (prop) {
|
||||||
|
if (VALARM) {
|
||||||
|
VALARM[prop.name] = prop;
|
||||||
|
} else if (multiple.includes(prop.name) || 'X-' == prop.name.slice(0,2)) {
|
||||||
|
VEVENT[prop.name] || (VEVENT[prop.name] = []);
|
||||||
|
VEVENT[prop.name].push(prop);
|
||||||
|
} else {
|
||||||
|
if ('DTSTART' === prop.name || 'DTEND' === prop.name) {
|
||||||
|
prop = parseDate(prop, prop.params.TZID);
|
||||||
|
}
|
||||||
|
VEVENT[prop.name] = prop;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (line.startsWith('END:VEVENT')) {
|
||||||
|
// Start parsing Event Component
|
||||||
|
VEVENT = {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// METHOD:REPLY || METHOD:REQUEST
|
||||||
|
if (VEVENT) {
|
||||||
|
VEVENT.rawText = text;
|
||||||
|
VEVENT.isCancelled = () => VEVENT.STATUS?.includes('CANCELLED');
|
||||||
|
VEVENT.isConfirmed = () => VEVENT.STATUS?.includes('CONFIRMED');
|
||||||
|
VEVENT.shouldReply = () => VEVENT.METHOD?.includes('REPLY');
|
||||||
|
console.dir({
|
||||||
|
VEVENT,
|
||||||
|
isCancelled: VEVENT.isCancelled(),
|
||||||
|
shouldReply: VEVENT.shouldReply()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return VEVENT;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Microsoft Windows timezones
|
||||||
|
// Subset from https://github.com/unicode-cldr/cldr-core/blob/master/supplemental/windowsZones.json
|
||||||
|
const windowsVTIMEZONEs = {
|
||||||
|
// Windows : [IANA...]
|
||||||
|
"Afghanistan Standard Time": [
|
||||||
|
"Asia/Kabul"
|
||||||
|
],
|
||||||
|
"Alaskan Standard Time": [
|
||||||
|
"America/Anchorage"/*,
|
||||||
|
"America/Juneau",
|
||||||
|
"America/Metlakatla",
|
||||||
|
"America/Nome",
|
||||||
|
"America/Sitka",
|
||||||
|
"America/Yakutat"*/
|
||||||
|
],
|
||||||
|
"Aleutian Standard Time": [
|
||||||
|
"America/Adak"
|
||||||
|
],
|
||||||
|
"Altai Standard Time": [
|
||||||
|
"Asia/Barnaul"
|
||||||
|
],
|
||||||
|
"Arab Standard Time": [
|
||||||
|
"Asia/Aden"/*,
|
||||||
|
"Asia/Bahrain",
|
||||||
|
"Asia/Kuwait",
|
||||||
|
"Asia/Qatar",
|
||||||
|
"Asia/Riyadh"
|
||||||
|
*/],
|
||||||
|
"Arabian Standard Time": [
|
||||||
|
"Asia/Dubai"/*,
|
||||||
|
"Asia/Muscat",
|
||||||
|
"Etc/GMT-4"
|
||||||
|
*/],
|
||||||
|
"Arabic Standard Time": [
|
||||||
|
"Asia/Baghdad"
|
||||||
|
],
|
||||||
|
"Argentina Standard Time": [
|
||||||
|
"America/Argentina/La_Rioja"/*,
|
||||||
|
"America/Argentina/Rio_Gallegos",
|
||||||
|
"America/Argentina/Salta",
|
||||||
|
"America/Argentina/San_Juan",
|
||||||
|
"America/Argentina/San_Luis",
|
||||||
|
"America/Argentina/Tucuman",
|
||||||
|
"America/Argentina/Ushuaia",
|
||||||
|
"America/Buenos_Aires",
|
||||||
|
"America/Catamarca",
|
||||||
|
"America/Cordoba",
|
||||||
|
"America/Jujuy",
|
||||||
|
"America/Mendoza"
|
||||||
|
*/],
|
||||||
|
"Astrakhan Standard Time": [
|
||||||
|
"Europe/Astrakhan"/*,
|
||||||
|
"Europe/Ulyanovsk"
|
||||||
|
*/],
|
||||||
|
"Atlantic Standard Time": [
|
||||||
|
"America/Glace_Bay"/*,
|
||||||
|
"America/Goose_Bay",
|
||||||
|
"America/Halifax",
|
||||||
|
"America/Moncton",
|
||||||
|
"America/Thule",
|
||||||
|
"Atlantic/Bermuda"
|
||||||
|
*/],
|
||||||
|
"AUS Central Standard Time": [
|
||||||
|
"Australia/Darwin"
|
||||||
|
],
|
||||||
|
"Aus Central W. Standard Time": [
|
||||||
|
"Australia/Eucla"
|
||||||
|
],
|
||||||
|
"AUS Eastern Standard Time": [
|
||||||
|
"Australia/Melbourne"/*,
|
||||||
|
"Australia/Sydney"
|
||||||
|
*/],
|
||||||
|
"Azerbaijan Standard Time": [
|
||||||
|
"Asia/Baku"
|
||||||
|
],
|
||||||
|
"Azores Standard Time": [
|
||||||
|
"America/Scoresbysund"/*,
|
||||||
|
"Atlantic/Azores"
|
||||||
|
*/],
|
||||||
|
"Bahia Standard Time": [
|
||||||
|
"America/Bahia"
|
||||||
|
],
|
||||||
|
"Bangladesh Standard Time": [
|
||||||
|
"Asia/Dhaka"/*,
|
||||||
|
"Asia/Thimphu"
|
||||||
|
*/],
|
||||||
|
"Belarus Standard Time": [
|
||||||
|
"Europe/Minsk"
|
||||||
|
],
|
||||||
|
"Bougainville Standard Time": [
|
||||||
|
"Pacific/Bougainville"
|
||||||
|
],
|
||||||
|
"Canada Central Standard Time": [
|
||||||
|
"America/Regina"/*,
|
||||||
|
"America/Swift_Current"
|
||||||
|
*/],
|
||||||
|
"Cape Verde Standard Time": [
|
||||||
|
"Atlantic/Cape_Verde"/*,
|
||||||
|
"Etc/GMT+1"
|
||||||
|
*/],
|
||||||
|
"Caucasus Standard Time": [
|
||||||
|
"Asia/Yerevan"
|
||||||
|
],
|
||||||
|
"Cen. Australia Standard Time": [
|
||||||
|
"Australia/Adelaide"/*,
|
||||||
|
"Australia/Broken_Hill"
|
||||||
|
*/],
|
||||||
|
"Central America Standard Time": [
|
||||||
|
"America/Belize"/*,
|
||||||
|
"America/Costa_Rica",
|
||||||
|
"America/El_Salvador",
|
||||||
|
"America/Guatemala",
|
||||||
|
"America/Managua",
|
||||||
|
"America/Tegucigalpa",
|
||||||
|
"Pacific/Galapagos",
|
||||||
|
"Etc/GMT+6"
|
||||||
|
*/],
|
||||||
|
"Central Asia Standard Time": [
|
||||||
|
"Asia/Almaty"/*,
|
||||||
|
"Asia/Bishkek",
|
||||||
|
"Asia/Qostanay",
|
||||||
|
"Asia/Urumqi",
|
||||||
|
"Indian/Chagos",
|
||||||
|
"Antarctica/Vostok",
|
||||||
|
"Etc/GMT-6"
|
||||||
|
*/],
|
||||||
|
"Central Brazilian Standard Time": [
|
||||||
|
"America/Campo_Grande"/*,
|
||||||
|
"America/Cuiaba"
|
||||||
|
*/],
|
||||||
|
"Central Europe Standard Time": [
|
||||||
|
"Europe/Belgrade"/*,
|
||||||
|
"Europe/Bratislava",
|
||||||
|
"Europe/Budapest",
|
||||||
|
"Europe/Ljubljana",
|
||||||
|
"Europe/Podgorica",
|
||||||
|
"Europe/Prague",
|
||||||
|
"Europe/Tirane"
|
||||||
|
*/],
|
||||||
|
"Central European Standard Time": [
|
||||||
|
"Europe/Sarajevo"/*,
|
||||||
|
"Europe/Skopje",
|
||||||
|
"Europe/Warsaw",
|
||||||
|
"Europe/Zagreb"
|
||||||
|
*/],
|
||||||
|
"Central Pacific Standard Time": [
|
||||||
|
"Pacific/Efate"/*,
|
||||||
|
"Pacific/Guadalcanal",
|
||||||
|
"Pacific/Noumea",
|
||||||
|
"Pacific/Ponape Pacific/Kosrae",
|
||||||
|
"Antarctica/Macquarie",
|
||||||
|
"Etc/GMT-11"
|
||||||
|
*/],
|
||||||
|
"Central Standard Time": [
|
||||||
|
"America/Chicago"/*,
|
||||||
|
"America/Indiana/Knox",
|
||||||
|
"America/Indiana/Tell_City",
|
||||||
|
"America/Matamoros",
|
||||||
|
"America/Menominee",
|
||||||
|
"America/North_Dakota/Beulah",
|
||||||
|
"America/North_Dakota/Center",
|
||||||
|
"America/North_Dakota/New_Salem",
|
||||||
|
"America/Rainy_River",
|
||||||
|
"America/Rankin_Inlet",
|
||||||
|
"America/Resolute",
|
||||||
|
"America/Winnipeg",
|
||||||
|
"CST6CDT"
|
||||||
|
*/],
|
||||||
|
"Central Standard Time (Mexico)": [
|
||||||
|
"America/Bahia_Banderas"/*,
|
||||||
|
"America/Merida",
|
||||||
|
"America/Mexico_City",
|
||||||
|
"America/Monterrey"
|
||||||
|
*/],
|
||||||
|
"Chatham Islands Standard Time": [
|
||||||
|
"Pacific/Chatham"
|
||||||
|
],
|
||||||
|
"China Standard Time": [
|
||||||
|
"Asia/Hong_Kong"/*,
|
||||||
|
"Asia/Macau",
|
||||||
|
"Asia/Shanghai"
|
||||||
|
*/],
|
||||||
|
"Cuba Standard Time": [
|
||||||
|
"America/Havana"
|
||||||
|
],
|
||||||
|
"Dateline Standard Time": [
|
||||||
|
"Etc/GMT+12"
|
||||||
|
],
|
||||||
|
"E. Africa Standard Time": [
|
||||||
|
"Africa/Addis_Ababa"/*,
|
||||||
|
"Africa/Asmera",
|
||||||
|
"Africa/Dar_es_Salaam",
|
||||||
|
"Africa/Djibouti",
|
||||||
|
"Africa/Juba",
|
||||||
|
"Africa/Kampala",
|
||||||
|
"Africa/Mogadishu",
|
||||||
|
"Africa/Nairobi",
|
||||||
|
"Indian/Antananarivo",
|
||||||
|
"Indian/Comoro",
|
||||||
|
"Indian/Mayotte",
|
||||||
|
"Antarctica/Syowa",
|
||||||
|
"Etc/GMT-3"
|
||||||
|
*/],
|
||||||
|
"E. Australia Standard Time": [
|
||||||
|
"Australia/Brisbane"/*,
|
||||||
|
"Australia/Lindeman"
|
||||||
|
*/],
|
||||||
|
"E. Europe Standard Time": [
|
||||||
|
"Europe/Chisinau"
|
||||||
|
],
|
||||||
|
"E. South America Standard Time": [
|
||||||
|
"America/Sao_Paulo"
|
||||||
|
],
|
||||||
|
"Easter Island Standard Time": [
|
||||||
|
"Pacific/Easter"
|
||||||
|
],
|
||||||
|
"Eastern Standard Time": [
|
||||||
|
"America/Detroit"/*,
|
||||||
|
"America/Indiana/Petersburg",
|
||||||
|
"America/Indiana/Vincennes",
|
||||||
|
"America/Indiana/Winamac",
|
||||||
|
"America/Iqaluit",
|
||||||
|
"America/Kentucky/Monticello",
|
||||||
|
"America/Louisville",
|
||||||
|
"America/Montreal",
|
||||||
|
"America/Nassau",
|
||||||
|
"America/New_York",
|
||||||
|
"America/Nipigon",
|
||||||
|
"America/Pangnirtung",
|
||||||
|
"America/Thunder_Bay",
|
||||||
|
"America/Toronto",
|
||||||
|
"EST5EDT"
|
||||||
|
*/],
|
||||||
|
"Eastern Standard Time (Mexico)": [
|
||||||
|
"America/Cancun"
|
||||||
|
],
|
||||||
|
"Egypt Standard Time": [
|
||||||
|
"Africa/Cairo"
|
||||||
|
],
|
||||||
|
"Ekaterinburg Standard Time": [
|
||||||
|
"Asia/Yekaterinburg"
|
||||||
|
],
|
||||||
|
"Fiji Standard Time": [
|
||||||
|
"Pacific/Fiji"
|
||||||
|
],
|
||||||
|
"FLE Standard Time": [
|
||||||
|
"Europe/Helsinki"/*,
|
||||||
|
"Europe/Kiev",
|
||||||
|
"Europe/Mariehamn",
|
||||||
|
"Europe/Riga",
|
||||||
|
"Europe/Sofia",
|
||||||
|
"Europe/Tallinn",
|
||||||
|
"Europe/Uzhgorod",
|
||||||
|
"Europe/Vilnius",
|
||||||
|
"Europe/Zaporozhye"
|
||||||
|
*/],
|
||||||
|
"Georgian Standard Time": [
|
||||||
|
"Asia/Tbilisi"
|
||||||
|
],
|
||||||
|
"GMT Standard Time": [
|
||||||
|
"Atlantic/Canary"/*,
|
||||||
|
"Atlantic/Faeroe",
|
||||||
|
"Atlantic/Madeira",
|
||||||
|
"Europe/Dublin",
|
||||||
|
"Europe/Guernsey",
|
||||||
|
"Europe/Isle_of_Man",
|
||||||
|
"Europe/Jersey",
|
||||||
|
"Europe/Lisbon",
|
||||||
|
"Europe/London"
|
||||||
|
*/],
|
||||||
|
"Greenland Standard Time": [
|
||||||
|
"America/Godthab"
|
||||||
|
],
|
||||||
|
"Greenwich Standard Time": [
|
||||||
|
"Africa/Abidjan"/*,
|
||||||
|
"Africa/Accra",
|
||||||
|
"Africa/Bamako",
|
||||||
|
"Africa/Banjul",
|
||||||
|
"Africa/Bissau",
|
||||||
|
"Africa/Conakry",
|
||||||
|
"Africa/Dakar",
|
||||||
|
"Africa/Freetown",
|
||||||
|
"Africa/Lome",
|
||||||
|
"Africa/Monrovia",
|
||||||
|
"Africa/Nouakchott",
|
||||||
|
"Africa/Ouagadougou",
|
||||||
|
"Atlantic/Reykjavik",
|
||||||
|
"Atlantic/St_Helena"
|
||||||
|
*/],
|
||||||
|
"GTB Standard Time": [
|
||||||
|
"Asia/Nicosia"/*,
|
||||||
|
"Asia/Famagusta",
|
||||||
|
"Europe/Athens",
|
||||||
|
"Europe/Bucharest"
|
||||||
|
*/],
|
||||||
|
"Haiti Standard Time": [
|
||||||
|
"America/Port-au-Prince"
|
||||||
|
],
|
||||||
|
"Hawaiian Standard Time": [
|
||||||
|
"Pacific/Honolulu"/*,
|
||||||
|
"Pacific/Johnston",
|
||||||
|
"Pacific/Rarotonga",
|
||||||
|
"Pacific/Tahiti",
|
||||||
|
"Etc/GMT+10"
|
||||||
|
*/],
|
||||||
|
"India Standard Time": [
|
||||||
|
"Asia/Calcutta"
|
||||||
|
],
|
||||||
|
"Iran Standard Time": [
|
||||||
|
"Asia/Tehran"
|
||||||
|
],
|
||||||
|
"Israel Standard Time": [
|
||||||
|
"Asia/Jerusalem"
|
||||||
|
],
|
||||||
|
"Jordan Standard Time": [
|
||||||
|
"Asia/Amman"
|
||||||
|
],
|
||||||
|
"Kaliningrad Standard Time": [
|
||||||
|
"Europe/Kaliningrad"
|
||||||
|
],
|
||||||
|
"Korea Standard Time": [
|
||||||
|
"Asia/Seoul"
|
||||||
|
],
|
||||||
|
"Libya Standard Time": [
|
||||||
|
"Africa/Tripoli"
|
||||||
|
],
|
||||||
|
"Line Islands Standard Time": [
|
||||||
|
"Pacific/Kiritimati"/*,
|
||||||
|
"Etc/GMT-14"
|
||||||
|
*/],
|
||||||
|
"Lord Howe Standard Time": [
|
||||||
|
"Australia/Lord_Howe"
|
||||||
|
],
|
||||||
|
"Magadan Standard Time": [
|
||||||
|
"Asia/Magadan"
|
||||||
|
],
|
||||||
|
"Magallanes Standard Time": [
|
||||||
|
"America/Punta_Arenas"
|
||||||
|
],
|
||||||
|
"Marquesas Standard Time": [
|
||||||
|
"Pacific/Marquesas"
|
||||||
|
],
|
||||||
|
"Mauritius Standard Time": [
|
||||||
|
"Indian/Mauritius"/*,
|
||||||
|
"Indian/Mahe",
|
||||||
|
"Indian/Reunion"
|
||||||
|
*/],
|
||||||
|
"Middle East Standard Time": [
|
||||||
|
"Asia/Beirut"
|
||||||
|
],
|
||||||
|
"Montevideo Standard Time": [
|
||||||
|
"America/Montevideo"
|
||||||
|
],
|
||||||
|
"Morocco Standard Time": [
|
||||||
|
"Africa/Casablanca"/*,
|
||||||
|
"Africa/El_Aaiun"
|
||||||
|
*/],
|
||||||
|
"Mountain Standard Time": [
|
||||||
|
"America/Boise"/*,
|
||||||
|
"America/Cambridge_Bay",
|
||||||
|
"America/Denver",
|
||||||
|
"America/Edmonton",
|
||||||
|
"America/Inuvik",
|
||||||
|
"America/Ojinaga",
|
||||||
|
"America/Yellowknife",
|
||||||
|
"MST7MDT"
|
||||||
|
*/],
|
||||||
|
"Mountain Standard Time (Mexico)": [
|
||||||
|
"America/Chihuahua"/*,
|
||||||
|
"America/Mazatlan"
|
||||||
|
*/],
|
||||||
|
"Myanmar Standard Time": [
|
||||||
|
"Asia/Rangoon"/*,
|
||||||
|
"Indian/Cocos"
|
||||||
|
*/],
|
||||||
|
"N. Central Asia Standard Time": [
|
||||||
|
"Asia/Novosibirsk"
|
||||||
|
],
|
||||||
|
"Namibia Standard Time": [
|
||||||
|
"Africa/Windhoek"
|
||||||
|
],
|
||||||
|
"Nepal Standard Time": [
|
||||||
|
"Asia/Katmandu"
|
||||||
|
],
|
||||||
|
"New Zealand Standard Time": [
|
||||||
|
"Pacific/Auckland"/*,
|
||||||
|
"Antarctica/McMurdo"
|
||||||
|
*/],
|
||||||
|
"Newfoundland Standard Time": [
|
||||||
|
"America/St_Johns"
|
||||||
|
],
|
||||||
|
"Norfolk Standard Time": [
|
||||||
|
"Pacific/Norfolk"
|
||||||
|
],
|
||||||
|
"North Asia East Standard Time": [
|
||||||
|
"Asia/Irkutsk"
|
||||||
|
],
|
||||||
|
"North Asia Standard Time": [
|
||||||
|
"Asia/Krasnoyarsk"/*,
|
||||||
|
"Asia/Novokuznetsk"
|
||||||
|
*/],
|
||||||
|
"North Korea Standard Time": [
|
||||||
|
"Asia/Pyongyang"
|
||||||
|
],
|
||||||
|
"Omsk Standard Time": [
|
||||||
|
"Asia/Omsk"
|
||||||
|
],
|
||||||
|
"Pacific SA Standard Time": [
|
||||||
|
"America/Santiago"
|
||||||
|
],
|
||||||
|
"Pacific Standard Time": [
|
||||||
|
"America/Los_Angeles"/*,
|
||||||
|
"America/Dawson",
|
||||||
|
"America/Vancouver",
|
||||||
|
"America/Whitehorse",
|
||||||
|
"PST8PDT"
|
||||||
|
*/],
|
||||||
|
"Pacific Standard Time (Mexico)": [
|
||||||
|
"America/Tijuana"/*,
|
||||||
|
"America/Santa_Isabel"
|
||||||
|
*/],
|
||||||
|
"Pakistan Standard Time": [
|
||||||
|
"Asia/Karachi"
|
||||||
|
],
|
||||||
|
"Paraguay Standard Time": [
|
||||||
|
"America/Asuncion"
|
||||||
|
],
|
||||||
|
"Qyzylorda Standard Time": [
|
||||||
|
"Asia/Qyzylorda"
|
||||||
|
],
|
||||||
|
"Romance Standard Time": [
|
||||||
|
"Europe/Paris"/*,
|
||||||
|
"Europe/Brussels",
|
||||||
|
"Europe/Copenhagen",
|
||||||
|
"Europe/Madrid",
|
||||||
|
"Africa/Ceuta"
|
||||||
|
*/],
|
||||||
|
"Russia Time Zone 3": [
|
||||||
|
"Europe/Samara"
|
||||||
|
],
|
||||||
|
"Russia Time Zone 10": [
|
||||||
|
"Asia/Srednekolymsk"
|
||||||
|
],
|
||||||
|
"Russia Time Zone 11": [
|
||||||
|
"Asia/Kamchatka"/*,
|
||||||
|
"Asia/Anadyr"
|
||||||
|
*/],
|
||||||
|
"Russian Standard Time": [
|
||||||
|
"Europe/Moscow"/*,
|
||||||
|
"Europe/Kirov",
|
||||||
|
"Europe/Simferopol"
|
||||||
|
*/],
|
||||||
|
"SA Eastern Standard Time": [
|
||||||
|
"America/Belem"/*,
|
||||||
|
"America/Cayenne",
|
||||||
|
"America/Fortaleza",
|
||||||
|
"America/Maceio",
|
||||||
|
"America/Paramaribo",
|
||||||
|
"America/Recife",
|
||||||
|
"America/Santarem",
|
||||||
|
"Atlantic/Stanley",
|
||||||
|
"Antarctica/Palmer",
|
||||||
|
"Antarctica/Rothera",
|
||||||
|
"Etc/GMT+3"
|
||||||
|
*/],
|
||||||
|
"SA Pacific Standard Time": [
|
||||||
|
"America/Bogota"/*,
|
||||||
|
"America/Cayman",
|
||||||
|
"America/Coral_Harbour",
|
||||||
|
"America/Eirunepe",
|
||||||
|
"America/Guayaquil",
|
||||||
|
"America/Jamaica",
|
||||||
|
"America/Lima",
|
||||||
|
"America/Panama",
|
||||||
|
"America/Rio_Branco",
|
||||||
|
"Etc/GMT+5"
|
||||||
|
*/],
|
||||||
|
"SA Western Standard Time": [
|
||||||
|
"America/Anguilla"/*,
|
||||||
|
"America/Antigua",
|
||||||
|
"America/Aruba",
|
||||||
|
"America/Barbados",
|
||||||
|
"America/Blanc-Sablon",
|
||||||
|
"America/Boa_Vista",
|
||||||
|
"America/Curacao",
|
||||||
|
"America/Dominica",
|
||||||
|
"America/Grenada",
|
||||||
|
"America/Guadeloupe",
|
||||||
|
"America/Guyana",
|
||||||
|
"America/Kralendijk",
|
||||||
|
"America/La_Paz",
|
||||||
|
"America/Lower_Princes",
|
||||||
|
"America/Manaus",
|
||||||
|
"America/Marigot",
|
||||||
|
"America/Martinique",
|
||||||
|
"America/Montserrat",
|
||||||
|
"America/Port_of_Spain",
|
||||||
|
"America/Porto_Velho",
|
||||||
|
"America/Puerto_Rico",
|
||||||
|
"America/Santo_Domingo",
|
||||||
|
"America/St_Barthelemy",
|
||||||
|
"America/St_Kitts",
|
||||||
|
"America/St_Lucia",
|
||||||
|
"America/St_Thomas",
|
||||||
|
"America/St_Vincent",
|
||||||
|
"America/Tortola",
|
||||||
|
"Etc/GMT+4"
|
||||||
|
*/],
|
||||||
|
"Saint Pierre Standard Time": [
|
||||||
|
"America/Miquelon"
|
||||||
|
],
|
||||||
|
"Sakhalin Standard Time": [
|
||||||
|
"Asia/Sakhalin"
|
||||||
|
],
|
||||||
|
"Samoa Standard Time": [
|
||||||
|
"Pacific/Apia"
|
||||||
|
],
|
||||||
|
"Sao Tome Standard Time": [
|
||||||
|
"Africa/Sao_Tome"
|
||||||
|
],
|
||||||
|
"Saratov Standard Time": [
|
||||||
|
"Europe/Saratov"
|
||||||
|
],
|
||||||
|
"SE Asia Standard Time": [
|
||||||
|
"Asia/Bangkok"/*,
|
||||||
|
"Asia/Jakarta",
|
||||||
|
"Asia/Phnom_Penh",
|
||||||
|
"Asia/Pontianak",
|
||||||
|
"Asia/Saigon",
|
||||||
|
"Asia/Vientiane",
|
||||||
|
"Indian/Christmas",
|
||||||
|
"Antarctica/Davis",
|
||||||
|
"Etc/GMT-7"
|
||||||
|
*/],
|
||||||
|
"Singapore Standard Time": [
|
||||||
|
"Asia/Singapore"/*,
|
||||||
|
"Asia/Brunei",
|
||||||
|
"Asia/Kuala_Lumpur",
|
||||||
|
"Asia/Kuching",
|
||||||
|
"Asia/Makassar",
|
||||||
|
"Asia/Manila",
|
||||||
|
"Antarctica/Casey",
|
||||||
|
"Etc/GMT-8"
|
||||||
|
*/],
|
||||||
|
"South Africa Standard Time": [
|
||||||
|
"Africa/Johannesburg"/*,
|
||||||
|
"Africa/Blantyre",
|
||||||
|
"Africa/Bujumbura",
|
||||||
|
"Africa/Gaborone",
|
||||||
|
"Africa/Harare",
|
||||||
|
"Africa/Kigali",
|
||||||
|
"Africa/Lubumbashi",
|
||||||
|
"Africa/Lusaka",
|
||||||
|
"Africa/Maputo",
|
||||||
|
"Africa/Maseru",
|
||||||
|
"Africa/Mbabane",
|
||||||
|
"Etc/GMT-2"
|
||||||
|
*/],
|
||||||
|
"Sri Lanka Standard Time": [
|
||||||
|
"Asia/Colombo"
|
||||||
|
],
|
||||||
|
"Sudan Standard Time": [
|
||||||
|
"Africa/Khartoum"
|
||||||
|
],
|
||||||
|
"Syria Standard Time": [
|
||||||
|
"Asia/Damascus"
|
||||||
|
],
|
||||||
|
"Taipei Standard Time": [
|
||||||
|
"Asia/Taipei"
|
||||||
|
],
|
||||||
|
"Tasmania Standard Time": [
|
||||||
|
"Australia/Currie"/*,
|
||||||
|
"Australia/Hobart"
|
||||||
|
*/],
|
||||||
|
"Tocantins Standard Time": [
|
||||||
|
"America/Araguaina"
|
||||||
|
],
|
||||||
|
"Tokyo Standard Time": [
|
||||||
|
"Asia/Tokyo"/*,
|
||||||
|
"Asia/Dili",
|
||||||
|
"Asia/Jayapura",
|
||||||
|
"Pacific/Palau",
|
||||||
|
"Etc/GMT-9"
|
||||||
|
*/],
|
||||||
|
"Tomsk Standard Time": [
|
||||||
|
"Asia/Tomsk"
|
||||||
|
],
|
||||||
|
"Tonga Standard Time": [
|
||||||
|
"Pacific/Tongatapu"
|
||||||
|
],
|
||||||
|
"Transbaikal Standard Time": [
|
||||||
|
"Asia/Chita"
|
||||||
|
],
|
||||||
|
"Turkey Standard Time": [
|
||||||
|
"Europe/Istanbul"
|
||||||
|
],
|
||||||
|
"Turks And Caicos Standard Time": [
|
||||||
|
"America/Grand_Turk"
|
||||||
|
],
|
||||||
|
"Ulaanbaatar Standard Time": [
|
||||||
|
"Asia/Ulaanbaatar"/*,
|
||||||
|
"Asia/Choibalsan"
|
||||||
|
*/],
|
||||||
|
"US Eastern Standard Time": [
|
||||||
|
"America/Indianapolis"/*,
|
||||||
|
"America/Indiana/Marengo",
|
||||||
|
"America/Indiana/Vevay"
|
||||||
|
*/],
|
||||||
|
"US Mountain Standard Time": [
|
||||||
|
"America/Phoenix"/*,
|
||||||
|
"America/Creston",
|
||||||
|
"America/Dawson_Creek",
|
||||||
|
"America/Fort_Nelson",
|
||||||
|
"America/Hermosillo",
|
||||||
|
"Etc/GMT+7"
|
||||||
|
*/],
|
||||||
|
"UTC": [
|
||||||
|
"Etc/GMT"/*,
|
||||||
|
"America/Danmarkshavn",
|
||||||
|
"Etc/UTC"
|
||||||
|
*/],
|
||||||
|
"UTC-02": [
|
||||||
|
"Etc/GMT+2"/*,
|
||||||
|
"America/Noronha",
|
||||||
|
"Atlantic/South_Georgia"
|
||||||
|
*/],
|
||||||
|
"UTC-08": [
|
||||||
|
"Etc/GMT+8"/*,
|
||||||
|
"Pacific/Pitcairn"
|
||||||
|
*/],
|
||||||
|
"UTC-09": [
|
||||||
|
"Etc/GMT+9"/*,
|
||||||
|
"Pacific/Gambier"
|
||||||
|
*/],
|
||||||
|
"UTC-11": [
|
||||||
|
"Etc/GMT+11"/*,
|
||||||
|
"Pacific/Midway",
|
||||||
|
"Pacific/Niue",
|
||||||
|
"Pacific/Pago_Pago"
|
||||||
|
*/],
|
||||||
|
"UTC+12": [
|
||||||
|
"Etc/GMT-12"/*,
|
||||||
|
"Pacific/Funafuti",
|
||||||
|
"Pacific/Kwajalein",
|
||||||
|
"Pacific/Majuro",
|
||||||
|
"Pacific/Nauru",
|
||||||
|
"Pacific/Tarawa",
|
||||||
|
"Pacific/Wake",
|
||||||
|
"Pacific/Wallis"
|
||||||
|
*/],
|
||||||
|
"UTC+13": [
|
||||||
|
"Etc/GMT-13"/*,
|
||||||
|
"Pacific/Enderbury",
|
||||||
|
"Pacific/Fakaofo"
|
||||||
|
*/],
|
||||||
|
"Venezuela Standard Time": [
|
||||||
|
"America/Caracas"
|
||||||
|
],
|
||||||
|
"Vladivostok Standard Time": [
|
||||||
|
"Asia/Vladivostok"/*,
|
||||||
|
"Asia/Ust-Nera"
|
||||||
|
*/],
|
||||||
|
"Volgograd Standard Time": [
|
||||||
|
"Europe/Volgograd"
|
||||||
|
],
|
||||||
|
"W. Australia Standard Time": [
|
||||||
|
"Australia/Perth"
|
||||||
|
],
|
||||||
|
"W. Central Africa Standard Time": [
|
||||||
|
"Africa/Algiers"/*,
|
||||||
|
"Africa/Bangui",
|
||||||
|
"Africa/Brazzaville",
|
||||||
|
"Africa/Douala",
|
||||||
|
"Africa/Kinshasa",
|
||||||
|
"Africa/Lagos",
|
||||||
|
"Africa/Libreville",
|
||||||
|
"Africa/Luanda",
|
||||||
|
"Africa/Malabo",
|
||||||
|
"Africa/Ndjamena",
|
||||||
|
"Africa/Niamey",
|
||||||
|
"Africa/Porto-Novo",
|
||||||
|
"Africa/Tunis",
|
||||||
|
"Etc/GMT-1"
|
||||||
|
*/],
|
||||||
|
"W. Europe Standard Time": [
|
||||||
|
"Europe/Amsterdam"/*,
|
||||||
|
"Europe/Andorra",
|
||||||
|
"Europe/Berlin",
|
||||||
|
"Europe/Busingen",
|
||||||
|
"Europe/Gibraltar",
|
||||||
|
"Europe/Luxembourg",
|
||||||
|
"Europe/Malta",
|
||||||
|
"Europe/Monaco",
|
||||||
|
"Europe/Oslo",
|
||||||
|
"Europe/Rome",
|
||||||
|
"Europe/San_Marino",
|
||||||
|
"Europe/Stockholm",
|
||||||
|
"Europe/Vaduz",
|
||||||
|
"Europe/Vatican",
|
||||||
|
"Europe/Vienna",
|
||||||
|
"Europe/Zurich",
|
||||||
|
"Arctic/Longyearbyen"
|
||||||
|
*/],
|
||||||
|
"W. Mongolia Standard Time": [
|
||||||
|
"Asia/Hovd"
|
||||||
|
],
|
||||||
|
"West Asia Standard Time": [
|
||||||
|
"Asia/Aqtau"/*,
|
||||||
|
"Asia/Aqtobe",
|
||||||
|
"Asia/Ashgabat",
|
||||||
|
"Asia/Atyrau",
|
||||||
|
"Asia/Dushanbe",
|
||||||
|
"Asia/Oral",
|
||||||
|
"Asia/Samarkand",
|
||||||
|
"Asia/Tashkent",
|
||||||
|
"Indian/Kerguelen",
|
||||||
|
"Indian/Maldives",
|
||||||
|
"Antarctica/Mawson",
|
||||||
|
"Etc/GMT-5"
|
||||||
|
*/],
|
||||||
|
"West Bank Standard Time": [
|
||||||
|
"Asia/Gaza"/*,
|
||||||
|
"Asia/Hebron"
|
||||||
|
*/],
|
||||||
|
"West Pacific Standard Time": [
|
||||||
|
"Pacific/Guam"/*,
|
||||||
|
"Pacific/Port_Moresby",
|
||||||
|
"Pacific/Saipan",
|
||||||
|
"Pacific/Truk",
|
||||||
|
"Antarctica/DumontDUrville",
|
||||||
|
"Etc/GMT-10"
|
||||||
|
*/],
|
||||||
|
"Yakutsk Standard Time": [
|
||||||
|
"Asia/Khandyga"/*,
|
||||||
|
"Asia/Yakutsk"
|
||||||
|
*/],
|
||||||
|
};
|
||||||
|
})(window.rl);
|
||||||
|
|
@ -13,7 +13,7 @@ class ViewICSPlugin extends \RainLoop\Plugins\AbstractPlugin
|
||||||
public function Init() : void
|
public function Init() : void
|
||||||
{
|
{
|
||||||
// $this->UseLangs(true);
|
// $this->UseLangs(true);
|
||||||
|
$this->addJs('ical.js');
|
||||||
$this->addJs('message.js');
|
$this->addJs('message.js');
|
||||||
$this->addJs('windowsZones.js');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,30 +7,7 @@
|
||||||
const
|
const
|
||||||
template = document.getElementById(templateId),
|
template = document.getElementById(templateId),
|
||||||
view = e.detail,
|
view = e.detail,
|
||||||
attachmentsPlace = template.content.querySelector('.attachmentsPlace'),
|
attachmentsPlace = template.content.querySelector('.attachmentsPlace');
|
||||||
dateRegEx = /(TZID=(?<tz>[^:]+):)?(?<year>[0-9]{4})(?<month>[0-9]{2})(?<day>[0-9]{2})T(?<hour>[0-9]{2})(?<minute>[0-9]{2})(?<second>[0-9]{2})(?<utc>Z?)/,
|
|
||||||
parseDate = str => {
|
|
||||||
let parts = dateRegEx.exec(str)?.groups,
|
|
||||||
options = {dateStyle: 'long', timeStyle: 'short'},
|
|
||||||
date = (parts ? new Date(
|
|
||||||
parseInt(parts.year, 10),
|
|
||||||
parseInt(parts.month, 10) - 1,
|
|
||||||
parseInt(parts.day, 10),
|
|
||||||
parseInt(parts.hour, 10),
|
|
||||||
parseInt(parts.minute, 10),
|
|
||||||
parseInt(parts.second, 10)
|
|
||||||
) : new Date(str));
|
|
||||||
parts?.tz && (options.timeZone = windowsVTIMEZONEs[parts.tz] || parts.tz);
|
|
||||||
try {
|
|
||||||
return date.format(options);
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
if (options.timeZone) {
|
|
||||||
options.timeZone = undefined;
|
|
||||||
return date.format(options);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
attachmentsPlace.after(Element.fromHTML(`
|
attachmentsPlace.after(Element.fromHTML(`
|
||||||
<details data-bind="if: viewICS, visible: viewICS">
|
<details data-bind="if: viewICS, visible: viewICS">
|
||||||
|
|
@ -74,8 +51,8 @@
|
||||||
if (item["ical:summary"]) {
|
if (item["ical:summary"]) {
|
||||||
let VEVENT = {
|
let VEVENT = {
|
||||||
SUMMARY: item["ical:summary"],
|
SUMMARY: item["ical:summary"],
|
||||||
DTSTART: parseDate(item["ical:dtstart"]),
|
DTSTART: rl.ICS.parseDate(item["ical:dtstart"]),
|
||||||
// DTEND: parseDate(item["ical:dtend"]),
|
// DTEND: rl.ICS.parseDate(item["ical:dtend"]),
|
||||||
// TRANSP: item["ical:transp"],
|
// TRANSP: item["ical:transp"],
|
||||||
// LOCATION: item["ical:location"],
|
// LOCATION: item["ical:location"],
|
||||||
ATTENDEE: []
|
ATTENDEE: []
|
||||||
|
|
@ -94,58 +71,8 @@
|
||||||
rl.fetch(ics.linkDownload())
|
rl.fetch(ics.linkDownload())
|
||||||
.then(response => (response.status < 400) ? response.text() : Promise.reject(new Error({ response })))
|
.then(response => (response.status < 400) ? response.text() : Promise.reject(new Error({ response })))
|
||||||
.then(text => {
|
.then(text => {
|
||||||
let VEVENT,
|
let VEVENT = rl.ICS.parseEvent(text);
|
||||||
VALARM,
|
|
||||||
multiple = ['ATTACH','ATTENDEE','CATEGORIES','COMMENT','CONTACT','EXDATE',
|
|
||||||
'EXRULE','RSTATUS','RELATED','RESOURCES','RDATE','RRULE'],
|
|
||||||
lines = text.split(/\r?\n/),
|
|
||||||
i = lines.length;
|
|
||||||
while (i--) {
|
|
||||||
let line = lines[i];
|
|
||||||
if (VEVENT) {
|
|
||||||
while (line.startsWith(' ') && i--) {
|
|
||||||
line = lines[i] + line.slice(1);
|
|
||||||
}
|
|
||||||
if (line.startsWith('END:VALARM')) {
|
|
||||||
VALARM = {};
|
|
||||||
continue;
|
|
||||||
} else if (line.startsWith('BEGIN:VALARM')) {
|
|
||||||
VEVENT.VALARM || (VEVENT.VALARM = []);
|
|
||||||
VEVENT.VALARM.push(VALARM);
|
|
||||||
VALARM = null;
|
|
||||||
continue;
|
|
||||||
} else if (line.startsWith('BEGIN:VEVENT')) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
line = line.match(/^([^:;]+)[:;](.+)$/);
|
|
||||||
if (line) {
|
|
||||||
if (VALARM) {
|
|
||||||
VALARM[line[1]] = line[2];
|
|
||||||
} else if (multiple.includes(line[1]) || 'X-' == line[1].slice(0,2)) {
|
|
||||||
VEVENT[line[1]] || (VEVENT[line[1]] = []);
|
|
||||||
VEVENT[line[1]].push(line[2]);
|
|
||||||
} else {
|
|
||||||
if ('DTSTART' === line[1] || 'DTEND' === line[1]) {
|
|
||||||
line[2] = parseDate(line[2]);
|
|
||||||
}
|
|
||||||
VEVENT[line[1]] = line[2];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (line.startsWith('END:VEVENT')) {
|
|
||||||
VEVENT = {};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// METHOD:REPLY || METHOD:REQUEST
|
|
||||||
// console.dir({VEVENT:VEVENT});
|
|
||||||
if (VEVENT) {
|
if (VEVENT) {
|
||||||
VEVENT.rawText = text;
|
|
||||||
VEVENT.isCancelled = () => VEVENT.STATUS?.includes('CANCELLED');
|
|
||||||
VEVENT.isConfirmed = () => VEVENT.STATUS?.includes('CONFIRMED');
|
|
||||||
VEVENT.shouldReply = () => VEVENT.METHOD?.includes('REPLY');
|
|
||||||
console.dir({
|
|
||||||
isCancelled: VEVENT.isCancelled(),
|
|
||||||
shouldReply: VEVENT.shouldReply()
|
|
||||||
});
|
|
||||||
view.viewICS(VEVENT);
|
view.viewICS(VEVENT);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -64,14 +64,14 @@ abstract class Collection extends \ArrayObject implements \JsonSerializable
|
||||||
return $callable($this->getArrayCopy(), ...$arguments);
|
return $callable($this->getArrayCopy(), ...$arguments);
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
public function Slice(int $offset, int $length = null, bool $preserve_keys = false)
|
public function Slice(int $offset, ?int $length = null, bool $preserve_keys = false)
|
||||||
{
|
{
|
||||||
return new static(
|
return new static(
|
||||||
\array_slice($this->getArrayCopy(), $offset, $length, $preserve_keys)
|
\array_slice($this->getArrayCopy(), $offset, $length, $preserve_keys)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function Crop(int $length = null, int $offset = 0, bool $preserve_keys = false)
|
public function Crop(?int $length = null, int $offset = 0, bool $preserve_keys = false)
|
||||||
{
|
{
|
||||||
$this->exchangeArray(
|
$this->exchangeArray(
|
||||||
\array_slice($this->getArrayCopy(), $offset, $length, $preserve_keys)
|
\array_slice($this->getArrayCopy(), $offset, $length, $preserve_keys)
|
||||||
|
|
|
||||||
|
|
@ -109,7 +109,7 @@ class Binary
|
||||||
* @return resource|bool
|
* @return resource|bool
|
||||||
*/
|
*/
|
||||||
public static function CreateStream($rStream,
|
public static function CreateStream($rStream,
|
||||||
string $sUtilsDecodeOrEncodeFunctionName = null, string $sFromEncoding = null, string $sToEncoding = null)
|
?string $sUtilsDecodeOrEncodeFunctionName = null, ?string $sFromEncoding = null, ?string $sToEncoding = null)
|
||||||
{
|
{
|
||||||
if (null === $sUtilsDecodeOrEncodeFunctionName || !\strlen($sUtilsDecodeOrEncodeFunctionName)) {
|
if (null === $sUtilsDecodeOrEncodeFunctionName || !\strlen($sUtilsDecodeOrEncodeFunctionName)) {
|
||||||
$sUtilsDecodeOrEncodeFunctionName = 'InlineNullDecode';
|
$sUtilsDecodeOrEncodeFunctionName = 'InlineNullDecode';
|
||||||
|
|
|
||||||
|
|
@ -285,7 +285,7 @@ trait Folders
|
||||||
* @throws \MailSo\Net\Exceptions\*
|
* @throws \MailSo\Net\Exceptions\*
|
||||||
* @throws \MailSo\Imap\Exceptions\*
|
* @throws \MailSo\Imap\Exceptions\*
|
||||||
*/
|
*/
|
||||||
public function FolderExpunge(SequenceSet $oUidRange = null) : void
|
public function FolderExpunge(?SequenceSet $oUidRange = null) : void
|
||||||
{
|
{
|
||||||
$sCmd = 'EXPUNGE';
|
$sCmd = 'EXPUNGE';
|
||||||
$aArguments = array();
|
$aArguments = array();
|
||||||
|
|
|
||||||
|
|
@ -163,7 +163,7 @@ trait Messages
|
||||||
* @throws \MailSo\Net\Exceptions\*
|
* @throws \MailSo\Net\Exceptions\*
|
||||||
* @throws \MailSo\Imap\Exceptions\*
|
* @throws \MailSo\Imap\Exceptions\*
|
||||||
*/
|
*/
|
||||||
public function MessageAppendStream(string $sFolderName, $rMessageStream, int $iStreamSize, array $aFlagsList = null, int $iDateTime = 0) : ?int
|
public function MessageAppendStream(string $sFolderName, $rMessageStream, int $iStreamSize, ?array $aFlagsList = null, int $iDateTime = 0) : ?int
|
||||||
{
|
{
|
||||||
if (!\is_resource($rMessageStream)) {
|
if (!\is_resource($rMessageStream)) {
|
||||||
throw new \InvalidArgumentException('$rMessageStream must be a resource');
|
throw new \InvalidArgumentException('$rMessageStream must be a resource');
|
||||||
|
|
@ -225,7 +225,7 @@ trait Messages
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* RFC 3502 MULTIAPPEND
|
* RFC 3502 MULTIAPPEND
|
||||||
public function MessageAppendStreams(string $sFolderName, $rMessageAppendStream, int $iStreamSize, array $aFlagsList = null, int &$iUid = null, int $iDateTime = 0) : ?int
|
public function MessageAppendStreams(string $sFolderName, $rMessageAppendStream, int $iStreamSize, ?array $aFlagsList = null, ?int &$iUid = null, int $iDateTime = 0) : ?int
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -311,7 +311,7 @@ trait Messages
|
||||||
* @throws \MailSo\Net\Exceptions\*
|
* @throws \MailSo\Net\Exceptions\*
|
||||||
* @throws \MailSo\Imap\Exceptions\*
|
* @throws \MailSo\Imap\Exceptions\*
|
||||||
*/
|
*/
|
||||||
public function MessageReplaceStream(string $sFolderName, int $iUid, $rMessageStream, int $iStreamSize, array $aFlagsList = null, int $iDateTime = 0) : ?int
|
public function MessageReplaceStream(string $sFolderName, int $iUid, $rMessageStream, int $iStreamSize, ?array $aFlagsList = null, int $iDateTime = 0) : ?int
|
||||||
{
|
{
|
||||||
if (1 > $iUid || !$this->hasCapability('REPLACE')) {
|
if (1 > $iUid || !$this->hasCapability('REPLACE')) {
|
||||||
$this->FolderSelect($sFolderName);
|
$this->FolderSelect($sFolderName);
|
||||||
|
|
@ -412,7 +412,7 @@ trait Messages
|
||||||
* @throws \MailSo\Net\Exceptions\*
|
* @throws \MailSo\Net\Exceptions\*
|
||||||
* @throws \MailSo\Imap\Exceptions\*
|
* @throws \MailSo\Imap\Exceptions\*
|
||||||
*/
|
*/
|
||||||
public function MessageESearch(string $sSearchCriterias, array $aSearchReturn = null, bool $bReturnUid = true, string $sLimit = '') : array
|
public function MessageESearch(string $sSearchCriterias, ?array $aSearchReturn = null, bool $bReturnUid = true, string $sLimit = '') : array
|
||||||
{
|
{
|
||||||
$oESearch = new \MailSo\Imap\Requests\ESEARCH($this);
|
$oESearch = new \MailSo\Imap\Requests\ESEARCH($this);
|
||||||
$oESearch->sCriterias = $sSearchCriterias ?: 'ALL';
|
$oESearch->sCriterias = $sSearchCriterias ?: 'ALL';
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@ class Folder implements \JsonSerializable
|
||||||
/**
|
/**
|
||||||
* @throws \ValueError
|
* @throws \ValueError
|
||||||
*/
|
*/
|
||||||
function __construct(string $sFullName, string $sDelimiter = null, array $aAttributes = array())
|
function __construct(string $sFullName, ?string $sDelimiter = null, array $aAttributes = array())
|
||||||
{
|
{
|
||||||
if (!\strlen($sFullName)) {
|
if (!\strlen($sFullName)) {
|
||||||
throw new \ValueError;
|
throw new \ValueError;
|
||||||
|
|
|
||||||
|
|
@ -466,7 +466,7 @@ class ImapClient extends \MailSo\Net\NetClient
|
||||||
* @throws \MailSo\Net\Exceptions\*
|
* @throws \MailSo\Net\Exceptions\*
|
||||||
* @throws \MailSo\Imap\Exceptions\*
|
* @throws \MailSo\Imap\Exceptions\*
|
||||||
*/
|
*/
|
||||||
protected function streamResponse(string $sEndTag = null) : void
|
protected function streamResponse(?string $sEndTag = null) : void
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
if (\is_resource($this->ConnectionResource())) {
|
if (\is_resource($this->ConnectionResource())) {
|
||||||
|
|
@ -488,7 +488,7 @@ class ImapClient extends \MailSo\Net\NetClient
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function getResponse(string $sEndTag = null) : ResponseCollection
|
protected function getResponse(?string $sEndTag = null) : ResponseCollection
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
$oResult = new ResponseCollection;
|
$oResult = new ResponseCollection;
|
||||||
|
|
|
||||||
|
|
@ -279,7 +279,7 @@ class MailClient
|
||||||
return ($aFetchResponse && 1 === \count($aFetchResponse));
|
return ($aFetchResponse && 1 === \count($aFetchResponse));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function MessageAppendFile(string $sMessageFileName, string $sFolderToSave, array $aAppendFlags = null) : int
|
public function MessageAppendFile(string $sMessageFileName, string $sFolderToSave, ?array $aAppendFlags = null) : int
|
||||||
{
|
{
|
||||||
if (!\is_file($sMessageFileName) || !\is_readable($sMessageFileName)) {
|
if (!\is_file($sMessageFileName) || !\is_readable($sMessageFileName)) {
|
||||||
throw new \ValueError;
|
throw new \ValueError;
|
||||||
|
|
@ -349,7 +349,7 @@ class MailClient
|
||||||
* @throws \MailSo\Net\Exceptions\*
|
* @throws \MailSo\Net\Exceptions\*
|
||||||
* @throws \MailSo\Imap\Exceptions\*
|
* @throws \MailSo\Imap\Exceptions\*
|
||||||
*/
|
*/
|
||||||
public function FolderInformation(string $sFolderName, int $iPrevUidNext = 0, SequenceSet $oRange = null) : array
|
public function FolderInformation(string $sFolderName, int $iPrevUidNext = 0, ?SequenceSet $oRange = null) : array
|
||||||
{
|
{
|
||||||
if ($oRange) {
|
if ($oRange) {
|
||||||
// $aInfo = $this->oImapClient->FolderExamine($sFolderName)->jsonSerialize();
|
// $aInfo = $this->oImapClient->FolderExamine($sFolderName)->jsonSerialize();
|
||||||
|
|
@ -553,11 +553,13 @@ class MailClient
|
||||||
$aSortUids = \array_reduce($aAllThreads, 'array_merge', []);
|
$aSortUids = \array_reduce($aAllThreads, 'array_merge', []);
|
||||||
$oParams->oSequenceSet = new \MailSo\Imap\SequenceSet($aSortUids);
|
$oParams->oSequenceSet = new \MailSo\Imap\SequenceSet($aSortUids);
|
||||||
$aSortUids = $this->GetUids($oParams, $oFolderInfo);
|
$aSortUids = $this->GetUids($oParams, $oFolderInfo);
|
||||||
foreach ($aAllThreads as $aThreadUIDs) {
|
if ($aSortUids) {
|
||||||
$aThreadUIDs = \array_intersect($aSortUids, $aThreadUIDs);
|
foreach ($aAllThreads as $aThreadUIDs) {
|
||||||
// Remove the most recent UID
|
$aThreadUIDs = \array_intersect($aSortUids, $aThreadUIDs);
|
||||||
\array_pop($aThreadUIDs);
|
// Remove the most recent UID
|
||||||
$aUids = \array_merge($aUids, $aThreadUIDs);
|
\array_pop($aThreadUIDs);
|
||||||
|
$aUids = \array_merge($aUids, $aThreadUIDs);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -631,7 +633,7 @@ class MailClient
|
||||||
* @throws \MailSo\Net\Exceptions\*
|
* @throws \MailSo\Net\Exceptions\*
|
||||||
* @throws \MailSo\Imap\Exceptions\*
|
* @throws \MailSo\Imap\Exceptions\*
|
||||||
*/
|
*/
|
||||||
private function GetUids(MessageListParams $oParams, FolderInformation $oInfo, bool $onlyCache = false) : array
|
private function GetUids(MessageListParams $oParams, FolderInformation $oInfo, bool $onlyCache = false) : ?array
|
||||||
{
|
{
|
||||||
$oCacher = $oParams->oCacher;
|
$oCacher = $oParams->oCacher;
|
||||||
$sFolderName = $oParams->sFolderName;
|
$sFolderName = $oParams->sFolderName;
|
||||||
|
|
@ -691,7 +693,7 @@ class MailClient
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if ($onlyCache) {
|
if ($onlyCache) {
|
||||||
return [];
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->oImapClient->FolderExamine($sFolderName);
|
$this->oImapClient->FolderExamine($sFolderName);
|
||||||
|
|
@ -749,9 +751,14 @@ class MailClient
|
||||||
*/
|
*/
|
||||||
public function MessageList(MessageListParams $oParams) : MessageCollection
|
public function MessageList(MessageListParams $oParams) : MessageCollection
|
||||||
{
|
{
|
||||||
if (0 > $oParams->iOffset || 0 > $oParams->iLimit || 999 < $oParams->iLimit) {
|
if (0 > $oParams->iOffset || 0 > $oParams->iLimit) {
|
||||||
throw new \ValueError;
|
throw new \ValueError;
|
||||||
}
|
}
|
||||||
|
if (10 > $oParams->iLimit) {
|
||||||
|
$oParams->iLimit = 10;
|
||||||
|
} else if (999 < $oParams->iLimit) {
|
||||||
|
$oParams->iLimit = 50;
|
||||||
|
}
|
||||||
|
|
||||||
$sSearch = \trim($oParams->sSearch);
|
$sSearch = \trim($oParams->sSearch);
|
||||||
|
|
||||||
|
|
@ -789,7 +796,7 @@ class MailClient
|
||||||
|
|
||||||
$aAllThreads = [];
|
$aAllThreads = [];
|
||||||
$aUnseenUIDs = [];
|
$aUnseenUIDs = [];
|
||||||
$aUids = [];
|
$aUids = null;
|
||||||
|
|
||||||
$message_list_limit = $this->oImapClient->Settings->message_list_limit;
|
$message_list_limit = $this->oImapClient->Settings->message_list_limit;
|
||||||
if (100 > $message_list_limit || $message_list_limit > $oInfo->MESSAGES) {
|
if (100 > $message_list_limit || $message_list_limit > $oInfo->MESSAGES) {
|
||||||
|
|
@ -802,7 +809,7 @@ class MailClient
|
||||||
$oAllParams->oSequenceSet = null;
|
$oAllParams->oSequenceSet = null;
|
||||||
if ($message_list_limit && !$oParams->iThreadUid && $oParams->oCacher && $oParams->oCacher->IsInited()) {
|
if ($message_list_limit && !$oParams->iThreadUid && $oParams->oCacher && $oParams->oCacher->IsInited()) {
|
||||||
$aUids = $this->GetUids($oAllParams, $oInfo, true);
|
$aUids = $this->GetUids($oAllParams, $oInfo, true);
|
||||||
if ($aUids) {
|
if (null !== $aUids) {
|
||||||
$message_list_limit = 0;
|
$message_list_limit = 0;
|
||||||
$oMessageCollection->Sort = $oAllParams->sSort;
|
$oMessageCollection->Sort = $oAllParams->sSort;
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -893,10 +900,12 @@ class MailClient
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$oMessageCollection->totalEmails = \count($aUids);
|
if (\is_array($aUids)) {
|
||||||
if ($oMessageCollection->totalEmails) {
|
$oMessageCollection->totalEmails = \count($aUids);
|
||||||
$aUids = \array_slice($aUids, $oParams->iOffset, $oParams->iLimit);
|
if ($oMessageCollection->totalEmails) {
|
||||||
$this->MessageListByRequestIndexOrUids($oMessageCollection, new SequenceSet($aUids), $aAllThreads, $aUnseenUIDs);
|
$aUids = \array_slice($aUids, $oParams->iOffset, $oParams->iLimit);
|
||||||
|
$this->MessageListByRequestIndexOrUids($oMessageCollection, new SequenceSet($aUids), $aAllThreads, $aUnseenUIDs);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return $oMessageCollection;
|
return $oMessageCollection;
|
||||||
|
|
|
||||||
|
|
@ -401,7 +401,7 @@ class Client
|
||||||
* @param int $form_content_type HTTP form content type to use
|
* @param int $form_content_type HTTP form content type to use
|
||||||
* @return array
|
* @return array
|
||||||
*/
|
*/
|
||||||
private function executeRequest($url, $parameters = array(), $http_method = self::HTTP_METHOD_GET, array $http_headers = null, $form_content_type = self::HTTP_FORM_CONTENT_TYPE_MULTIPART)
|
private function executeRequest($url, $parameters = array(), $http_method = self::HTTP_METHOD_GET, ?array $http_headers = null, $form_content_type = self::HTTP_FORM_CONTENT_TYPE_MULTIPART)
|
||||||
{
|
{
|
||||||
$curl_options = array(
|
$curl_options = array(
|
||||||
CURLOPT_RETURNTRANSFER => true,
|
CURLOPT_RETURNTRANSFER => true,
|
||||||
|
|
|
||||||
|
|
@ -595,6 +595,7 @@ class Actions
|
||||||
'listGrouped' => $oConfig->Get('defaults', 'mail_list_grouped', false),
|
'listGrouped' => $oConfig->Get('defaults', 'mail_list_grouped', false),
|
||||||
'MessagesPerPage' => \max(10, \intval($oConfig->Get('webmail', 'messages_per_page', 25)) ?: 25),
|
'MessagesPerPage' => \max(10, \intval($oConfig->Get('webmail', 'messages_per_page', 25)) ?: 25),
|
||||||
'messageNewWindow' => false,
|
'messageNewWindow' => false,
|
||||||
|
'markdown' => false,
|
||||||
'messageReadAuto' => true, // (bool) $oConfig->Get('webmail', 'message_read_auto', true),
|
'messageReadAuto' => true, // (bool) $oConfig->Get('webmail', 'message_read_auto', true),
|
||||||
'MessageReadDelay' => (int) $oConfig->Get('webmail', 'message_read_delay', 5),
|
'MessageReadDelay' => (int) $oConfig->Get('webmail', 'message_read_delay', 5),
|
||||||
'MsgDefaultAction' => (int) $oConfig->Get('defaults', 'msg_default_action', 1),
|
'MsgDefaultAction' => (int) $oConfig->Get('defaults', 'msg_default_action', 1),
|
||||||
|
|
@ -690,7 +691,8 @@ class Actions
|
||||||
$aResult['listGrouped'] = (bool)$oSettings->GetConf('listGrouped', $aResult['listGrouped']);
|
$aResult['listGrouped'] = (bool)$oSettings->GetConf('listGrouped', $aResult['listGrouped']);
|
||||||
$aResult['ContactsAutosave'] = (bool)$oSettings->GetConf('ContactsAutosave', $aResult['ContactsAutosave']);
|
$aResult['ContactsAutosave'] = (bool)$oSettings->GetConf('ContactsAutosave', $aResult['ContactsAutosave']);
|
||||||
$aResult['MessagesPerPage'] = \max(10, \intval($oSettings->GetConf('MessagesPerPage', $aResult['MessagesPerPage']) ?: $aResult['MessagesPerPage']));
|
$aResult['MessagesPerPage'] = \max(10, \intval($oSettings->GetConf('MessagesPerPage', $aResult['MessagesPerPage']) ?: $aResult['MessagesPerPage']));
|
||||||
$aResult['messageNewWindow'] = (int)$oSettings->GetConf('messageNewWindow', $aResult['messageNewWindow']);
|
$aResult['messageNewWindow'] = (bool)$oSettings->GetConf('messageNewWindow', $aResult['messageNewWindow']);
|
||||||
|
$aResult['markdown'] = (bool)$oSettings->GetConf('markdown', $aResult['markdown']);
|
||||||
$aResult['messageReadAuto'] = (int)$oSettings->GetConf('messageReadAuto', $aResult['messageReadAuto']);
|
$aResult['messageReadAuto'] = (int)$oSettings->GetConf('messageReadAuto', $aResult['messageReadAuto']);
|
||||||
$aResult['MessageReadDelay'] = (int)$oSettings->GetConf('MessageReadDelay', $aResult['MessageReadDelay']);
|
$aResult['MessageReadDelay'] = (int)$oSettings->GetConf('MessageReadDelay', $aResult['MessageReadDelay']);
|
||||||
$aResult['MsgDefaultAction'] = (int)$oSettings->GetConf('MsgDefaultAction', $aResult['MsgDefaultAction']);
|
$aResult['MsgDefaultAction'] = (int)$oSettings->GetConf('MsgDefaultAction', $aResult['MsgDefaultAction']);
|
||||||
|
|
|
||||||
|
|
@ -69,7 +69,20 @@ trait AdminExtensions
|
||||||
'id' => $sId,
|
'id' => $sId,
|
||||||
'name' => $oPlugin->Name(),
|
'name' => $oPlugin->Name(),
|
||||||
'readme' => $oPlugin->Description(),
|
'readme' => $oPlugin->Description(),
|
||||||
'config' => array()
|
'config' => array(),
|
||||||
|
|
||||||
|
'author' => $oPlugin::AUTHOR,
|
||||||
|
'url' => $oPlugin::URL,
|
||||||
|
'version' => $oPlugin::VERSION,
|
||||||
|
'released' => $oPlugin::RELEASE
|
||||||
|
/*
|
||||||
|
$oPlugin::NAME
|
||||||
|
$oPlugin::REQUIRED
|
||||||
|
$oPlugin::DEPRECATED
|
||||||
|
$oPlugin::CATEGORY
|
||||||
|
$oPlugin::LICENSE
|
||||||
|
$oPlugin::DESCRIPTION
|
||||||
|
*/
|
||||||
);
|
);
|
||||||
|
|
||||||
$aMap = $oPlugin->ConfigMap();
|
$aMap = $oPlugin->ConfigMap();
|
||||||
|
|
|
||||||
|
|
@ -198,6 +198,7 @@ trait User
|
||||||
$this->setSettingsFromParams($oSettings, 'MessageReadDelay', 'int');
|
$this->setSettingsFromParams($oSettings, 'MessageReadDelay', 'int');
|
||||||
$this->setSettingsFromParams($oSettings, 'MsgDefaultAction', 'int');
|
$this->setSettingsFromParams($oSettings, 'MsgDefaultAction', 'int');
|
||||||
$this->setSettingsFromParams($oSettings, 'showNextMessage', 'bool');
|
$this->setSettingsFromParams($oSettings, 'showNextMessage', 'bool');
|
||||||
|
$this->setSettingsFromParams($oSettings, 'markdown', 'bool');
|
||||||
|
|
||||||
$this->setSettingsFromParams($oSettings, 'Resizer4Width', 'int');
|
$this->setSettingsFromParams($oSettings, 'Resizer4Width', 'int');
|
||||||
$this->setSettingsFromParams($oSettings, 'Resizer5Width', 'int');
|
$this->setSettingsFromParams($oSettings, 'Resizer5Width', 'int');
|
||||||
|
|
|
||||||
|
|
@ -439,7 +439,7 @@ trait UserAuth
|
||||||
/**
|
/**
|
||||||
* @throws \RainLoop\Exceptions\ClientException
|
* @throws \RainLoop\Exceptions\ClientException
|
||||||
*/
|
*/
|
||||||
protected function imapConnect(Account $oAccount, bool $bAuthLog = false, \MailSo\Imap\ImapClient $oImapClient = null): void
|
protected function imapConnect(Account $oAccount, bool $bAuthLog = false, ?\MailSo\Imap\ImapClient $oImapClient = null): void
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
if (!$oImapClient) {
|
if (!$oImapClient) {
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,7 @@ abstract class Api
|
||||||
return $oConfig;
|
return $oConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function getCSP(string $sScriptNonce = null) : \SnappyMail\HTTP\CSP
|
public static function getCSP(?string $sScriptNonce = null) : \SnappyMail\HTTP\CSP
|
||||||
{
|
{
|
||||||
$oConfig = static::Config();
|
$oConfig = static::Config();
|
||||||
$CSP = new \SnappyMail\HTTP\CSP(\trim($oConfig->Get('security', 'content_security_policy', '')));
|
$CSP = new \SnappyMail\HTTP\CSP(\trim($oConfig->Get('security', 'content_security_policy', '')));
|
||||||
|
|
|
||||||
|
|
@ -235,6 +235,7 @@ Warning: only enable when server does not do this, else double compression error
|
||||||
'admin_login' => array('admin', 'Login and password for web admin panel'),
|
'admin_login' => array('admin', 'Login and password for web admin panel'),
|
||||||
'admin_password' => array(''),
|
'admin_password' => array(''),
|
||||||
'admin_totp' => array(''),
|
'admin_totp' => array(''),
|
||||||
|
'insecure_cryptkey' => array(false, 'Use email address instead of login password for encrypting sensitive data (like account passwords)'),
|
||||||
|
|
||||||
'force_https' => array(false),
|
'force_https' => array(false),
|
||||||
'hide_x_mailer_header' => array(true),
|
'hide_x_mailer_header' => array(true),
|
||||||
|
|
|
||||||
|
|
@ -144,28 +144,34 @@ abstract class Account implements \JsonSerializable
|
||||||
{
|
{
|
||||||
$oAccount = null;
|
$oAccount = null;
|
||||||
$aAccountHash = static::convertArray($aAccountHash);
|
$aAccountHash = static::convertArray($aAccountHash);
|
||||||
if (!empty($aAccountHash['email']) && !empty($aAccountHash['login']) && !empty($aAccountHash['pass'])) {
|
try {
|
||||||
try {
|
/*
|
||||||
$oDomain = $oActions->DomainProvider()->getByEmailAddress($aAccountHash['email']);
|
if (empty($aAccountHash['email'])) {
|
||||||
if ($oDomain) {
|
throw new ClientException(Notifications::InvalidToken, null, 'TokenArray missing email');
|
||||||
// $aAccountHash['email'] = $oDomain->ImapSettings()->fixUsername($aAccountHash['email'], false);
|
|
||||||
// $aAccountHash['login'] = $oDomain->ImapSettings()->fixUsername($aAccountHash['login']);
|
|
||||||
$oAccount = new static;
|
|
||||||
$oAccount->sEmail = \SnappyMail\IDN::emailToAscii($aAccountHash['email']);
|
|
||||||
$oAccount->sImapUser = \SnappyMail\IDN::emailToAscii($aAccountHash['login']);
|
|
||||||
$oAccount->setImapPass(new SensitiveString($aAccountHash['pass']));
|
|
||||||
$oAccount->oDomain = $oDomain;
|
|
||||||
$oActions->Plugins()->RunHook('filter.account', array($oAccount));
|
|
||||||
if ($bThrowExceptionOnFalse && !$oAccount) {
|
|
||||||
throw new ClientException(Notifications::AccountFilterError);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (\Throwable $e) {
|
|
||||||
if ($bThrowExceptionOnFalse) {
|
|
||||||
throw $e;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if ($oAccount) {
|
if (empty($aAccountHash['login'])) {
|
||||||
|
throw new ClientException(Notifications::InvalidToken, null, 'TokenArray missing login');
|
||||||
|
}
|
||||||
|
if (empty($aAccountHash['pass'])) {
|
||||||
|
throw new ClientException(Notifications::InvalidToken, null, 'TokenArray missing pass');
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
if (empty($aAccountHash['email']) || empty($aAccountHash['login']) || empty($aAccountHash['pass'])) {
|
||||||
|
throw new \RuntimeException("Invalid TokenArray");
|
||||||
|
}
|
||||||
|
$oDomain = $oActions->DomainProvider()->getByEmailAddress($aAccountHash['email']);
|
||||||
|
if ($oDomain) {
|
||||||
|
// $aAccountHash['email'] = $oDomain->ImapSettings()->fixUsername($aAccountHash['email'], false);
|
||||||
|
// $aAccountHash['login'] = $oDomain->ImapSettings()->fixUsername($aAccountHash['login']);
|
||||||
|
$oAccount = new static;
|
||||||
|
$oAccount->sEmail = \SnappyMail\IDN::emailToAscii($aAccountHash['email']);
|
||||||
|
$oAccount->sImapUser = \SnappyMail\IDN::emailToAscii($aAccountHash['login']);
|
||||||
|
$oAccount->setImapPass(new SensitiveString($aAccountHash['pass']));
|
||||||
|
$oAccount->oDomain = $oDomain;
|
||||||
|
$oActions->Plugins()->RunHook('filter.account', array($oAccount));
|
||||||
|
if (!$oAccount) {
|
||||||
|
throw new ClientException(Notifications::AccountFilterError);
|
||||||
|
}
|
||||||
if (isset($aAccountHash['name'])) {
|
if (isset($aAccountHash['name'])) {
|
||||||
$oAccount->sName = $aAccountHash['name'];
|
$oAccount->sName = $aAccountHash['name'];
|
||||||
}
|
}
|
||||||
|
|
@ -177,6 +183,11 @@ abstract class Account implements \JsonSerializable
|
||||||
$oAccount->setSmtpPass(new SensitiveString($aAccountHash['smtp']['pass']));
|
$oAccount->setSmtpPass(new SensitiveString($aAccountHash['smtp']['pass']));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
\SnappyMail\Log::debug('ACCOUNT', $e->getMessage());
|
||||||
|
if ($bThrowExceptionOnFalse) {
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return $oAccount;
|
return $oAccount;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,10 @@ class MainAccount extends Account
|
||||||
if (!$sKey) {
|
if (!$sKey) {
|
||||||
throw new ClientException(Notifications::CryptKeyError);
|
throw new ClientException(Notifications::CryptKeyError);
|
||||||
}
|
}
|
||||||
$sKey = \SnappyMail\Crypt::EncryptToJSON($sKey, $this->IncPassword());
|
$sPass = \RainLoop\Api::Config()->Get('security', 'insecure_cryptkey', false)
|
||||||
|
? $this->Email()
|
||||||
|
: $this->ImapPass();
|
||||||
|
$sKey = \SnappyMail\Crypt::EncryptToJSON($sKey, $sPass);
|
||||||
if ($sKey) {
|
if ($sKey) {
|
||||||
$this->sCryptKey = null;
|
$this->sCryptKey = null;
|
||||||
if (\RainLoop\Api::Actions()->StorageProvider()->Put($this, StorageType::ROOT, '.cryptkey', $sKey)) {
|
if (\RainLoop\Api::Actions()->StorageProvider()->Put($this, StorageType::ROOT, '.cryptkey', $sKey)) {
|
||||||
|
|
@ -39,14 +42,17 @@ class MainAccount extends Account
|
||||||
// can use the old password to re-seal the cryptkey
|
// can use the old password to re-seal the cryptkey
|
||||||
$oStorage = \RainLoop\Api::Actions()->StorageProvider();
|
$oStorage = \RainLoop\Api::Actions()->StorageProvider();
|
||||||
$sKey = $oStorage->Get($this, StorageType::ROOT, '.cryptkey');
|
$sKey = $oStorage->Get($this, StorageType::ROOT, '.cryptkey');
|
||||||
|
$sPass = \RainLoop\Api::Config()->Get('security', 'insecure_cryptkey', false)
|
||||||
|
? $this->Email()
|
||||||
|
: $this->ImapPass();
|
||||||
if (!$sKey) {
|
if (!$sKey) {
|
||||||
$sKey = \SnappyMail\Crypt::EncryptToJSON(
|
$sKey = \SnappyMail\Crypt::EncryptToJSON(
|
||||||
\sha1($this->IncPassword() . APP_SALT),
|
\sha1($this->ImapPass() . APP_SALT),
|
||||||
$this->IncPassword()
|
$sPass
|
||||||
);
|
);
|
||||||
$oStorage->Put($this, StorageType::ROOT, '.cryptkey', $sKey);
|
$oStorage->Put($this, StorageType::ROOT, '.cryptkey', $sKey);
|
||||||
}
|
}
|
||||||
$sKey = \SnappyMail\Crypt::DecryptFromJSON($sKey, $this->IncPassword());
|
$sKey = \SnappyMail\Crypt::DecryptFromJSON($sKey, $sPass);
|
||||||
if (!$sKey) {
|
if (!$sKey) {
|
||||||
throw new ClientException(Notifications::CryptKeyError);
|
throw new ClientException(Notifications::CryptKeyError);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ abstract class AbstractPlugin
|
||||||
VERSION = '0.0',
|
VERSION = '0.0',
|
||||||
RELEASE = '2020-11-01',
|
RELEASE = '2020-11-01',
|
||||||
REQUIRED = '2.0.0',
|
REQUIRED = '2.0.0',
|
||||||
|
// DEPRECATED = '',
|
||||||
CATEGORY = 'General',
|
CATEGORY = 'General',
|
||||||
LICENSE = 'AGPL v3',
|
LICENSE = 'AGPL v3',
|
||||||
DESCRIPTION = '';
|
DESCRIPTION = '';
|
||||||
|
|
|
||||||
|
|
@ -300,6 +300,30 @@ class Manager
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
private static function getCallableName(callable $callable) {
|
||||||
|
if (\is_string($callable)) {
|
||||||
|
if (\str_contains($callable, '::')) {
|
||||||
|
return '[static] ' . $callable;
|
||||||
|
}
|
||||||
|
return '[function] ' . $callable;
|
||||||
|
}
|
||||||
|
if (\is_array($callable)) {
|
||||||
|
if (\is_object($callable[0])) {
|
||||||
|
return '[method] ' . \get_class($callable[0]) . '->' . $callable[1];
|
||||||
|
}
|
||||||
|
return '[static] ' . $callable[0] . '::' . $callable[1];
|
||||||
|
}
|
||||||
|
if ($callable instanceof \Closure) {
|
||||||
|
return '[closure]';
|
||||||
|
}
|
||||||
|
if (\is_object($callable)) {
|
||||||
|
return '[invokable] ' . \get_class($callable);
|
||||||
|
}
|
||||||
|
return '[unknown]';
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
public function RunHook(string $sHookName, array $aArg = array(), bool $bLogHook = true) : self
|
public function RunHook(string $sHookName, array $aArg = array(), bool $bLogHook = true) : self
|
||||||
{
|
{
|
||||||
if ($this->bIsEnabled) {
|
if ($this->bIsEnabled) {
|
||||||
|
|
@ -307,9 +331,13 @@ class Manager
|
||||||
if (isset($this->aHooks[$sHookName])) {
|
if (isset($this->aHooks[$sHookName])) {
|
||||||
if ($bLogHook) {
|
if ($bLogHook) {
|
||||||
$this->WriteLog('Hook: '.$sHookName, \LOG_INFO);
|
$this->WriteLog('Hook: '.$sHookName, \LOG_INFO);
|
||||||
|
// $this->WriteLog('Hooks: '.\implode(',', \array_map('self::getCallableName', $this->aHooks[$sHookName])), \LOG_DEBUG);
|
||||||
}
|
}
|
||||||
foreach ($this->aHooks[$sHookName] as $mCallback) {
|
foreach ($this->aHooks[$sHookName] as $mCallback) try {
|
||||||
$mCallback(...$aArg);
|
$mCallback(...$aArg);
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
// $this->WriteLog("Hook {$sHookName} {$e->getMessage()}\n".static::getCallableName($mCallback), \LOG_ERR);
|
||||||
|
throw $e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@
|
||||||
|
|
||||||
namespace RainLoop\Providers;
|
namespace RainLoop\Providers;
|
||||||
|
|
||||||
|
use RainLoop\Exceptions\ClientException;
|
||||||
|
|
||||||
class Domain extends AbstractProvider
|
class Domain extends AbstractProvider
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
|
|
@ -35,7 +37,7 @@ class Domain extends AbstractProvider
|
||||||
public function SaveAlias(string $sName, string $sAlias) : bool
|
public function SaveAlias(string $sName, string $sAlias) : bool
|
||||||
{
|
{
|
||||||
if ($this->Load($sName, false, false)) {
|
if ($this->Load($sName, false, false)) {
|
||||||
throw new \RainLoop\Exceptions\ClientException(\RainLoop\Notifications::DomainAlreadyExists);
|
throw new ClientException(\RainLoop\Notifications::DomainAlreadyExists);
|
||||||
}
|
}
|
||||||
return $this->oDriver->SaveAlias($sName, $sAlias);
|
return $this->oDriver->SaveAlias($sName, $sAlias);
|
||||||
}
|
}
|
||||||
|
|
@ -55,7 +57,7 @@ class Domain extends AbstractProvider
|
||||||
return $this->oDriver->GetList($bIncludeAliases);
|
return $this->oDriver->GetList($bIncludeAliases);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function LoadOrCreateNewFromAction(\RainLoop\Actions $oActions, string $sNameForTest = null) : ?\RainLoop\Model\Domain
|
public function LoadOrCreateNewFromAction(\RainLoop\Actions $oActions, ?string $sNameForTest = null) : ?\RainLoop\Model\Domain
|
||||||
{
|
{
|
||||||
$sName = \mb_strtolower((string) $oActions->GetActionParam('name', ''));
|
$sName = \mb_strtolower((string) $oActions->GetActionParam('name', ''));
|
||||||
if (\strlen($sName) && $sNameForTest && !\str_contains($sName, '*')) {
|
if (\strlen($sName) && $sNameForTest && !\str_contains($sName, '*')) {
|
||||||
|
|
@ -63,7 +65,7 @@ class Domain extends AbstractProvider
|
||||||
}
|
}
|
||||||
if (\strlen($sName) || $sNameForTest) {
|
if (\strlen($sName) || $sNameForTest) {
|
||||||
if (!$sNameForTest && !empty($oActions->GetActionParam('create', 0)) && $this->Load($sName)) {
|
if (!$sNameForTest && !empty($oActions->GetActionParam('create', 0)) && $this->Load($sName)) {
|
||||||
throw new \RainLoop\Exceptions\ClientException(\RainLoop\Notifications::DomainAlreadyExists);
|
throw new ClientException(\RainLoop\Notifications::DomainAlreadyExists);
|
||||||
}
|
}
|
||||||
return \RainLoop\Model\Domain::fromArray($sNameForTest ?: $sName, [
|
return \RainLoop\Model\Domain::fromArray($sNameForTest ?: $sName, [
|
||||||
'IMAP' => $oActions->GetActionParam('IMAP'),
|
'IMAP' => $oActions->GetActionParam('IMAP'),
|
||||||
|
|
|
||||||
|
|
@ -251,7 +251,7 @@ abstract class Service
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static function setCSP(string $sScriptNonce = null) : void
|
private static function setCSP(?string $sScriptNonce = null) : void
|
||||||
{
|
{
|
||||||
Api::getCSP($sScriptNonce)->setHeaders();
|
Api::getCSP($sScriptNonce)->setHeaders();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -100,14 +100,14 @@ class ServiceActions
|
||||||
if ($_SERVER['HTTP_X_SM_TOKEN'] !== $token) {
|
if ($_SERVER['HTTP_X_SM_TOKEN'] !== $token) {
|
||||||
$oAccount = $this->oActions->getAccountFromToken(false);
|
$oAccount = $this->oActions->getAccountFromToken(false);
|
||||||
$sEmail = $oAccount ? $oAccount->Email() : 'guest';
|
$sEmail = $oAccount ? $oAccount->Email() : 'guest';
|
||||||
$this->oActions->logWrite("{$_SERVER['HTTP_X_SM_TOKEN']} !== {$token} for {$sEmail}", \LOG_ERROR, 'Token');
|
$this->oActions->logWrite("{$_SERVER['HTTP_X_SM_TOKEN']} !== {$token} for {$sEmail}", \LOG_ERR, 'Token');
|
||||||
throw new Exceptions\ClientException(Notifications::InvalidToken, null, 'HTTP Token mismatch');
|
throw new Exceptions\ClientException(Notifications::InvalidToken, null, 'HTTP Token mismatch');
|
||||||
}
|
}
|
||||||
} else if ($this->oHttp->IsPost()) {
|
} else if ($this->oHttp->IsPost()) {
|
||||||
if (empty($_POST['XToken']) || $_POST['XToken'] !== $token) {
|
if (empty($_POST['XToken']) || $_POST['XToken'] !== $token) {
|
||||||
$oAccount = $this->oActions->getAccountFromToken(false);
|
$oAccount = $this->oActions->getAccountFromToken(false);
|
||||||
$sEmail = $oAccount ? $oAccount->Email() : 'guest';
|
$sEmail = $oAccount ? $oAccount->Email() : 'guest';
|
||||||
$this->oActions->logWrite("{$_POST['XToken']} !== {$token} for {$sEmail}", \LOG_ERROR, 'XToken');
|
$this->oActions->logWrite("{$_POST['XToken']} !== {$token} for {$sEmail}", \LOG_ERR, 'XToken');
|
||||||
throw new Exceptions\ClientException(Notifications::InvalidToken, null, 'XToken mismatch');
|
throw new Exceptions\ClientException(Notifications::InvalidToken, null, 'XToken mismatch');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -102,7 +102,7 @@ class Utils
|
||||||
public static function ClearHtmlOutput(string $sHtml) : string
|
public static function ClearHtmlOutput(string $sHtml) : string
|
||||||
{
|
{
|
||||||
// return $sHtml;
|
// return $sHtml;
|
||||||
return \preg_replace('/>\\s+</', '><', \preg_replace(
|
return \preg_replace('/>\\s+</', '> <', \preg_replace(
|
||||||
['@\\s*/>@', '/\\s* /i', '/ \\s*/i', '/[\\r\\n\\t]+/'],
|
['@\\s*/>@', '/\\s* /i', '/ \\s*/i', '/[\\r\\n\\t]+/'],
|
||||||
['>', "\xC2\xA0", "\xC2\xA0", ' '],
|
['>', "\xC2\xA0", "\xC2\xA0", ' '],
|
||||||
\trim($sHtml)
|
\trim($sHtml)
|
||||||
|
|
|
||||||
|
|
@ -60,7 +60,7 @@ abstract class Crypt
|
||||||
|
|
||||||
public static function Decrypt(array $data,
|
public static function Decrypt(array $data,
|
||||||
#[\SensitiveParameter]
|
#[\SensitiveParameter]
|
||||||
string $key = null
|
?string $key = null
|
||||||
) /* : mixed */
|
) /* : mixed */
|
||||||
{
|
{
|
||||||
if (3 === \count($data) && isset($data[0], $data[1], $data[2]) && \strlen($data[0])) {
|
if (3 === \count($data) && isset($data[0], $data[1], $data[2]) && \strlen($data[0])) {
|
||||||
|
|
@ -85,7 +85,7 @@ abstract class Crypt
|
||||||
|
|
||||||
public static function DecryptFromJSON(string $data,
|
public static function DecryptFromJSON(string $data,
|
||||||
#[\SensitiveParameter]
|
#[\SensitiveParameter]
|
||||||
string $key = null
|
?string $key = null
|
||||||
) /* : mixed */
|
) /* : mixed */
|
||||||
{
|
{
|
||||||
$data = static::jsonDecode($data);
|
$data = static::jsonDecode($data);
|
||||||
|
|
@ -98,7 +98,7 @@ abstract class Crypt
|
||||||
|
|
||||||
public static function DecryptUrlSafe(string $data,
|
public static function DecryptUrlSafe(string $data,
|
||||||
#[\SensitiveParameter]
|
#[\SensitiveParameter]
|
||||||
string $key = null
|
?string $key = null
|
||||||
) /* : mixed */
|
) /* : mixed */
|
||||||
{
|
{
|
||||||
$data = \explode('.', $data);
|
$data = \explode('.', $data);
|
||||||
|
|
@ -113,7 +113,7 @@ abstract class Crypt
|
||||||
#[\SensitiveParameter]
|
#[\SensitiveParameter]
|
||||||
$data,
|
$data,
|
||||||
#[\SensitiveParameter]
|
#[\SensitiveParameter]
|
||||||
string $key = null
|
?string $key = null
|
||||||
) : array
|
) : array
|
||||||
{
|
{
|
||||||
$data = \json_encode($data);
|
$data = \json_encode($data);
|
||||||
|
|
@ -151,7 +151,7 @@ abstract class Crypt
|
||||||
#[\SensitiveParameter]
|
#[\SensitiveParameter]
|
||||||
$data,
|
$data,
|
||||||
#[\SensitiveParameter]
|
#[\SensitiveParameter]
|
||||||
string $key = null
|
?string $key = null
|
||||||
) : string
|
) : string
|
||||||
{
|
{
|
||||||
return \json_encode(\array_map('base64_encode', static::Encrypt($data, $key)));
|
return \json_encode(\array_map('base64_encode', static::Encrypt($data, $key)));
|
||||||
|
|
@ -161,7 +161,7 @@ abstract class Crypt
|
||||||
#[\SensitiveParameter]
|
#[\SensitiveParameter]
|
||||||
$data,
|
$data,
|
||||||
#[\SensitiveParameter]
|
#[\SensitiveParameter]
|
||||||
string $key = null
|
?string $key = null
|
||||||
) : string
|
) : string
|
||||||
{
|
{
|
||||||
return \implode('.', \array_map('MailSo\\Base\\Utils::UrlSafeBase64Encode', static::Encrypt($data, $key)));
|
return \implode('.', \array_map('MailSo\\Base\\Utils::UrlSafeBase64Encode', static::Encrypt($data, $key)));
|
||||||
|
|
@ -169,7 +169,7 @@ abstract class Crypt
|
||||||
|
|
||||||
public static function SodiumDecrypt(string $data, string $nonce,
|
public static function SodiumDecrypt(string $data, string $nonce,
|
||||||
#[\SensitiveParameter]
|
#[\SensitiveParameter]
|
||||||
string $key = null
|
?string $key = null
|
||||||
) /* : string|false */
|
) /* : string|false */
|
||||||
{
|
{
|
||||||
if (!\is_callable('sodium_crypto_aead_xchacha20poly1305_ietf_decrypt')) {
|
if (!\is_callable('sodium_crypto_aead_xchacha20poly1305_ietf_decrypt')) {
|
||||||
|
|
@ -188,7 +188,7 @@ abstract class Crypt
|
||||||
string $data,
|
string $data,
|
||||||
string $nonce,
|
string $nonce,
|
||||||
#[\SensitiveParameter]
|
#[\SensitiveParameter]
|
||||||
string $key = null
|
?string $key = null
|
||||||
) : string
|
) : string
|
||||||
{
|
{
|
||||||
if (!\is_callable('sodium_crypto_aead_xchacha20poly1305_ietf_encrypt')) {
|
if (!\is_callable('sodium_crypto_aead_xchacha20poly1305_ietf_encrypt')) {
|
||||||
|
|
@ -208,7 +208,7 @@ abstract class Crypt
|
||||||
|
|
||||||
public static function OpenSSLDecrypt(string $data, string $iv,
|
public static function OpenSSLDecrypt(string $data, string $iv,
|
||||||
#[\SensitiveParameter]
|
#[\SensitiveParameter]
|
||||||
string $key = null
|
?string $key = null
|
||||||
) /* : string|false */
|
) /* : string|false */
|
||||||
{
|
{
|
||||||
if (!$data || !$iv) {
|
if (!$data || !$iv) {
|
||||||
|
|
@ -235,7 +235,7 @@ abstract class Crypt
|
||||||
string $data,
|
string $data,
|
||||||
string $iv,
|
string $iv,
|
||||||
#[\SensitiveParameter]
|
#[\SensitiveParameter]
|
||||||
string $key = null
|
?string $key = null
|
||||||
) : string
|
) : string
|
||||||
{
|
{
|
||||||
if (!$data || !$iv) {
|
if (!$data || !$iv) {
|
||||||
|
|
@ -263,7 +263,7 @@ abstract class Crypt
|
||||||
|
|
||||||
public static function XxteaDecrypt(string $data, string $salt,
|
public static function XxteaDecrypt(string $data, string $salt,
|
||||||
#[\SensitiveParameter]
|
#[\SensitiveParameter]
|
||||||
string $key = null
|
?string $key = null
|
||||||
) /* : mixed */
|
) /* : mixed */
|
||||||
{
|
{
|
||||||
if (!$data || !$salt) {
|
if (!$data || !$salt) {
|
||||||
|
|
@ -280,7 +280,7 @@ abstract class Crypt
|
||||||
string $data,
|
string $data,
|
||||||
string $salt,
|
string $salt,
|
||||||
#[\SensitiveParameter]
|
#[\SensitiveParameter]
|
||||||
string $key = null
|
?string $key = null
|
||||||
) : string
|
) : string
|
||||||
{
|
{
|
||||||
if (!$data || !$salt) {
|
if (!$data || !$salt) {
|
||||||
|
|
|
||||||
|
|
@ -63,7 +63,7 @@ class Client
|
||||||
*
|
*
|
||||||
* If the specified url is relative, it will be expanded based on the base url.
|
* If the specified url is relative, it will be expanded based on the base url.
|
||||||
*/
|
*/
|
||||||
public function request(string $method, string $url = '', string $body = null, array $headers = array()) : \SnappyMail\HTTP\Response
|
public function request(string $method, string $url = '', ?string $body = null, array $headers = array()) : \SnappyMail\HTTP\Response
|
||||||
{
|
{
|
||||||
if (!\preg_match('@^(https?:)?//@', $url)) {
|
if (!\preg_match('@^(https?:)?//@', $url)) {
|
||||||
// If the url starts with a slash, we must calculate the url based off
|
// If the url starts with a slash, we must calculate the url based off
|
||||||
|
|
|
||||||
|
|
@ -66,7 +66,7 @@ class Exception extends \Exception
|
||||||
511 => 'Network Authentication Required',
|
511 => 'Network Authentication Required',
|
||||||
);
|
);
|
||||||
|
|
||||||
function __construct(string $message = "", int $code = 0, Response $response = null)
|
function __construct(string $message = "", int $code = 0, ?Response $response = null)
|
||||||
{
|
{
|
||||||
if ($response) {
|
if ($response) {
|
||||||
if (\in_array($code, array(301, 302, 303, 307))) {
|
if (\in_array($code, array(301, 302, 303, 307))) {
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ class Response
|
||||||
$headers = array(), # The headers returned from the final_uri
|
$headers = array(), # The headers returned from the final_uri
|
||||||
$body; # The body returned from the final_uri
|
$body; # The body returned from the final_uri
|
||||||
|
|
||||||
function __construct(string $request_uri, int $status = 0, array $headers = null, $body = null)
|
function __construct(string $request_uri, int $status = 0, ?array $headers = null, $body = null)
|
||||||
{
|
{
|
||||||
if ($headers) {
|
if ($headers) {
|
||||||
$name = null;
|
$name = null;
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ class Exif
|
||||||
ORIENTATION_RIGHTBOTTOM = 7,
|
ORIENTATION_RIGHTBOTTOM = 7,
|
||||||
ORIENTATION_LEFTBOTTOM = 8;
|
ORIENTATION_LEFTBOTTOM = 8;
|
||||||
*/
|
*/
|
||||||
public static function getImageOrientation(string &$data, array $image_info = null) : int
|
public static function getImageOrientation(string &$data, ?array $image_info = null) : int
|
||||||
{
|
{
|
||||||
$image_info = empty($image_info['mime']) ? \getimagesizefromstring($data) : $image_info;
|
$image_info = empty($image_info['mime']) ? \getimagesizefromstring($data) : $image_info;
|
||||||
if (!empty($image_info['mime']) && \IMG_JPG == $image_info[2] && \is_callable('exif_read_data')) {
|
if (!empty($image_info['mime']) && \IMG_JPG == $image_info[2] && \is_callable('exif_read_data')) {
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ abstract class JWT
|
||||||
* @param int $timestamp Allow the current timestamp to be specified. default time()
|
* @param int $timestamp Allow the current timestamp to be specified. default time()
|
||||||
* @param int $leeway When checking nbf (Not Before) Claim, iat (Issued At) Claim or expiration times, we want to provide some extra leeway time to account for clock skew.
|
* @param int $leeway When checking nbf (Not Before) Claim, iat (Issued At) Claim or expiration times, we want to provide some extra leeway time to account for clock skew.
|
||||||
*/
|
*/
|
||||||
public static function decode(string $jwt, $key, int $timestamp = null, int $leeway = 0) : object
|
public static function decode(string $jwt, $key, ?int $timestamp = null, int $leeway = 0) : object
|
||||||
{
|
{
|
||||||
$timestamp = $timestamp ?: \time();
|
$timestamp = $timestamp ?: \time();
|
||||||
|
|
||||||
|
|
|
||||||
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