mirror of
https://github.com/the-djmaze/snappymail.git
synced 2026-08-25 10:09:20 +03:00
Merge branch 'master' into dev/sync-nextcloud-addressbook
This commit is contained in:
commit
721a849254
122 changed files with 1816 additions and 1677 deletions
59
CHANGELOG.md
59
CHANGELOG.md
|
|
@ -1,3 +1,60 @@
|
|||
## 2.38.2 – 2024-10-09
|
||||
### Fixed
|
||||
- error '$index is not defined' in Settings > Accounts
|
||||
[#1797](https://github.com/the-djmaze/snappymail/issues/1797)
|
||||
|
||||
|
||||
## 2.38.1 – 2024-10-08
|
||||
|
||||
### Added
|
||||
- Admin - Extensions search filter
|
||||
- Options to unset current font family and size
|
||||
[#1726](https://github.com/the-djmaze/snappymail/issues/1726)
|
||||
- Option to allow insecure cryptkey
|
||||
[#1746](https://github.com/the-djmaze/snappymail/issues/1746)
|
||||
- Save vCard FN property
|
||||
[#1761](https://github.com/the-djmaze/snappymail/issues/1761)
|
||||
- Docker Nginx should listen on IPv6 addresses in addition to IPv4
|
||||
[#1770](https://github.com/the-djmaze/snappymail/issues/1770)
|
||||
- Full-screen Message View on Double-Click
|
||||
[#1787](https://github.com/the-djmaze/snappymail/issues/1787)
|
||||
|
||||
### Changed
|
||||
- Use the custom Squire as submodule vendors/squire2
|
||||
- Disallow noembed and noframes HTML elements
|
||||
- Keep 1 space between HTML elements in compressed templates
|
||||
- Squire: sanitizeToDOMFragment now uses cleanHTML()
|
||||
- Squire: improved handling of BR elements
|
||||
[#1389](https://github.com/the-djmaze/snappymail/issues/1389)
|
||||
- Nextcloud border-radius by @gnilebein
|
||||
[#1790](https://github.com/the-djmaze/snappymail/pull/1790)
|
||||
- Compose window sentFolder handling for
|
||||
[#1793](https://github.com/the-djmaze/snappymail/pull/1793)
|
||||
- Update Portuguese by @ner00
|
||||
- Update Polish by @tinola
|
||||
- Update French by @hguilbert
|
||||
- Update Portuguese (Brazil) by @mstolf
|
||||
|
||||
### Fixed
|
||||
- Composer dialog scroll got broken in v2.28
|
||||
- Composer dialog "from" triangle button wrong position due to font changes
|
||||
- Admin - Config `search` should be ko.observable() not ko.observableArray()
|
||||
- Squire: paste images
|
||||
[#1389](https://github.com/the-djmaze/snappymail/issues/1389)
|
||||
- Domain whitelist failures on login
|
||||
[#1706](https://github.com/the-djmaze/snappymail/issues/1706)
|
||||
- Sieve parse errors
|
||||
- Sieve support `index` was not optional for
|
||||
[#1709](https://github.com/the-djmaze/snappymail/issues/1709)
|
||||
- Pagination problem for large mailbox after 2.36.1
|
||||
[#1716](https://github.com/the-djmaze/snappymail/issues/1716)
|
||||
- Undefined constant "LOG_ERROR"
|
||||
[#1754](https://github.com/the-djmaze/snappymail/issues/1754)
|
||||
- Search Filter Capital "B" not working
|
||||
[#1780](https://github.com/the-djmaze/snappymail/issues/1780)
|
||||
- PHP 8.4: Implicitly nullable parameter declarations deprecated
|
||||
|
||||
|
||||
## 2.38.0 – 2024-09-16
|
||||
|
||||
### Added
|
||||
|
|
@ -247,7 +304,7 @@
|
|||
- CSS set min-width for .attachmentParent and .flagParent to line them up
|
||||
- cPanel use extension login-cpanel instead of login-remote
|
||||
- Improved login credentials handling
|
||||
- Speedup Knockout a bit
|
||||
- Speedup Knockout a bit and removed `with($context)` scope
|
||||
- Update Belarusian by @spoooyders
|
||||
- Update Chinese by @mayswind
|
||||
- Update French by @hguilbert
|
||||
|
|
|
|||
36
README.md
36
README.md
|
|
@ -66,7 +66,7 @@ This fork of RainLoop has the following changes:
|
|||
* CRLF => LF line endings
|
||||
* Embed boot.js and boot.css into index.html
|
||||
* Removal of old JavaScript code (things are native these days)
|
||||
* Added modified [Squire](https://github.com/neilj/Squire) HTML editor as replacement for CKEditor
|
||||
* Added modified [Squire](https://github.com/the-djmaze/Squire/tree/snappymail) HTML editor as replacement for CKEditor
|
||||
* Updated [Sabre/VObject](https://github.com/sabre-io/vobject)
|
||||
* Split Admin specific JavaScript code from User code
|
||||
* Split Sieve specific JavaScript code from User code
|
||||
|
|
@ -140,28 +140,28 @@ RainLoop 1.17 vs SnappyMail
|
|||
|
||||
|js/* |RainLoop |Snappy |
|
||||
|--------------- |--------: |--------: |
|
||||
|admin.js |2.170.153 | 84.203 |
|
||||
|app.js |4.207.787 | 446.887 |
|
||||
|admin.js |2.170.153 | 84.925 |
|
||||
|app.js |4.207.787 | 447.263 |
|
||||
|boot.js | 868.735 | 4.343 |
|
||||
|libs.js | 658.812 | 236.059 |
|
||||
|sieve.js | 0 | 84.571 |
|
||||
|libs.js | 658.812 | 233.728 |
|
||||
|sieve.js | 0 | 91.418 |
|
||||
|polyfills.js | 334.608 | 0 |
|
||||
|serviceworker.js | 0 | 285 |
|
||||
|TOTAL |8.240.095 | 856.348 |
|
||||
|TOTAL |8.240.095 | 861.962 |
|
||||
|
||||
|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 |
|
||||
|app.min.js | 515.367 | 201.917 |139.456 | 68.450 |110.485 | 58.321 |
|
||||
|admin.min.js | 256.831 | 41.719 | 73.606 | 14.022 | 60.877 | 12.567 |
|
||||
|app.min.js | 515.367 | 202.101 |139.456 | 68.505 |110.485 | 58.481 |
|
||||
|boot.min.js | 84.659 | 2.231 | 26.998 | 1.271 | 23.643 | 1.067 |
|
||||
|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 |
|
||||
|libs.min.js | 584.772 | 110.646 |180.901 | 39.518 |155.182 | 35.207 |
|
||||
|sieve.min.js | 0 | 45.504 | 0 | 11.131 | 0 | 9.917 |
|
||||
|polyfills.min.js | 32.837 | 0 | 11.406 | 0 | 10.175 | 0 |
|
||||
|TOTAL user |1.217.635 | 315.890 |358.761 |109.603 |299.485 | 94.972 |
|
||||
|TOTAL user+sieve |1.217.635 | 357.178 |358.761 |119.930 |299.485 |104.290 |
|
||||
|TOTAL admin | 959.099 | 155.229 |292.911 | 55.093 |249.877 | 49.144 |
|
||||
|TOTAL user |1.217.635 | 314.978 |358.761 |109.294 |299.485 | 94.755 |
|
||||
|TOTAL user+sieve |1.217.635 | 360.482 |358.761 |120.425 |299.485 |104.672 |
|
||||
|TOTAL admin | 959.099 | 154.596 |292.911 | 54.811 |249.877 | 48.841 |
|
||||
|
||||
For a user it is around 67% smaller and faster than traditional RainLoop.
|
||||
For a user it is around 66% smaller and faster than traditional RainLoop.
|
||||
|
||||
### CSS changes
|
||||
|
||||
|
|
@ -188,12 +188,12 @@ For a user it is around 67% smaller and faster than traditional RainLoop.
|
|||
|
||||
|css/* |RainLoop |Snappy |RL gzip |SM gzip |SM brotli |
|
||||
|------------ |-------: |------: |------: |------: |--------: |
|
||||
|app.css | 340.331 | 84.850 | 46.946 | 17.710 | 15.161 |
|
||||
|app.min.css | 274.947 | 68.192 | 39.647 | 15.602 | 13.626 |
|
||||
|app.css | 340.331 | 85.073 | 46.946 | 17.792 | 15.210 |
|
||||
|app.min.css | 274.947 | 68.272 | 39.647 | 15.615 | 13.636 |
|
||||
|boot.css | | 1.326 | | 664 | 545 |
|
||||
|boot.min.css | | 1.071 | | 590 | 474 |
|
||||
|admin.css | | 30.761 | | 7.038 | 6.118 |
|
||||
|admin.min.css | | 24.857 | | 6.360 | 5.598 |
|
||||
|admin.css | | 30.880 | | 7.045 | 6.127 |
|
||||
|admin.min.css | | 24.959 | | 6.368 | 5.615 |
|
||||
|
||||
### PGP
|
||||
RainLoop uses the old OpenPGP.js v2
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ const
|
|||
// Structural Elements:
|
||||
'blockquote','br','div','figcaption','figure','h1','h2','h3','h4','h5','h6','hgroup','hr','p','wbr',
|
||||
'article','aside','header','footer','main','section',
|
||||
'details','summary',
|
||||
'details','summary','nav',
|
||||
// List Elements
|
||||
'dd','dl','dt','li','ol','ul',
|
||||
// Text Formatting Elements
|
||||
|
|
@ -239,6 +239,8 @@ export const
|
|||
linkedData: []
|
||||
},
|
||||
|
||||
isMsg = !!msgId,
|
||||
|
||||
findAttachmentByCid = cId => oAttachments.findByCid(cId),
|
||||
findLocationByCid = cId => {
|
||||
const attachment = findAttachmentByCid(cId);
|
||||
|
|
@ -282,6 +284,8 @@ export const
|
|||
'abbr', 'scope',
|
||||
// td
|
||||
'colspan', 'rowspan', 'headers'
|
||||
// others
|
||||
//'class', 'id', 'target'
|
||||
];
|
||||
|
||||
if (SettingsUserStore.allowStyles()) {
|
||||
|
|
@ -332,7 +336,7 @@ export const
|
|||
tmpl.content.querySelectorAll(keepTagContent).forEach(oElement => replaceWithChildren(oElement));
|
||||
|
||||
tmpl.content.querySelectorAll(
|
||||
':not('+allowedTags+')'
|
||||
':not('+allowedTags+'),a:empty,span:empty'
|
||||
+ (0 < bqLevel ? ',' + (new Array(1 + bqLevel).fill('blockquote').join(' ')) : '')
|
||||
).forEach(oElement => oElement.remove());
|
||||
/* // Is this slower or faster?
|
||||
|
|
@ -481,71 +485,73 @@ export const
|
|||
*/
|
||||
|
||||
let skipStyle = false;
|
||||
value = delAttribute('src');
|
||||
if (value) {
|
||||
if ('IMG' === name) {
|
||||
oElement.loading = 'lazy';
|
||||
let attachment;
|
||||
if (value.startsWith('cid:'))
|
||||
{
|
||||
value = value.slice(4);
|
||||
setAttribute('data-x-src-cid', value);
|
||||
attachment = findAttachmentByCid(value);
|
||||
if (attachment?.download) {
|
||||
oElement.src = attachment.linkPreview();
|
||||
oElement.title += ' ('+attachment.fileName+')';
|
||||
attachment.isInline(true);
|
||||
attachment.isLinked(true);
|
||||
if (isMsg) {
|
||||
value = isMsg && delAttribute('src');
|
||||
if (value) {
|
||||
if ('IMG' === name) {
|
||||
oElement.loading = 'lazy';
|
||||
let attachment;
|
||||
if (value.startsWith('cid:'))
|
||||
{
|
||||
value = value.slice(4);
|
||||
setAttribute('data-x-src-cid', value);
|
||||
attachment = findAttachmentByCid(value);
|
||||
if (attachment?.download) {
|
||||
oElement.src = attachment.linkPreview();
|
||||
oElement.title += ' ('+attachment.fileName+')';
|
||||
attachment.isInline(true);
|
||||
attachment.isLinked(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if ((attachment = findLocationByCid(value)))
|
||||
{
|
||||
if (attachment.download) {
|
||||
oElement.src = attachment.linkPreview();
|
||||
attachment.isLinked(true);
|
||||
else if ((attachment = findLocationByCid(value)))
|
||||
{
|
||||
if (attachment.download) {
|
||||
oElement.src = attachment.linkPreview();
|
||||
attachment.isLinked(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (detectHiddenImages
|
||||
&& ((oStyle.maxHeight && 3 > pInt(oStyle.maxHeight)) // TODO: issue with 'in'
|
||||
|| (oStyle.maxWidth && 3 > pInt(oStyle.maxWidth)) // TODO: issue with 'in'
|
||||
|| (oStyle.width && 2 > pInt(oStyle.width))
|
||||
|| [
|
||||
'email.microsoftemail.com/open',
|
||||
'github.com/notifications/beacon/',
|
||||
'/track/open', // mandrillapp.com list-manage.com
|
||||
'google-analytics.com'
|
||||
].filter(uri => value.toLowerCase().includes(uri)).length
|
||||
)) {
|
||||
skipStyle = true;
|
||||
oStyle.display = 'none';
|
||||
// setAttribute('style', 'display:none');
|
||||
setAttribute('data-x-src-hidden', value);
|
||||
// result.tracking = true;
|
||||
}
|
||||
else if (httpre.test(value))
|
||||
{
|
||||
let src = stripTracking(value);
|
||||
if (src != value) {
|
||||
result.tracking = true;
|
||||
setAttribute('data-x-src-tracking', value);
|
||||
else if (detectHiddenImages
|
||||
&& ((oStyle.maxHeight && 3 > pInt(oStyle.maxHeight)) // TODO: issue with 'in'
|
||||
|| (oStyle.maxWidth && 3 > pInt(oStyle.maxWidth)) // TODO: issue with 'in'
|
||||
|| (oStyle.width && 2 > pInt(oStyle.width))
|
||||
|| [
|
||||
'email.microsoftemail.com/open',
|
||||
'github.com/notifications/beacon/',
|
||||
'/track/open', // mandrillapp.com list-manage.com
|
||||
'google-analytics.com'
|
||||
].filter(uri => value.toLowerCase().includes(uri)).length
|
||||
)) {
|
||||
skipStyle = true;
|
||||
oStyle.display = 'none';
|
||||
// setAttribute('style', 'display:none');
|
||||
setAttribute('data-x-src-hidden', value);
|
||||
// result.tracking = true;
|
||||
}
|
||||
else if (httpre.test(value))
|
||||
{
|
||||
let src = stripTracking(value);
|
||||
if (src != value) {
|
||||
result.tracking = true;
|
||||
setAttribute('data-x-src-tracking', value);
|
||||
}
|
||||
setAttribute('data-x-src', src);
|
||||
result.hasExternals = true;
|
||||
oElement.alt || (oElement.alt = src.replace(/^.+\/([^/?]+).*$/, '$1').slice(-20));
|
||||
}
|
||||
else if (value.startsWith('data:image/'))
|
||||
{
|
||||
oElement.src = value;
|
||||
}
|
||||
else
|
||||
{
|
||||
setAttribute('data-x-src-broken', value);
|
||||
}
|
||||
setAttribute('data-x-src', src);
|
||||
result.hasExternals = true;
|
||||
oElement.alt || (oElement.alt = src.replace(/^.+\/([^/?]+).*$/, '$1').slice(-20));
|
||||
}
|
||||
else if (value.startsWith('data:image/'))
|
||||
{
|
||||
oElement.src = value;
|
||||
}
|
||||
else
|
||||
{
|
||||
setAttribute('data-x-src-broken', value);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
setAttribute('data-x-src-broken', value);
|
||||
}
|
||||
}
|
||||
|
||||
if (hasAttribute('background')) {
|
||||
|
|
@ -631,7 +637,7 @@ export const
|
|||
}
|
||||
});
|
||||
|
||||
blockquoteSwitcher();
|
||||
isMsg && blockquoteSwitcher();
|
||||
|
||||
// return tmpl.content.firstChild;
|
||||
result.html = tmpl.innerHTML.trim();
|
||||
|
|
@ -819,6 +825,7 @@ export const
|
|||
};
|
||||
|
||||
rl.Utils = {
|
||||
cleanHtml: cleanHtml,
|
||||
htmlToPlain: htmlToPlain,
|
||||
plainToHtml: plainToHtml,
|
||||
htmlToMarkdown: htmlToMarkdown
|
||||
|
|
|
|||
67
dev/External/SquireUI.js
vendored
67
dev/External/SquireUI.js
vendored
|
|
@ -3,10 +3,6 @@
|
|||
(doc => {
|
||||
|
||||
const
|
||||
removeElements = 'HEAD,LINK,META,NOSCRIPT,SCRIPT,TEMPLATE,TITLE',
|
||||
allowedElements = 'A,B,BLOCKQUOTE,BR,DIV,FONT,H1,H2,H3,H4,H5,H6,HR,IMG,LI,OL,P,SPAN,STRONG,TABLE,TD,TH,TR,U,UL',
|
||||
allowedAttributes = 'abbr,align,background,bgcolor,border,cellpadding,cellspacing,class,color,colspan,dir,face,frame,height,href,hspace,id,lang,rowspan,rules,scope,size,src,style,target,type,usemap,valign,vspace,width'.split(','),
|
||||
|
||||
i18n = (str, def) => rl.i18n(str) || def,
|
||||
|
||||
ctrlKey = shortcuts.getMetaKey() + ' + ',
|
||||
|
|
@ -21,38 +17,17 @@ const
|
|||
|
||||
forEachObjectValue = (obj, fn) => Object.values(obj).forEach(fn),
|
||||
|
||||
getFragmentOfChildren = parent => {
|
||||
let frag = doc.createDocumentFragment();
|
||||
frag.append(...parent.childNodes);
|
||||
return frag;
|
||||
},
|
||||
|
||||
SquireDefaultConfig = {
|
||||
/*
|
||||
addLinks: true // allow_smart_html_links
|
||||
*/
|
||||
sanitizeToDOMFragment: (html, isPaste/*, squire*/) => {
|
||||
tpl.innerHTML = (html||'')
|
||||
html = (html||'')
|
||||
.replace(/<\/?(BODY|HTML)[^>]*>/gi,'')
|
||||
.replace(/<!--[^>]+-->/g,'')
|
||||
.replace(/<span[^>]*>\s*<\/span>/gi,'')
|
||||
.trim();
|
||||
tpl.querySelectorAll('a:empty,span:empty').forEach(el => el.remove());
|
||||
if (isPaste) {
|
||||
tpl.querySelectorAll(removeElements).forEach(el => el.remove());
|
||||
tpl.querySelectorAll('*').forEach(el => {
|
||||
if (!el.matches(allowedElements)) {
|
||||
el.replaceWith(getFragmentOfChildren(el));
|
||||
} else if (el.hasAttributes()) {
|
||||
[...el.attributes].forEach(attr => {
|
||||
let name = attr.name.toLowerCase();
|
||||
if (!allowedAttributes.includes(name)) {
|
||||
el.removeAttribute(name);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
tpl.innerHTML = isPaste ? rl.Utils.cleanHtml(html).html : html;
|
||||
return tpl.content;
|
||||
}
|
||||
};
|
||||
|
|
@ -411,6 +386,44 @@ class SquireUI
|
|||
changes.redo.input.disabled = !e.detail.canRedo;
|
||||
});
|
||||
|
||||
squire.addEventListener('pasteImage', e => {
|
||||
const items = e.detail.clipboardData.items;
|
||||
let l = items.length;
|
||||
while (l--) {
|
||||
const item = items[l];
|
||||
if (/^image\/(png|jpeg|webp)/.test(item.type)) {
|
||||
let reader = new FileReader();
|
||||
reader.onload = event => {
|
||||
let img = createElement("img"),
|
||||
canvas = createElement("canvas"),
|
||||
ctx = canvas.getContext('2d');
|
||||
img.onload = ()=>{
|
||||
ctx.drawImage(img, 0, 0);
|
||||
let width = img.width, height = img.height;
|
||||
if (width > height) {
|
||||
// Landscape
|
||||
if (width > 1024) {
|
||||
height = height * 1024 / width;
|
||||
width = 1024;
|
||||
}
|
||||
} else if (height > 1024) {
|
||||
// Portrait
|
||||
width = width * 1024 / height;
|
||||
height = 1024;
|
||||
}
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
ctx.drawImage(img, 0, 0, width, height);
|
||||
squire.insertHTML('<img alt="" style="width:100%;max-width:'+width+'px" src="'+canvas.toDataURL()+'">', true);
|
||||
};
|
||||
img.src = event.target.result;
|
||||
}
|
||||
reader.readAsDataURL(item.getAsFile());
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
actions.font.fontSize.input.selectedIndex = actions.font.fontSize.defaultValueIndex;
|
||||
|
||||
// squire.addEventListener('focus', () => shortcuts.off());
|
||||
|
|
|
|||
|
|
@ -44,6 +44,51 @@ export class UserSettingsGeneral extends AbstractViewSettings {
|
|||
this.desktopNotifications = NotificationUserStore.enabled;
|
||||
this.isDesktopNotificationAllowed = NotificationUserStore.allowed;
|
||||
|
||||
const i18nSort = s => i18n('MESSAGE_LIST/SORT_'+s);
|
||||
this.sortSupported = FolderUserStore.hasCapability('SORT');
|
||||
this.sortOptions = [
|
||||
{
|
||||
value: '',
|
||||
text: i18nSort('DATE_DESC')
|
||||
},
|
||||
{
|
||||
value: 'DATE',
|
||||
text: i18nSort('DATE_ASC')
|
||||
},
|
||||
{
|
||||
value: 'FROM',
|
||||
text: i18nSort('FROM_ASC')
|
||||
},
|
||||
{
|
||||
value: 'REVERSE FROM',
|
||||
text: i18nSort('FROM_DESC')
|
||||
},
|
||||
{
|
||||
value: 'REVERSE SIZE',
|
||||
text: i18nSort('SIZE_DESC')
|
||||
},
|
||||
{
|
||||
value: 'SIZE',
|
||||
text: i18nSort('SIZE_ASC')
|
||||
},
|
||||
{
|
||||
value: 'SUBJECT',
|
||||
text: i18nSort('SUBJECT_ASC')
|
||||
},
|
||||
{
|
||||
value: 'REVERSE SUBJECT',
|
||||
text: i18nSort('SUBJECT_DESC')
|
||||
},
|
||||
{
|
||||
value: 'REVERSE ARRIVAL',
|
||||
text: i18nSort('ARRIVAL_DESC')
|
||||
},
|
||||
{
|
||||
value: 'ARRIVAL',
|
||||
text: i18nSort('ARRIVAL_ASC')
|
||||
}
|
||||
];
|
||||
|
||||
this.threadsAllowed = AppUserStore.threadsAllowed;
|
||||
// 'THREAD=REFS', 'THREAD=REFERENCES', 'THREAD=ORDEREDSUBJECT'
|
||||
this.threadAlgorithms = ko.observableArray();
|
||||
|
|
@ -51,9 +96,8 @@ export class UserSettingsGeneral extends AbstractViewSettings {
|
|||
capa.startsWith('THREAD=') && this.threadAlgorithms.push(capa.slice(7))
|
||||
);
|
||||
this.threadAlgorithms.sort((a, b) => a.length - b.length);
|
||||
this.threadAlgorithm = SettingsUserStore.threadAlgorithm;
|
||||
|
||||
['useThreads', 'threadAlgorithm',
|
||||
['defaultSort', 'useThreads', 'threadAlgorithm',
|
||||
// These use addSetting()
|
||||
'layout', 'messageReadDelay', 'messagesPerPage', 'checkMailInterval',
|
||||
'editorDefaultType', 'editorWysiwyg', 'msgDefaultAction', 'maxBlockquotesLevel',
|
||||
|
|
@ -117,6 +161,7 @@ export class UserSettingsGeneral extends AbstractViewSettings {
|
|||
this.addSetting('CheckMailInterval');
|
||||
this.addSetting('Layout');
|
||||
this.addSetting('MaxBlockquotesLevel');
|
||||
this.addSetting('defaultSort');
|
||||
|
||||
this.addSettings([
|
||||
'requestReadReceipt', 'requestDsn', 'requireTLS', 'pgpSign', 'pgpEncrypt',
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@ export class FilterPopupView extends rl.pluginPopupView {
|
|||
});
|
||||
|
||||
this.defaultOptionsAfterRender = defaultOptionsAfterRender;
|
||||
this.folderSelectList = koComputable(() => folderListOptionsBuilder());
|
||||
this.folderSelectList = koComputable(folderListOptionsBuilder);
|
||||
|
||||
this.selectedFolderValue.subscribe(() => this.filter().actionValueError(false));
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import { forEachObjectEntry } from 'Common/Utils';
|
|||
import { getFolderInboxName, getFolderFromCacheList } from 'Common/Cache';
|
||||
import { Settings, SettingsCapa } from 'Common/Globals';
|
||||
//import Remote from 'Remote/User/Fetch'; // Circular dependency
|
||||
import { SettingsUserStore } from 'Stores/User/Settings';
|
||||
|
||||
export const
|
||||
|
||||
|
|
@ -59,7 +60,7 @@ FolderUserStore = new class {
|
|||
*/
|
||||
displaySpecSetting: false,
|
||||
|
||||
sortMode: '',
|
||||
sortMode: SettingsUserStore.defaultSort(),
|
||||
|
||||
quotaLimit: 0,
|
||||
quotaUsage: 0,
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ export const SettingsUserStore = new class {
|
|||
showNextMessage: 0,
|
||||
allowDraftAutosave: 1,
|
||||
useThreads: 0,
|
||||
defaultSort: '',
|
||||
threadAlgorithm: '',
|
||||
replySameFolder: 0,
|
||||
hideUnsubscribed: 0,
|
||||
|
|
@ -103,6 +104,7 @@ export const SettingsUserStore = new class {
|
|||
'showNextMessage',
|
||||
'AllowDraftAutosave',
|
||||
'useThreads',
|
||||
'defaultSort',
|
||||
'threadAlgorithm',
|
||||
'ReplySameFolder',
|
||||
'HideUnsubscribed',
|
||||
|
|
|
|||
|
|
@ -22,12 +22,18 @@
|
|||
|
||||
.tabs {
|
||||
height: 100%;
|
||||
max-height: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.tabs > label > * + * {
|
||||
margin-left: 0.5em;
|
||||
}
|
||||
|
||||
.tabs .tab-content {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.textAreaParent, .attachmentAreaParent {
|
||||
height: 100%;
|
||||
min-height: 200px;
|
||||
|
|
|
|||
|
|
@ -45,4 +45,8 @@
|
|||
content: ')';
|
||||
}
|
||||
|
||||
.identities-list tr + tr .identity-default {
|
||||
display: none;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -56,6 +56,12 @@
|
|||
.squire-wysiwyg {
|
||||
font-family: var(--fontSans);
|
||||
|
||||
/* Prevent collapse of empty elements so that the caret can be placed in there
|
||||
*:empty::before {
|
||||
content: "\200B";
|
||||
}
|
||||
*/
|
||||
|
||||
ul {
|
||||
padding-left: 40px;
|
||||
li {
|
||||
|
|
|
|||
|
|
@ -457,10 +457,21 @@ export class ComposePopupView extends AbstractViewPopup {
|
|||
this.from(IdentityUserStore()[0].formattedName());
|
||||
}
|
||||
|
||||
sendCommand() {
|
||||
const identity = this.currentIdentity();
|
||||
let sSentFolder = identity?.sentFolder?.() || FolderUserStore.sentFolder();
|
||||
sentFolder()
|
||||
{
|
||||
let sSentFolder = this.currentIdentity()?.sentFolder?.() || FolderUserStore.sentFolder();
|
||||
if (SettingsUserStore.replySameFolder()) {
|
||||
if (
|
||||
3 === arrayLength(this.aDraftInfo) &&
|
||||
this.aDraftInfo[2]?.length
|
||||
) {
|
||||
sSentFolder = this.aDraftInfo[2];
|
||||
}
|
||||
}
|
||||
return UNUSED_OPTION_VALUE === sSentFolder ? null : sSentFolder;
|
||||
}
|
||||
|
||||
sendCommand() {
|
||||
this.attachmentsInProcessError(false);
|
||||
this.attachmentsInErrorError(false);
|
||||
this.emptyToError(false);
|
||||
|
|
@ -478,16 +489,8 @@ export class ComposePopupView extends AbstractViewPopup {
|
|||
}
|
||||
|
||||
if (!this.emptyToError() && !this.attachmentsInErrorError() && !this.attachmentsInProcessError()) {
|
||||
if (SettingsUserStore.replySameFolder()) {
|
||||
if (
|
||||
3 === arrayLength(this.aDraftInfo) &&
|
||||
this.aDraftInfo[2]?.length
|
||||
) {
|
||||
sSentFolder = this.aDraftInfo[2];
|
||||
}
|
||||
}
|
||||
|
||||
if (!sSentFolder) {
|
||||
const sSentFolder = this.sentFolder();
|
||||
if ('' === sSentFolder) {
|
||||
showScreenPopup(FolderSystemPopupView, [FolderType.Sent]);
|
||||
} else {
|
||||
const sendError = e => {
|
||||
|
|
@ -507,8 +510,6 @@ export class ComposePopupView extends AbstractViewPopup {
|
|||
this.sendError(false);
|
||||
this.sending(true);
|
||||
|
||||
sSentFolder = UNUSED_OPTION_VALUE === sSentFolder ? '' : sSentFolder;
|
||||
|
||||
const sendMessage = params => {
|
||||
Remote.request('SendMessage',
|
||||
(iError, data) => {
|
||||
|
|
@ -538,7 +539,7 @@ export class ComposePopupView extends AbstractViewPopup {
|
|||
this.sendError(true);
|
||||
sendFailed(iError, data);
|
||||
// Remove remembered passphrase as it could be wrong
|
||||
let key = ('S/MIME' === params.sign) ? identity : null;
|
||||
let key = ('S/MIME' === params.sign) ? this.currentIdentity() : null;
|
||||
params.signFingerprint
|
||||
&& this.signOptions.forEach(option => ('GnuPG' === option[0]) && (key = option[1]));
|
||||
key && Passphrases.delete(key);
|
||||
|
|
@ -560,10 +561,9 @@ export class ComposePopupView extends AbstractViewPopup {
|
|||
this.close();
|
||||
}
|
||||
setFolderETag(this.draftsFolder(), '');
|
||||
setFolderETag(sSentFolder, '');
|
||||
setFolderETag(params.saveFolder, '');
|
||||
if (3 === arrayLength(this.aDraftInfo)) {
|
||||
const folder = this.aDraftInfo[2];
|
||||
setFolderETag(folder, '');
|
||||
setFolderETag(this.aDraftInfo[2], '');
|
||||
}
|
||||
reloadDraftFolder();
|
||||
},
|
||||
|
|
|
|||
|
|
@ -213,15 +213,19 @@ export class MailMessageList extends AbstractViewRight {
|
|||
|
||||
sortText: () => {
|
||||
let mode = FolderUserStore.sortMode(),
|
||||
desc = '' === mode || mode.includes('REVERSE');
|
||||
has = w => mode.includes(w),
|
||||
desc = '' === mode || has('REVERSE');
|
||||
mode = mode.split(/\s+/);
|
||||
if (mode.includes('FROM')) {
|
||||
if (has('FROM')) {
|
||||
return '@' + (desc ? '⬆' : '⬇');
|
||||
}
|
||||
if (mode.includes('SUBJECT')) {
|
||||
if (has('SUBJECT')) {
|
||||
return '𝐒' + (desc ? '⬆' : '⬇');
|
||||
}
|
||||
return (mode.includes('SIZE') ? '✉' : '📅') + (desc ? '⬇' : '⬆');
|
||||
if (has('SIZE')) {
|
||||
return '✉' + (desc ? '⬇' : '⬆');
|
||||
}
|
||||
return (has('ARRIVAL') ? '📨' : '📅') + (desc ? '⬇' : '⬆');
|
||||
},
|
||||
|
||||
downloadAsZipAllowed: () => this.attachmentsActions.includes('zip')
|
||||
|
|
@ -629,8 +633,10 @@ export class MailMessageList extends AbstractViewRight {
|
|||
}
|
||||
},
|
||||
dblclick: event => {
|
||||
let el = eqs(event, '.messageListItem');
|
||||
el && this.gotoThread(ko.dataFor(el));
|
||||
let msg = ko.dataFor(eqs(event, '.messageListItem'));
|
||||
if (msg) {
|
||||
msg.threadsLen() ? this.gotoThread(msg) : toggleFullscreen();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -20,7 +20,8 @@ import {
|
|||
fireEvent,
|
||||
stopEvent,
|
||||
addShortcut,
|
||||
registerShortcut
|
||||
registerShortcut,
|
||||
formFieldFocused
|
||||
} from 'Common/Globals';
|
||||
|
||||
import { arrayLength } from 'Common/Utils';
|
||||
|
|
@ -467,8 +468,10 @@ export class MailMessageView extends AbstractViewRight {
|
|||
});
|
||||
|
||||
addShortcut('b', 'shift', [ScopeMessageList, ScopeMessageView], () => {
|
||||
currentMessage()?.swapColors?.();
|
||||
return false;
|
||||
if (!formFieldFocused()) {
|
||||
currentMessage()?.swapColors?.();
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
addShortcut('arrowup,arrowleft', 'meta', [ScopeMessageList, ScopeMessageView], () => {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
This app packages SnappyMail <upstream>2.38.0</upstream>.
|
||||
This app packages SnappyMail <upstream>2.38.2</upstream>.
|
||||
|
||||
SnappyMail is a simple, modern, lightweight & fast web-based email client.
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ RUN mkdir -p /app/code
|
|||
WORKDIR /app/code
|
||||
|
||||
# If you change the extraction below, be sure to test on scaleway
|
||||
VERSION=2.38.0
|
||||
VERSION=2.38.2
|
||||
RUN wget https://github.com/the-djmaze/snappymail/releases/download/v${VERSION}/snappymail-${VERSION}.zip -O /tmp/snappymail.zip && \
|
||||
unzip /tmp/snappymail.zip -d /app/code && \
|
||||
rm /tmp/snappymail.zip && \
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
<id>snappymail</id>
|
||||
<name>SnappyMail</name>
|
||||
<summary>SnappyMail Webmail</summary>
|
||||
<version>2.38.0</version>
|
||||
<version>2.38.2</version>
|
||||
<licence>agpl</licence>
|
||||
<author>SnappyMail, RainLoop Team, Nextgen-Networks, Tab Fitts, Nathan Kinkade, Pierre-Alain Bandinelli</author>
|
||||
<description><![CDATA[**Lightweight & fast email client.**
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ return "SnappyMail Webmail is a browser-based multilingual IMAP client with an a
|
|||
# script_snappymail_versions()
|
||||
sub script_snappymail_versions
|
||||
{
|
||||
return ( "2.38.0" );
|
||||
return ( "2.38.2" );
|
||||
}
|
||||
|
||||
sub script_snappymail_version_desc
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
"title": "SnappyMail",
|
||||
"description": "Simple, modern & fast web-based email client",
|
||||
"private": true,
|
||||
"version": "2.38.0",
|
||||
"version": "2.38.2",
|
||||
"homepage": "https://snappymail.eu",
|
||||
"author": {
|
||||
"name": "DJ Maze",
|
||||
|
|
|
|||
13
plugins/change-password/langs/uk.ini
Normal file
13
plugins/change-password/langs/uk.ini
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
[SETTINGS_CHANGE_PASSWORD]
|
||||
LEGEND_CHANGE_PASSWORD = "Змінити пароль"
|
||||
LABEL_CURRENT_PASSWORD = "Поточний пароль"
|
||||
LABEL_NEW_PASSWORD = "Новий пароль"
|
||||
LABEL_REPEAT_PASSWORD = "Підтвердьте новий пароль"
|
||||
BUTTON_UPDATE_PASSWORD = "Змінити"
|
||||
ERROR_PASSWORD_MISMATCH = "Паролі не збігаються, спробуйте ще раз"
|
||||
[NOTIFICATIONS]
|
||||
COULD_NOT_SAVE_NEW_PASSWORD = "Не вдалося зберегти новий пароль"
|
||||
CURRENT_PASSWORD_INCORRECT = "Поточний пароль не вірний"
|
||||
NEW_PASSWORD_SHORT = "Пароль закороткий"
|
||||
NEW_PASSWORD_WEAK = "Пароль надто легкий"
|
||||
NEW_PASSWORD_HIBP = "Пароль знайдено в сервісі Have I Been Pwned"
|
||||
|
|
@ -1,3 +1,8 @@
|
|||
/* Prevent collapse of empty elements so that the caret can be placed in there */
|
||||
.CompactComposer *:empty::before {
|
||||
content: "\200B";
|
||||
}
|
||||
|
||||
.CompactComposer .squire-toolbar {
|
||||
padding-top: 4px;
|
||||
padding-bottom: 0;
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@ class CompactComposerPlugin extends \RainLoop\Plugins\AbstractPlugin
|
|||
NAME = 'Compact Composer',
|
||||
AUTHOR = 'Sergey Mosin',
|
||||
URL = 'https://github.com/the-djmaze/snappymail/pull/1466',
|
||||
VERSION = '1.0.4',
|
||||
RELEASE = '2024-04-23',
|
||||
VERSION = '1.0.5',
|
||||
RELEASE = '2024-08-08',
|
||||
REQUIRED = '2.34.0',
|
||||
LICENSE = 'AGPL v3',
|
||||
DESCRIPTION = 'WYSIWYG editor with a compact toolbar';
|
||||
|
|
|
|||
|
|
@ -17,18 +17,28 @@
|
|||
addEventListener('rl-view-model', e => {
|
||||
const vm = e.detail;
|
||||
if ('PopupsCompose' === vm.viewModelTemplateID && rl.settings.get('editorWysiwyg') === 'CompactComposer') {
|
||||
vm.querySelector('.tabs label[for="tab-body"]').dataset.bind = "visible: canMailvelope";
|
||||
// add visible binding to the label
|
||||
const bodyLabel = vm.querySelector('.tabs label[for="tab-body"]');
|
||||
bodyLabel.dataset.bind = 'visible: canMailvelope';
|
||||
// re-apply the binding
|
||||
const labelNext = bodyLabel.nextElementSibling;
|
||||
ko.removeNode(bodyLabel);
|
||||
labelNext.parentElement.insertBefore(bodyLabel, labelNext);
|
||||
ko.applyBindingAccessorsToNode(bodyLabel, null, vm);
|
||||
|
||||
// Now move the attachments tab to the bottom of the screen
|
||||
const
|
||||
input = vm.querySelector('.tabs input[value="attachments"]'),
|
||||
label = vm.querySelector('.tabs label[for="tab-attachments"]'),
|
||||
area = vm.querySelector('.tabs .attachmentAreaParent');
|
||||
input.remove();
|
||||
label.remove();
|
||||
area.remove();
|
||||
const area = vm.querySelector('.tabs .attachmentAreaParent');
|
||||
vm.querySelector('.tabs input[value="attachments"]').remove();
|
||||
vm.querySelector('.tabs label[for="tab-attachments"]').remove();
|
||||
area.querySelector('.no-attachments-desc').remove();
|
||||
area.classList.add('compact');
|
||||
area.querySelector('.b-attachment-place').dataset.bind = "visible: addAttachmentEnabled(), css: {dragAndDropOver: dragAndDropVisible}";
|
||||
vm.viewModelDom.append(area);
|
||||
// Add and re-apply the bindings for the attachment-place
|
||||
const place = area.querySelector('.b-attachment-place');
|
||||
ko.removeNode(place);
|
||||
area.insertBefore(place, area.firstElementChild);
|
||||
place.dataset.bind = 'visible: addAttachmentEnabled(), css: {dragAndDropOver: dragAndDropVisible}';
|
||||
ko.applyBindingAccessorsToNode(place, null, vm);
|
||||
// There is a better way to do this probably,
|
||||
// but we need this for drag and drop to work
|
||||
e.detail.attachmentsArea = e.detail.bodyArea;
|
||||
|
|
@ -36,10 +46,6 @@
|
|||
});
|
||||
|
||||
const
|
||||
removeElements = 'HEAD,LINK,META,NOSCRIPT,SCRIPT,TEMPLATE,TITLE',
|
||||
allowedElements = 'A,B,BLOCKQUOTE,BR,DIV,EM,FONT,H1,H2,H3,H4,H5,H6,HR,I,IMG,LI,OL,P,SPAN,STRONG,TABLE,TD,TH,TR,U,UL',
|
||||
allowedAttributes = 'abbr,align,background,bgcolor,border,cellpadding,cellspacing,class,color,colspan,dir,face,frame,height,href,hspace,id,lang,rowspan,rules,scope,size,src,style,target,type,usemap,valign,vspace,width'.split(','),
|
||||
|
||||
// TODO: labels translations
|
||||
i18n = (str, def) => rl.i18n(str) || def,
|
||||
|
||||
|
|
@ -95,21 +101,7 @@
|
|||
},
|
||||
|
||||
pasteSanitizer = (event) => {
|
||||
const frag = event.detail.fragment;
|
||||
frag.querySelectorAll('a:empty,span:empty').forEach(el => el.remove());
|
||||
frag.querySelectorAll(removeElements).forEach(el => el.remove());
|
||||
frag.querySelectorAll('*').forEach(el => {
|
||||
if (!el.matches(allowedElements)) {
|
||||
el.replaceWith(getFragmentOfChildren(el));
|
||||
} else if (el.hasAttributes()) {
|
||||
[...el.attributes].forEach(attr => {
|
||||
let name = attr.name.toLowerCase();
|
||||
if (!allowedAttributes.includes(name)) {
|
||||
el.removeAttribute(name);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
return rl.Utils.cleanHtml(event.detail.html).html;
|
||||
},
|
||||
|
||||
pasteImageHandler = (e, squire) => {
|
||||
|
|
@ -317,7 +309,8 @@
|
|||
clr.style.left = (input.offsetLeft + input.parentNode.offsetLeft) + 'px';
|
||||
clr.style.width = input.offsetWidth + 'px';
|
||||
|
||||
clr.value = '';
|
||||
// firefox does not call "onchange" for #000 if we use clr.value=''
|
||||
clr.value = '#00ff0c';
|
||||
clr.onchange = () => {
|
||||
switch (name) {
|
||||
case 'color':
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ class CustomLoginMappingPlugin extends \RainLoop\Plugins\AbstractPlugin
|
|||
|
||||
public function Init() : void
|
||||
{
|
||||
// Happens only at DoLogin | ServiceSso | DoAccountSetup
|
||||
$this->addHook('login.credentials', 'FilterLoginCredentials');
|
||||
}
|
||||
|
||||
|
|
@ -29,18 +30,14 @@ class CustomLoginMappingPlugin extends \RainLoop\Plugins\AbstractPlugin
|
|||
if (!empty($sMapping)) {
|
||||
$aLines = \explode("\n", \preg_replace('/[\r\n\t\s]+/', "\n", $sMapping));
|
||||
foreach ($aLines as $sLine) {
|
||||
if (false !== \strpos($sLine, ':')) {
|
||||
$aData = \explode(':', $sLine, 3);
|
||||
if (\is_array($aData) && !empty($aData[0]) && isset($aData[1])) {
|
||||
$aData = \array_map('trim', $aData);
|
||||
if ($sEmail === $aData[0]) {
|
||||
if (\strlen($aData[1])) {
|
||||
$sImapUser = $aData[1];
|
||||
}
|
||||
if (isset($aData[2]) && \strlen($aData[2])) {
|
||||
$sSmtpUser = $aData[2];
|
||||
}
|
||||
}
|
||||
$aData = \explode(':', $sLine, 3);
|
||||
if (\is_array($aData) && isset($aData[1]) && \trim($aData[0]) === $sEmail) {
|
||||
$aData = \array_map('trim', $aData);
|
||||
if (\strlen($aData[1])) {
|
||||
$sImapUser = $aData[1];
|
||||
}
|
||||
if (isset($aData[2]) && \strlen($aData[2])) {
|
||||
$sSmtpUser = $aData[2];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
(rl => {
|
||||
const client_id = rl.pluginSettingsGet('login-o365', 'client_id'),
|
||||
// https://learn.microsoft.com/en-us/entra/identity-platform/reply-url#query-parameter-support-in-redirect-uris
|
||||
query = rl.pluginSettingsGet('login-o365', 'personal') ? '' : '?',
|
||||
tenant = rl.pluginSettingsGet('login-o365', 'tenant'),
|
||||
login = () => {
|
||||
document.location = 'https://login.microsoftonline.com/'+tenant+'/oauth2/v2.0/authorize?' + (new URLSearchParams({
|
||||
response_type: 'code',
|
||||
client_id: client_id,
|
||||
redirect_uri: document.location.href.replace(/\/$/, '') + '/LoginO365',
|
||||
redirect_uri: document.location.href.replace(/\/$/, '') + '/' + query + 'LoginO365',
|
||||
scope: [
|
||||
// Associate personal info
|
||||
'openid',
|
||||
|
|
|
|||
|
|
@ -7,8 +7,9 @@
|
|||
*
|
||||
* https://portal.azure.com/#view/Microsoft_AAD_IAM/ActiveDirectoryMenuBlade/~/RegisteredApps
|
||||
*
|
||||
* redirect_uri=https://{DOMAIN}/?LoginO365
|
||||
* redirect_uri=https://{DOMAIN}/LoginO365
|
||||
* https://learn.microsoft.com/en-us/entra/identity-platform/reply-url#query-parameter-support-in-redirect-uris
|
||||
* Azure: redirect_uri=https://{DOMAIN}/?LoginO365
|
||||
* Personal: redirect_uri=https://{DOMAIN}/LoginO365
|
||||
*/
|
||||
|
||||
use RainLoop\Model\MainAccount;
|
||||
|
|
@ -18,8 +19,8 @@ class LoginO365Plugin extends \RainLoop\Plugins\AbstractPlugin
|
|||
{
|
||||
const
|
||||
NAME = 'Office365/Outlook OAuth2',
|
||||
VERSION = '0.2',
|
||||
RELEASE = '2024-08-13',
|
||||
VERSION = '0.3',
|
||||
RELEASE = '2024-09-29',
|
||||
REQUIRED = '2.36.1',
|
||||
CATEGORY = 'Login',
|
||||
DESCRIPTION = 'Office365/Outlook IMAP, Sieve & SMTP login using RFC 7628 OAuth2';
|
||||
|
|
@ -47,6 +48,7 @@ class LoginO365Plugin extends \RainLoop\Plugins\AbstractPlugin
|
|||
|
||||
public function httpPaths(array &$aPaths) : void
|
||||
{
|
||||
// Personal accounts workaround
|
||||
if (!empty($_SERVER['PATH_INFO']) && \str_ends_with($_SERVER['PATH_INFO'], 'LoginO365')) {
|
||||
$aPaths = ['LoginO365'];
|
||||
}
|
||||
|
|
@ -113,7 +115,7 @@ class LoginO365Plugin extends \RainLoop\Plugins\AbstractPlugin
|
|||
$iExpires += $aResponse['expires_in'];
|
||||
|
||||
$oO365->setAccessToken($sAccessToken);
|
||||
$aUserInfo = $oO365->fetch('https://graph.microsoft.com/oidc/userinfo"');
|
||||
$aUserInfo = $oO365->fetch('https://graph.microsoft.com/oidc/userinfo');
|
||||
if (200 != $aUserInfo['code']) {
|
||||
throw new \RuntimeException("HTTP: {$aResponse['code']}");
|
||||
}
|
||||
|
|
@ -154,6 +156,12 @@ class LoginO365Plugin extends \RainLoop\Plugins\AbstractPlugin
|
|||
public function configMapping() : array
|
||||
{
|
||||
return [
|
||||
\RainLoop\Plugins\Property::NewInstance('personal')
|
||||
->SetLabel('Use with personal accounts')
|
||||
->SetType(\RainLoop\Enumerations\PluginPropertyType::BOOL)
|
||||
->SetDefaultValue(true)
|
||||
->SetAllowedInJs()
|
||||
->SetDescription('Sign in users with personal Microsoft accounts such as Outlook.com (Hotmail)'),
|
||||
\RainLoop\Plugins\Property::NewInstance('client_id')
|
||||
->SetLabel('Client ID')
|
||||
->SetType(\RainLoop\Enumerations\PluginPropertyType::STRING)
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@ class NextcloudPlugin extends \RainLoop\Plugins\AbstractPlugin
|
|||
{
|
||||
const
|
||||
NAME = 'Nextcloud',
|
||||
VERSION = '2.38.0',
|
||||
RELEASE = '2024-09-16',
|
||||
VERSION = '2.38.1',
|
||||
RELEASE = '2024-10-08',
|
||||
CATEGORY = 'Integrations',
|
||||
DESCRIPTION = 'Integrate with Nextcloud v20+',
|
||||
REQUIRED = '2.38.0';
|
||||
|
|
@ -55,7 +55,6 @@ class NextcloudPlugin extends \RainLoop\Plugins\AbstractPlugin
|
|||
|
||||
$this->addTemplate('templates/PopupsNextcloudFiles.html');
|
||||
$this->addTemplate('templates/PopupsNextcloudCalendars.html');
|
||||
$this->addTemplate('templates/PopupsNextcloudInvites.html');
|
||||
|
||||
// $this->addHook('login.credentials.step-2', 'loginCredentials2');
|
||||
// $this->addHook('login.credentials', 'loginCredentials');
|
||||
|
|
|
|||
|
|
@ -21,18 +21,9 @@
|
|||
|
||||
// https://github.com/nextcloud/calendar/issues/4684
|
||||
if (cfg.CalDAV) {
|
||||
attachmentsControls.append(Element.fromHTML(`<span data-bind="visible: nextcloudICSShow" data-icon="📅">
|
||||
attachmentsControls.append(Element.fromHTML(`<span data-bind="visible: nextcloudICS" data-icon="📅">
|
||||
<span class="g-ui-link" data-bind="click: nextcloudSaveICS" data-i18n="NEXTCLOUD/SAVE_ICS"></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>`))
|
||||
}
|
||||
}
|
||||
/*
|
||||
|
|
@ -116,169 +107,18 @@
|
|||
};
|
||||
|
||||
view.nextcloudICS = ko.observable(null);
|
||||
view.nextcloudICSOldInvitation = ko.observable(null);
|
||||
view.nextcloudICSNewInvitation = ko.observable(null);
|
||||
view.nextcloudICSLastInvitation = ko.observable(null);
|
||||
|
||||
view.nextcloudICSShow = ko.observable(null);
|
||||
|
||||
view.nextcloudICSCalendar = ko.observable(null);
|
||||
view.filteredEventsUrls = ko.observable([]);
|
||||
|
||||
view.nextcloudSaveICS = async () => {
|
||||
view.nextcloudSaveICS = () => {
|
||||
let VEVENT = view.nextcloudICS();
|
||||
VEVENT = await view.handleUpdatedRecurrentEvents(VEVENT)
|
||||
VEVENT && rl.nextcloud.selectCalendar(VEVENT)
|
||||
VEVENT && rl.nextcloud.selectCalendar()
|
||||
.then(href => href && rl.nextcloud.calendarPut(href, 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
|
||||
*/
|
||||
view.message.subscribe(msg => {
|
||||
view.nextcloudICS(null);
|
||||
view.nextcloudICSOldInvitation(null);
|
||||
view.nextcloudICSNewInvitation(null);
|
||||
view.nextcloudICSLastInvitation(null);
|
||||
view.nextcloudICSShow(view.nextcloudICS())
|
||||
|
||||
|
||||
if (msg && cfg.CalDAV) {
|
||||
// let ics = msg.attachments.find(attachment => 'application/ics' == attachment.mimeType);
|
||||
let ics = msg.attachments.find(attachment => 'text/calendar' == attachment.mimeType);
|
||||
|
|
@ -286,7 +126,7 @@
|
|||
// fetch it and parse the VEVENT
|
||||
rl.fetch(ics.linkDownload())
|
||||
.then(response => (response.status < 400) ? response.text() : Promise.reject(new Error({ response })))
|
||||
.then(async (text) => {
|
||||
.then(text => {
|
||||
let VEVENT,
|
||||
VALARM,
|
||||
multiple = ['ATTACH','ATTENDEE','CATEGORIES','COMMENT','CONTACT','EXDATE',
|
||||
|
|
@ -337,158 +177,6 @@
|
|||
shouldReply: VEVENT.shouldReply()
|
||||
});
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -498,148 +186,3 @@
|
|||
});
|
||||
|
||||
})(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,28 +320,12 @@ class NextcloudCalendarsPopupView extends rl.pluginPopupView {
|
|||
}
|
||||
|
||||
onBuild(dom) {
|
||||
let modalObj = this
|
||||
this.tree = dom.querySelector('#sm-nc-calendars');
|
||||
this.tree.addEventListener('click', event => {
|
||||
let el = event.target;
|
||||
if (el.matches('button')) {
|
||||
this.select = el.href;
|
||||
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()}
|
||||
])
|
||||
})
|
||||
this.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -391,10 +375,9 @@ class NextcloudCalendarsPopupView extends rl.pluginPopupView {
|
|||
treeElement.appendChild(li);
|
||||
}
|
||||
// Happens after showModal()
|
||||
beforeShow(fResolve, VEVENT) {
|
||||
beforeShow(fResolve) {
|
||||
this.select = '';
|
||||
this.fResolve = fResolve;
|
||||
this.VEVENT = VEVENT
|
||||
this.tree.innerHTML = '';
|
||||
davFetch('calendars', '/', {
|
||||
method: 'PROPFIND',
|
||||
|
|
@ -449,15 +432,14 @@ close() {}
|
|||
}
|
||||
|
||||
rl.nextcloud = {
|
||||
selectCalendar: (VEVENT) =>
|
||||
selectCalendar: () =>
|
||||
new Promise(resolve => {
|
||||
NextcloudCalendarsPopupView.showModal([
|
||||
href => resolve(href),
|
||||
VEVENT
|
||||
]);
|
||||
}),
|
||||
|
||||
calendarPut: (path, event, callback) => {
|
||||
calendarPut: (path, event) => {
|
||||
davFetch('calendars', path + '/' + event.UID + '.ics', {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
|
|
@ -481,8 +463,6 @@ rl.nextcloud = {
|
|||
// response.text().then(text => console.error({status:response.status, body:text}));
|
||||
Promise.reject(new Error({ response }));
|
||||
}
|
||||
|
||||
callback && callback(response)
|
||||
});
|
||||
},
|
||||
|
||||
|
|
@ -520,27 +500,3 @@ function getElementsInNamespaces(xmlDocument, tagName) {
|
|||
}
|
||||
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,21 +2,14 @@
|
|||
"NEXTCLOUD": {
|
||||
"SAVE_ATTACHMENTS": "Save in Nextcloud",
|
||||
"SAVE_EML": "Save as .eml in Nextcloud",
|
||||
"SAVE_ICS": "Add to calendar",
|
||||
"SELECT_FOLDER": "Select folder",
|
||||
"SELECT_FILES": "Select file(s)",
|
||||
"ATTACH_FILES": "Attach Nextcloud files",
|
||||
"SELECT_CALENDAR": "Select calendar",
|
||||
"FILE_ATTACH": "attach",
|
||||
"FILE_INTERNAL": "internal",
|
||||
"FILE_PUBLIC": "public",
|
||||
"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"
|
||||
"FILE_PUBLIC": "public"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +0,0 @@
|
|||
<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>
|
||||
20
plugins/send-save-in/LICENSE
Normal file
20
plugins/send-save-in/LICENSE
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2016 RainLoop Team
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
23
plugins/send-save-in/index.php
Normal file
23
plugins/send-save-in/index.php
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
<?php
|
||||
|
||||
class SendSaveInPlugin extends \RainLoop\Plugins\AbstractPlugin
|
||||
{
|
||||
// use \MailSo\Log\Inherit;
|
||||
|
||||
const
|
||||
NAME = 'Send Save In',
|
||||
AUTHOR = 'SnappyMail',
|
||||
URL = 'https://snappymail.eu/',
|
||||
VERSION = '0.1',
|
||||
RELEASE = '2024-10-08',
|
||||
REQUIRED = '2.38.1',
|
||||
CATEGORY = 'General',
|
||||
LICENSE = 'MIT',
|
||||
DESCRIPTION = 'When composing a message, select the save folder';
|
||||
|
||||
public function Init() : void
|
||||
{
|
||||
// $this->UseLangs(true); // start use langs folder
|
||||
$this->addJs('savein.js');
|
||||
}
|
||||
}
|
||||
75
plugins/send-save-in/savein.js
Normal file
75
plugins/send-save-in/savein.js
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
(rl => {
|
||||
const templateId = 'PopupsCompose',
|
||||
folderListOptionsBuilder = () => {
|
||||
const
|
||||
aResult = [{
|
||||
id: '',
|
||||
name: '',
|
||||
system: false,
|
||||
disabled: false
|
||||
}],
|
||||
sDeepPrefix = '\u00A0\u00A0\u00A0',
|
||||
showUnsubscribed = true/*!SettingsUserStore.hideUnsubscribed()*/,
|
||||
|
||||
foldersWalk = folders => {
|
||||
folders.forEach(oItem => {
|
||||
if (showUnsubscribed || oItem.hasSubscriptions() || !oItem.exists) {
|
||||
aResult.push({
|
||||
id: oItem.fullName,
|
||||
name: sDeepPrefix.repeat(oItem.deep) + oItem.detailedName(),
|
||||
system: false,
|
||||
disabled: !oItem.selectable()
|
||||
});
|
||||
}
|
||||
|
||||
if (oItem.subFolders.length) {
|
||||
foldersWalk(oItem.subFolders());
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
// FolderUserStore.folderList()
|
||||
foldersWalk(rl.app.folderList() || []);
|
||||
|
||||
return aResult;
|
||||
};
|
||||
|
||||
let oldSentFolderFn;
|
||||
|
||||
addEventListener('rl-view-model.create', e => {
|
||||
if (templateId === e.detail.viewModelTemplateID) {
|
||||
const view = e.detail; // ComposePopupView
|
||||
|
||||
view.sentFolderValue = ko.observable('');
|
||||
view.sentFolderSelectList = ko.computed(folderListOptionsBuilder, {'pure':true});
|
||||
view.defaultOptionsAfterRender = (domItem, item) =>
|
||||
item && undefined !== item.disabled && domItem?.classList.toggle('disabled', domItem.disabled = item.disabled);
|
||||
|
||||
oldSentFolderFn = view.sentFolder.bind(view);
|
||||
view.sentFolder = () => view.sentFolderValue() || oldSentFolderFn();
|
||||
|
||||
document.getElementById(templateId).content.querySelector('.b-header tbody').append(Element.fromHTML(`
|
||||
<tr>
|
||||
<td>Store in</td>
|
||||
<td>
|
||||
<select class="span3" data-bind="options: sentFolderSelectList, value: sentFolderValue,
|
||||
optionsText: 'name', optionsValue: 'id', optionsAfterRender: defaultOptionsAfterRender"></select>
|
||||
(When send, store a copy of the message in the selected folder)
|
||||
</td>
|
||||
</tr>`));
|
||||
|
||||
view.currentIdentity.subscribe(()=>{
|
||||
view.sentFolderValue(oldSentFolderFn());
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
addEventListener('rl-vm-visible', e => {
|
||||
if (templateId === e.detail.viewModelTemplateID) {
|
||||
const view = e.detail; // ComposePopupView
|
||||
view.sentFolderValue(oldSentFolderFn());
|
||||
}
|
||||
});
|
||||
|
||||
})(window.rl);
|
||||
|
|
@ -12,8 +12,8 @@ BUTTON_SHOW_SECRET = "Show Secret"
|
|||
BUTTON_HIDE_SECRET = "Hide Secret"
|
||||
TWO_FACTOR_SECRET_CONFIGURED_DESC = "Configured"
|
||||
TWO_FACTOR_SECRET_NOT_CONFIGURED_DESC = "Not configured"
|
||||
TWO_FACTOR_SECRET_DESC = "Import this info into your Google Authenticator client (or other TOTP client) using the provided QR code below or by entering the code manually.\n"
|
||||
TWO_FACTOR_BACKUP_CODES_DESC = "If you can't receive codes via Google Authenticator (or other TOTP client), you can use backup codes to sign in. After you’ve used a backup code to sign in, it will become inactive.\n"
|
||||
TWO_FACTOR_SECRET_DESC = "Import this info into your Google Authenticator client (or other TOTP client) using the provided QR code below or by entering the code manually."
|
||||
TWO_FACTOR_BACKUP_CODES_DESC = "If you can't receive codes via Google Authenticator (or other TOTP client), you can use backup codes to sign in. After you’ve used a backup code to sign in, it will become inactive."
|
||||
TWO_FACTOR_SECRET_TEST_BEFORE_DESC = "You can't change this setting before test."
|
||||
TITLE_TEST_CODE = "2-Step verification test"
|
||||
LABEL_CODE = "Code"
|
||||
|
|
|
|||
|
|
@ -12,8 +12,8 @@ BUTTON_SHOW_SECRET = "Show Secret"
|
|||
BUTTON_HIDE_SECRET = "Hide Secret"
|
||||
TWO_FACTOR_SECRET_CONFIGURED_DESC = "Configured"
|
||||
TWO_FACTOR_SECRET_NOT_CONFIGURED_DESC = "Not configured"
|
||||
TWO_FACTOR_SECRET_DESC = "Import this info into your Google Authenticator client (or other TOTP client) using the provided QR code below or by entering the code manually.\n"
|
||||
TWO_FACTOR_BACKUP_CODES_DESC = "If you can't receive codes via Google Authenticator (or other TOTP client), you can use backup codes to sign in. After you’ve used a backup code to sign in, it will become inactive.\n"
|
||||
TWO_FACTOR_SECRET_DESC = "Import this info into your Google Authenticator client (or other TOTP client) using the provided QR code below or by entering the code manually."
|
||||
TWO_FACTOR_BACKUP_CODES_DESC = "If you can't receive codes via Google Authenticator (or other TOTP client), you can use backup codes to sign in. After you’ve used a backup code to sign in, it will become inactive."
|
||||
TWO_FACTOR_SECRET_TEST_BEFORE_DESC = "You can't change this setting before test."
|
||||
TITLE_TEST_CODE = "2-Step verification test"
|
||||
LABEL_CODE = "Code"
|
||||
|
|
|
|||
|
|
@ -12,8 +12,8 @@ BUTTON_SHOW_SECRET = "Titok megjelenítése"
|
|||
BUTTON_HIDE_SECRET = "Titok elrejtése"
|
||||
TWO_FACTOR_SECRET_CONFIGURED_DESC = "Beállítva"
|
||||
TWO_FACTOR_SECRET_NOT_CONFIGURED_DESC = "Nincs beállítva"
|
||||
TWO_FACTOR_SECRET_DESC = "Importáld ezt az infót a Google Authenticator kliensedbe (vagy más TOTP kliensbe) az alábbi QR kód használatával vagy a kód manuális megadatásával.\n"
|
||||
TWO_FACTOR_BACKUP_CODES_DESC = "Ha nem kapod meg a kódokat a Google Authenticator kliensből (vagy más TOTP kliensből), akkor a bejelentkezéshez használhatod a biztonsági kódot. A biztonsági kód használata után inaktívvá válik.\n"
|
||||
TWO_FACTOR_SECRET_DESC = "Importáld ezt az infót a Google Authenticator kliensedbe (vagy más TOTP kliensbe) az alábbi QR kód használatával vagy a kód manuális megadatásával."
|
||||
TWO_FACTOR_BACKUP_CODES_DESC = "Ha nem kapod meg a kódokat a Google Authenticator kliensből (vagy más TOTP kliensből), akkor a bejelentkezéshez használhatod a biztonsági kódot. A biztonsági kód használata után inaktívvá válik."
|
||||
TWO_FACTOR_SECRET_TEST_BEFORE_DESC = "Tesztelés nélkül nem lehet megváltoztatni ezt a beállítást."
|
||||
TITLE_TEST_CODE = "2-lépéses hitelesítés teszt"
|
||||
LABEL_CODE = "Kód"
|
||||
|
|
|
|||
|
|
@ -12,8 +12,8 @@ BUTTON_SHOW_SECRET = "Mostra la chiave segreta"
|
|||
BUTTON_HIDE_SECRET = "Nascondi la chiave segreta"
|
||||
TWO_FACTOR_SECRET_CONFIGURED_DESC = "Configurato"
|
||||
TWO_FACTOR_SECRET_NOT_CONFIGURED_DESC = "Non configurato"
|
||||
TWO_FACTOR_SECRET_DESC = "Importa queste informazioni nella tua app Google Authenticator (o un altro programma per l'autenticazione a due fattori) utilizzando il codice QR fornito qui sotto o inserendo il codice manualmente.\n"
|
||||
TWO_FACTOR_BACKUP_CODES_DESC = "Se non riesci a ricevere i codici tramite Google Authenticator (o un altro programma per l'autenticazione a due fattori), puoi utilizzare i codici di backup per accedere. Dopo che hai utilizzato un codice di backup per accedere, diventerà inattivo.\n"
|
||||
TWO_FACTOR_SECRET_DESC = "Importa queste informazioni nella tua app Google Authenticator (o un altro programma per l'autenticazione a due fattori) utilizzando il codice QR fornito qui sotto o inserendo il codice manualmente."
|
||||
TWO_FACTOR_BACKUP_CODES_DESC = "Se non riesci a ricevere i codici tramite Google Authenticator (o un altro programma per l'autenticazione a due fattori), puoi utilizzare i codici di backup per accedere. Dopo che hai utilizzato un codice di backup per accedere, diventerà inattivo."
|
||||
TWO_FACTOR_SECRET_TEST_BEFORE_DESC = "Non puoi modificare questa impostazione prima del test."
|
||||
TITLE_TEST_CODE = "Test di verifica a 2 fattori"
|
||||
LABEL_CODE = "Codice"
|
||||
|
|
|
|||
20
plugins/two-factor-auth/langs/uk.ini
Normal file
20
plugins/two-factor-auth/langs/uk.ini
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
[PLUGIN_2FA]
|
||||
LEGEND_TWO_FACTOR_AUTH = "2-етапна перевірка (TOTP)"
|
||||
LABEL_ENABLE_TWO_FACTOR = "Увімкнути 2-етапну перевірку"
|
||||
LABEL_TWO_FACTOR_USER = "Ім'я користувача"
|
||||
LABEL_TWO_FACTOR_STATUS = "Статус"
|
||||
LABEL_TWO_FACTOR_SECRET = "Ключ"
|
||||
LABEL_TWO_FACTOR_BACKUP_CODES = "Резервні коди"
|
||||
BUTTON_CREATE = "Create a secret"
|
||||
BUTTON_ACTIVATE = "Activate"
|
||||
LINK_TEST = "Тестувати"
|
||||
BUTTON_SHOW_SECRET = "Показати ключ"
|
||||
BUTTON_HIDE_SECRET = "Приховати ключ"
|
||||
TWO_FACTOR_SECRET_CONFIGURED_DESC = "Налаштовано"
|
||||
TWO_FACTOR_SECRET_NOT_CONFIGURED_DESC = "Не налаштовано"
|
||||
TWO_FACTOR_SECRET_DESC = "Імпортуйте цю інформацію у свій клієнт Google Authenticator (або інший клієнт TOTP), використовуючи наданий нижче QR-код або ввівши код вручну"
|
||||
TWO_FACTOR_BACKUP_CODES_DESC = "Якщо ви не можете отримати код через Google Authenticator (або інший клієнт TOTP), ви можете скористатися резервними кодами для входу. Після того, як ви використаєте резервний код для входу, він стане неактивним. Закреслюйте його"
|
||||
TWO_FACTOR_SECRET_TEST_BEFORE_DESC = "Ви не можете змінити це налаштування перед тестуванням."
|
||||
TITLE_TEST_CODE = "Тест 2-етапної перевірки"
|
||||
LABEL_CODE = "Код"
|
||||
LABEL_TWO_FACTOR_CODE = "Код підтвердження"
|
||||
|
|
@ -12,8 +12,8 @@ BUTTON_SHOW_SECRET = "显示密钥"
|
|||
BUTTON_HIDE_SECRET = "隐藏密钥"
|
||||
TWO_FACTOR_SECRET_CONFIGURED_DESC = "已设置"
|
||||
TWO_FACTOR_SECRET_NOT_CONFIGURED_DESC = "未设置"
|
||||
TWO_FACTOR_SECRET_DESC = "将下面的信息导入 Google Authenticator (或其他 TOTP 客户端)。 扫描下面的二维码或手动输入密钥。\n"
|
||||
TWO_FACTOR_BACKUP_CODES_DESC = "如果你无法从验证器获取验证码,你可以使用备用代码登录。 当你使用过某一个备用代码后,它将会失效。\n"
|
||||
TWO_FACTOR_SECRET_DESC = "将下面的信息导入 Google Authenticator (或其他 TOTP 客户端)。 扫描下面的二维码或手动输入密钥。"
|
||||
TWO_FACTOR_BACKUP_CODES_DESC = "如果你无法从验证器获取验证码,你可以使用备用代码登录。 当你使用过某一个备用代码后,它将会失效。"
|
||||
TWO_FACTOR_SECRET_TEST_BEFORE_DESC = "完成测试前你无法更改此设置。"
|
||||
TITLE_TEST_CODE = "两步验证测试"
|
||||
LABEL_CODE = "代码"
|
||||
|
|
|
|||
|
|
@ -69,6 +69,7 @@ trait Accounts
|
|||
}
|
||||
|
||||
/**
|
||||
* Add/Edit additional account
|
||||
* @throws \MailSo\RuntimeException
|
||||
*/
|
||||
public function DoAccountSetup(): array
|
||||
|
|
@ -209,6 +210,7 @@ trait Accounts
|
|||
'mainEmail' => \RainLoop\Api::Actions()->getMainAccountFromToken()->Email(),
|
||||
'contactsAllowed' => $this->AddressBookProvider($oAccount)->IsActive(),
|
||||
'HideUnsubscribed' => false,
|
||||
'defaultSort' => '',
|
||||
'useThreads' => (bool) $oConfig->Get('defaults', 'mail_use_threads', false),
|
||||
'threadAlgorithm' => '',
|
||||
'ReplySameFolder' => (bool) $oConfig->Get('defaults', 'mail_reply_same_folder', false),
|
||||
|
|
@ -225,6 +227,7 @@ trait Accounts
|
|||
$aResult['TrashFolder'] = (string) $oSettingsLocal->GetConf('TrashFolder', '');
|
||||
$aResult['ArchiveFolder'] = (string) $oSettingsLocal->GetConf('ArchiveFolder', '');
|
||||
$aResult['HideUnsubscribed'] = (bool) $oSettingsLocal->GetConf('HideUnsubscribed', $aResult['HideUnsubscribed']);
|
||||
$aResult['defaultSort'] = (string) $oSettingsLocal->GetConf('defaultSort', $aResult['defaultSort']);
|
||||
$aResult['useThreads'] = (bool) $oSettingsLocal->GetConf('UseThreads', $aResult['useThreads']);
|
||||
$aResult['threadAlgorithm'] = (string) $oSettingsLocal->GetConf('threadAlgorithm', $aResult['threadAlgorithm']);
|
||||
$aResult['ReplySameFolder'] = (bool) $oSettingsLocal->GetConf('ReplySameFolder', $aResult['ReplySameFolder']);
|
||||
|
|
|
|||
|
|
@ -204,6 +204,7 @@ trait User
|
|||
$this->setSettingsFromParams($oSettings, 'Resizer5Width', 'int');
|
||||
$this->setSettingsFromParams($oSettings, 'Resizer5Height', 'int');
|
||||
|
||||
$this->setSettingsFromParams($oSettingsLocal, 'defaultSort', 'string');
|
||||
$this->setSettingsFromParams($oSettingsLocal, 'UseThreads', 'bool');
|
||||
$this->setSettingsFromParams($oSettingsLocal, 'threadAlgorithm', 'string');
|
||||
$this->setSettingsFromParams($oSettingsLocal, 'ReplySameFolder', 'bool');
|
||||
|
|
|
|||
|
|
@ -141,21 +141,23 @@ trait UserAuth
|
|||
*/
|
||||
public function LoginProcess(string $sEmail, SensitiveString $oPassword, bool $bMainAccount = true): Account
|
||||
{
|
||||
$sCredentials = $this->resolveLoginCredentials($sEmail, $oPassword);
|
||||
$aCredentials = $this->resolveLoginCredentials($sEmail, $oPassword);
|
||||
|
||||
if (!\str_contains($sCredentials['email'], '@') || !\strlen($oPassword)) {
|
||||
if (!\str_contains($aCredentials['email'], '@') || !\strlen($oPassword)) {
|
||||
throw new ClientException(Notifications::InvalidInputArgument);
|
||||
}
|
||||
|
||||
$oDomain = $this->DomainProvider()->getByEmailAddress($aCredentials['email']);
|
||||
|
||||
$oAccount = null;
|
||||
try {
|
||||
$oAccount = $bMainAccount ? new MainAccount : new AdditionalAccount;
|
||||
$oAccount->setCredentials(
|
||||
$sCredentials['domain'],
|
||||
$sCredentials['email'],
|
||||
$sCredentials['imapUser'],
|
||||
$aCredentials['domain'],
|
||||
$aCredentials['email'],
|
||||
$aCredentials['imapUser'],
|
||||
$oPassword,
|
||||
$sCredentials['smtpUser']
|
||||
$aCredentials['smtpUser']
|
||||
// ,new SensitiveString($oPassword)
|
||||
);
|
||||
$this->Plugins()->RunHook('filter.account', array($oAccount));
|
||||
|
|
@ -207,6 +209,7 @@ trait UserAuth
|
|||
|
||||
// Test the login
|
||||
$oImapClient = new \MailSo\Imap\ImapClient;
|
||||
$oImapClient->SetLogger($this->Logger());
|
||||
$this->imapConnect($oAccount, false, $oImapClient);
|
||||
}
|
||||
$this->SetAdditionalAuthToken($oAccount);
|
||||
|
|
|
|||
|
|
@ -165,7 +165,8 @@ abstract class Account implements \JsonSerializable
|
|||
// $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->sImapUser = \SnappyMail\IDN::emailToAscii($aAccountHash['login']);
|
||||
$oAccount->sImapUser = $aAccountHash['login'];
|
||||
$oAccount->setImapPass(new SensitiveString($aAccountHash['pass']));
|
||||
$oAccount->oDomain = $oDomain;
|
||||
$oActions->Plugins()->RunHook('filter.account', array($oAccount));
|
||||
|
|
@ -286,7 +287,7 @@ abstract class Account implements \JsonSerializable
|
|||
*/
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
* @deprecated since v2.36.1
|
||||
*/
|
||||
public function IncLogin() : string
|
||||
{
|
||||
|
|
|
|||
|
|
@ -106,7 +106,7 @@ class Domain implements \JsonSerializable
|
|||
|
||||
public function ValidateWhiteList(string $sEmail) : bool
|
||||
{
|
||||
$sW = \trim($this->whiteList);
|
||||
$sW = $this->whiteList;
|
||||
if (!$sW) {
|
||||
return true;
|
||||
}
|
||||
|
|
@ -210,7 +210,7 @@ class Domain implements \JsonSerializable
|
|||
$oDomain->SMTP->setSender = !empty($aDomain['smtp_set_sender']);
|
||||
$oDomain->SMTP->usePhpMail = !empty($aDomain['smtp_php_mail']);
|
||||
|
||||
$oDomain->whiteList = \trim($aDomain['white_list'] ?? '');
|
||||
$oDomain->whiteList = $aDomain['white_list'] ?? '';
|
||||
|
||||
$oDomain->Normalize();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,19 +2,14 @@
|
|||
|
||||
namespace RainLoop\Providers;
|
||||
|
||||
use RainLoop\Notifications;
|
||||
use RainLoop\Exceptions\ClientException;
|
||||
|
||||
class Domain extends AbstractProvider
|
||||
{
|
||||
/**
|
||||
* @var Domain\DomainInterface
|
||||
*/
|
||||
private $oDriver;
|
||||
private Domain\DomainInterface $oDriver;
|
||||
|
||||
/**
|
||||
* @var \RainLoop\Plugins\Manager
|
||||
*/
|
||||
private $oPlugins;
|
||||
private \RainLoop\Plugins\Manager $oPlugins;
|
||||
|
||||
public function __construct(Domain\DomainInterface $oDriver, \RainLoop\Plugins\Manager $oPlugins)
|
||||
{
|
||||
|
|
@ -86,10 +81,10 @@ class Domain extends AbstractProvider
|
|||
{
|
||||
$oDomain = $this->Load(\MailSo\Base\Utils::getEmailAddressDomain($sEmail), true);
|
||||
if (!$oDomain) {
|
||||
throw new ClientException(Notifications::DomainNotAllowed);
|
||||
throw new ClientException(Notifications::DomainNotAllowed, null, "{$sEmail} has no domain configuration");
|
||||
}
|
||||
if (!$oDomain->ValidateWhiteList($sEmail)) {
|
||||
throw new ClientException(Notifications::AccountNotAllowed);
|
||||
throw new ClientException(Notifications::AccountNotAllowed, null, "{$sEmail} not whitelisted");
|
||||
}
|
||||
return $oDomain;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
|
||||
namespace Sabre\VObject;
|
||||
|
||||
use Sabre\VObject;
|
||||
use Sabre\Xml;
|
||||
|
||||
/**
|
||||
|
|
@ -15,7 +14,7 @@ use Sabre\Xml;
|
|||
* @author Evert Pot (http://evertpot.com/)
|
||||
* @license http://sabre.io/license/ Modified BSD License
|
||||
*
|
||||
* @property VObject\Property\FlatText UID
|
||||
* @property Property\FlatText UID
|
||||
*/
|
||||
class Component extends Node
|
||||
{
|
||||
|
|
|
|||
|
|
@ -39,13 +39,7 @@ class VAlarm extends VObject\Component
|
|||
/** @var VEvent|VTodo $parentComponent */
|
||||
$parentComponent = $this->parent;
|
||||
if ('START' === $related) {
|
||||
if ('VTODO' === $parentComponent->name) {
|
||||
$propName = 'DUE';
|
||||
} else {
|
||||
$propName = 'DTSTART';
|
||||
}
|
||||
|
||||
$effectiveTrigger = $parentComponent->$propName->getDateTime();
|
||||
$effectiveTrigger = $parentComponent->DTSTART->getDateTime();
|
||||
} else {
|
||||
if ('VTODO' === $parentComponent->name) {
|
||||
$endProp = 'DUE';
|
||||
|
|
|
|||
|
|
@ -20,8 +20,8 @@ use Sabre\VObject\Recur\NoInstancesException;
|
|||
*
|
||||
* @property VEvent VEVENT
|
||||
* @property VJournal VJOURNAL
|
||||
* @property VObject\Property\Text ORG
|
||||
* @property VObject\Property\FlatText METHOD
|
||||
* @property Property\Text ORG
|
||||
* @property Property\FlatText METHOD
|
||||
*/
|
||||
class VCalendar extends VObject\Document
|
||||
{
|
||||
|
|
@ -51,21 +51,21 @@ class VCalendar extends VObject\Document
|
|||
* List of value-types, and which classes they map to.
|
||||
*/
|
||||
public static array $valueMap = [
|
||||
'BINARY' => VObject\Property\Binary::class,
|
||||
'BOOLEAN' => VObject\Property\Boolean::class,
|
||||
'CAL-ADDRESS' => VObject\Property\ICalendar\CalAddress::class,
|
||||
'DATE' => VObject\Property\ICalendar\Date::class,
|
||||
'DATE-TIME' => VObject\Property\ICalendar\DateTime::class,
|
||||
'DURATION' => VObject\Property\ICalendar\Duration::class,
|
||||
'FLOAT' => VObject\Property\FloatValue::class,
|
||||
'INTEGER' => VObject\Property\IntegerValue::class,
|
||||
'PERIOD' => VObject\Property\ICalendar\Period::class,
|
||||
'RECUR' => VObject\Property\ICalendar\Recur::class,
|
||||
'TEXT' => VObject\Property\Text::class,
|
||||
'TIME' => VObject\Property\Time::class,
|
||||
'UNKNOWN' => VObject\Property\Unknown::class, // jCard / jCal-only.
|
||||
'URI' => VObject\Property\Uri::class,
|
||||
'UTC-OFFSET' => VObject\Property\UtcOffset::class,
|
||||
'BINARY' => Property\Binary::class,
|
||||
'BOOLEAN' => Property\Boolean::class,
|
||||
'CAL-ADDRESS' => Property\ICalendar\CalAddress::class,
|
||||
'DATE' => Property\ICalendar\Date::class,
|
||||
'DATE-TIME' => Property\ICalendar\DateTime::class,
|
||||
'DURATION' => Property\ICalendar\Duration::class,
|
||||
'FLOAT' => Property\FloatValue::class,
|
||||
'INTEGER' => Property\IntegerValue::class,
|
||||
'PERIOD' => Property\ICalendar\Period::class,
|
||||
'RECUR' => Property\ICalendar\Recur::class,
|
||||
'TEXT' => Property\Text::class,
|
||||
'TIME' => Property\Time::class,
|
||||
'UNKNOWN' => Property\Unknown::class, // jCard / jCal-only.
|
||||
'URI' => Property\Uri::class,
|
||||
'UTC-OFFSET' => Property\UtcOffset::class,
|
||||
];
|
||||
|
||||
/**
|
||||
|
|
@ -73,78 +73,78 @@ class VCalendar extends VObject\Document
|
|||
*/
|
||||
public static array $propertyMap = [
|
||||
// Calendar properties
|
||||
'CALSCALE' => VObject\Property\FlatText::class,
|
||||
'METHOD' => VObject\Property\FlatText::class,
|
||||
'PRODID' => VObject\Property\FlatText::class,
|
||||
'VERSION' => VObject\Property\FlatText::class,
|
||||
'CALSCALE' => Property\FlatText::class,
|
||||
'METHOD' => Property\FlatText::class,
|
||||
'PRODID' => Property\FlatText::class,
|
||||
'VERSION' => Property\FlatText::class,
|
||||
|
||||
// Component properties
|
||||
'ATTACH' => VObject\Property\Uri::class,
|
||||
'CATEGORIES' => VObject\Property\Text::class,
|
||||
'CLASS' => VObject\Property\FlatText::class,
|
||||
'COMMENT' => VObject\Property\FlatText::class,
|
||||
'DESCRIPTION' => VObject\Property\FlatText::class,
|
||||
'GEO' => VObject\Property\FloatValue::class,
|
||||
'LOCATION' => VObject\Property\FlatText::class,
|
||||
'PERCENT-COMPLETE' => VObject\Property\IntegerValue::class,
|
||||
'PRIORITY' => VObject\Property\IntegerValue::class,
|
||||
'RESOURCES' => VObject\Property\Text::class,
|
||||
'STATUS' => VObject\Property\FlatText::class,
|
||||
'SUMMARY' => VObject\Property\FlatText::class,
|
||||
'ATTACH' => Property\Uri::class,
|
||||
'CATEGORIES' => Property\Text::class,
|
||||
'CLASS' => Property\FlatText::class,
|
||||
'COMMENT' => Property\FlatText::class,
|
||||
'DESCRIPTION' => Property\FlatText::class,
|
||||
'GEO' => Property\FloatValue::class,
|
||||
'LOCATION' => Property\FlatText::class,
|
||||
'PERCENT-COMPLETE' => Property\IntegerValue::class,
|
||||
'PRIORITY' => Property\IntegerValue::class,
|
||||
'RESOURCES' => Property\Text::class,
|
||||
'STATUS' => Property\FlatText::class,
|
||||
'SUMMARY' => Property\FlatText::class,
|
||||
|
||||
// Date and Time Component Properties
|
||||
'COMPLETED' => VObject\Property\ICalendar\DateTime::class,
|
||||
'DTEND' => VObject\Property\ICalendar\DateTime::class,
|
||||
'DUE' => VObject\Property\ICalendar\DateTime::class,
|
||||
'DTSTART' => VObject\Property\ICalendar\DateTime::class,
|
||||
'DURATION' => VObject\Property\ICalendar\Duration::class,
|
||||
'FREEBUSY' => VObject\Property\ICalendar\Period::class,
|
||||
'TRANSP' => VObject\Property\FlatText::class,
|
||||
'COMPLETED' => Property\ICalendar\DateTime::class,
|
||||
'DTEND' => Property\ICalendar\DateTime::class,
|
||||
'DUE' => Property\ICalendar\DateTime::class,
|
||||
'DTSTART' => Property\ICalendar\DateTime::class,
|
||||
'DURATION' => Property\ICalendar\Duration::class,
|
||||
'FREEBUSY' => Property\ICalendar\Period::class,
|
||||
'TRANSP' => Property\FlatText::class,
|
||||
|
||||
// Time Zone Component Properties
|
||||
'TZID' => VObject\Property\FlatText::class,
|
||||
'TZNAME' => VObject\Property\FlatText::class,
|
||||
'TZOFFSETFROM' => VObject\Property\UtcOffset::class,
|
||||
'TZOFFSETTO' => VObject\Property\UtcOffset::class,
|
||||
'TZURL' => VObject\Property\Uri::class,
|
||||
'TZID' => Property\FlatText::class,
|
||||
'TZNAME' => Property\FlatText::class,
|
||||
'TZOFFSETFROM' => Property\UtcOffset::class,
|
||||
'TZOFFSETTO' => Property\UtcOffset::class,
|
||||
'TZURL' => Property\Uri::class,
|
||||
|
||||
// Relationship Component Properties
|
||||
'ATTENDEE' => VObject\Property\ICalendar\CalAddress::class,
|
||||
'CONTACT' => VObject\Property\FlatText::class,
|
||||
'ORGANIZER' => VObject\Property\ICalendar\CalAddress::class,
|
||||
'RECURRENCE-ID' => VObject\Property\ICalendar\DateTime::class,
|
||||
'RELATED-TO' => VObject\Property\FlatText::class,
|
||||
'URL' => VObject\Property\Uri::class,
|
||||
'UID' => VObject\Property\FlatText::class,
|
||||
'ATTENDEE' => Property\ICalendar\CalAddress::class,
|
||||
'CONTACT' => Property\FlatText::class,
|
||||
'ORGANIZER' => Property\ICalendar\CalAddress::class,
|
||||
'RECURRENCE-ID' => Property\ICalendar\DateTime::class,
|
||||
'RELATED-TO' => Property\FlatText::class,
|
||||
'URL' => Property\Uri::class,
|
||||
'UID' => Property\FlatText::class,
|
||||
|
||||
// Recurrence Component Properties
|
||||
'EXDATE' => VObject\Property\ICalendar\DateTime::class,
|
||||
'RDATE' => VObject\Property\ICalendar\DateTime::class,
|
||||
'RRULE' => VObject\Property\ICalendar\Recur::class,
|
||||
'EXRULE' => VObject\Property\ICalendar\Recur::class, // Deprecated since rfc5545
|
||||
'EXDATE' => Property\ICalendar\DateTime::class,
|
||||
'RDATE' => Property\ICalendar\DateTime::class,
|
||||
'RRULE' => Property\ICalendar\Recur::class,
|
||||
'EXRULE' => Property\ICalendar\Recur::class, // Deprecated since rfc5545
|
||||
|
||||
// Alarm Component Properties
|
||||
'ACTION' => VObject\Property\FlatText::class,
|
||||
'REPEAT' => VObject\Property\IntegerValue::class,
|
||||
'TRIGGER' => VObject\Property\ICalendar\Duration::class,
|
||||
'ACTION' => Property\FlatText::class,
|
||||
'REPEAT' => Property\IntegerValue::class,
|
||||
'TRIGGER' => Property\ICalendar\Duration::class,
|
||||
|
||||
// Change Management Component Properties
|
||||
'CREATED' => VObject\Property\ICalendar\DateTime::class,
|
||||
'DTSTAMP' => VObject\Property\ICalendar\DateTime::class,
|
||||
'LAST-MODIFIED' => VObject\Property\ICalendar\DateTime::class,
|
||||
'SEQUENCE' => VObject\Property\IntegerValue::class,
|
||||
'CREATED' => Property\ICalendar\DateTime::class,
|
||||
'DTSTAMP' => Property\ICalendar\DateTime::class,
|
||||
'LAST-MODIFIED' => Property\ICalendar\DateTime::class,
|
||||
'SEQUENCE' => Property\IntegerValue::class,
|
||||
|
||||
// Request Status
|
||||
'REQUEST-STATUS' => VObject\Property\Text::class,
|
||||
'REQUEST-STATUS' => Property\Text::class,
|
||||
|
||||
// Additions from draft-daboo-valarm-extensions-04
|
||||
'ALARM-AGENT' => VObject\Property\Text::class,
|
||||
'ACKNOWLEDGED' => VObject\Property\ICalendar\DateTime::class,
|
||||
'PROXIMITY' => VObject\Property\Text::class,
|
||||
'DEFAULT-ALARM' => VObject\Property\Boolean::class,
|
||||
'ALARM-AGENT' => Property\Text::class,
|
||||
'ACKNOWLEDGED' => Property\ICalendar\DateTime::class,
|
||||
'PROXIMITY' => Property\Text::class,
|
||||
'DEFAULT-ALARM' => Property\Boolean::class,
|
||||
|
||||
// Additions from draft-daboo-calendar-availability-05
|
||||
'BUSYTYPE' => VObject\Property\Text::class,
|
||||
'BUSYTYPE' => Property\Text::class,
|
||||
];
|
||||
|
||||
/**
|
||||
|
|
@ -166,10 +166,10 @@ class VCalendar extends VObject\Document
|
|||
*
|
||||
* @return VObject\Component[]
|
||||
*/
|
||||
public function getBaseComponents(string $componentName = null): array
|
||||
public function getBaseComponents(?string $componentName = null): array
|
||||
{
|
||||
$isBaseComponent = function ($component): bool {
|
||||
if (!$component instanceof VObject\Component) {
|
||||
if (!$component instanceof Component) {
|
||||
return false;
|
||||
}
|
||||
if ('VTIMEZONE' === $component->name) {
|
||||
|
|
@ -214,13 +214,11 @@ class VCalendar extends VObject\Document
|
|||
* If there is no such component, null will be returned.
|
||||
*
|
||||
* @param string|null $componentName filter by component name
|
||||
*
|
||||
* @return VObject\Component|null
|
||||
*/
|
||||
public function getBaseComponent(string $componentName = null): ?Component
|
||||
public function getBaseComponent(?string $componentName = null): ?Component
|
||||
{
|
||||
$isBaseComponent = function ($component): bool {
|
||||
if (!$component instanceof VObject\Component) {
|
||||
if (!$component instanceof Component) {
|
||||
return false;
|
||||
}
|
||||
if ('VTIMEZONE' === $component->name) {
|
||||
|
|
@ -275,7 +273,7 @@ class VCalendar extends VObject\Document
|
|||
* @throws InvalidDataException
|
||||
* @throws VObject\Recur\MaxInstancesExceededException
|
||||
*/
|
||||
public function expand(\DateTimeInterface $start, \DateTimeInterface $end, \DateTimeZone $timeZone = null): VCalendar
|
||||
public function expand(\DateTimeInterface $start, \DateTimeInterface $end, ?\DateTimeZone $timeZone = null): VCalendar
|
||||
{
|
||||
$newChildren = [];
|
||||
$recurringEvents = [];
|
||||
|
|
|
|||
|
|
@ -82,7 +82,7 @@ class VCard extends VObject\Document
|
|||
'ROLE' => VObject\Property\FlatText::class,
|
||||
'LOGO' => VObject\Property\Binary::class,
|
||||
// 'AGENT' => 'Sabre\\VObject\\Property\\', // Todo: is an embedded vCard. Probably rare, so
|
||||
// not supported at the moment
|
||||
// not supported at the moment
|
||||
'ORG' => VObject\Property\Text::class,
|
||||
'NOTE' => VObject\Property\FlatText::class,
|
||||
'REV' => VObject\Property\VCard\TimeStamp::class,
|
||||
|
|
|
|||
|
|
@ -104,7 +104,7 @@ class VTodo extends VObject\Component
|
|||
'LAST-MODIFIED' => '?',
|
||||
'LOCATION' => '?',
|
||||
'ORGANIZER' => '?',
|
||||
'PERCENT' => '?',
|
||||
'PERCENT-COMPLETE' => '?',
|
||||
'PRIORITY' => '?',
|
||||
'RECURRENCE-ID' => '?',
|
||||
'SEQUENCE' => '?',
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ class DateTimeParser
|
|||
*
|
||||
* @throws InvalidDataException
|
||||
*/
|
||||
public static function parseDateTime(string $dt, \DateTimeZone $tz = null): \DateTimeImmutable
|
||||
public static function parseDateTime(string $dt, ?\DateTimeZone $tz = null): \DateTimeImmutable
|
||||
{
|
||||
// Format is YYYYMMDD + "T" + hhmmss
|
||||
$result = preg_match('/^([0-9]{4})([0-1][0-9])([0-3][0-9])T([0-2][0-9])([0-5][0-9])([0-5][0-9])([Z]?)$/', $dt, $matches);
|
||||
|
|
@ -54,7 +54,7 @@ class DateTimeParser
|
|||
*
|
||||
* @throws InvalidDataException
|
||||
*/
|
||||
public static function parseDate(string $date, \DateTimeZone $tz = null): \DateTimeImmutable
|
||||
public static function parseDate(string $date, ?\DateTimeZone $tz = null): \DateTimeImmutable
|
||||
{
|
||||
// Format is YYYYMMDD
|
||||
$result = preg_match('/^([0-9]{4})([0-1][0-9])([0-3][0-9])$/', $date, $matches);
|
||||
|
|
@ -98,12 +98,12 @@ class DateTimeParser
|
|||
}
|
||||
|
||||
$parts = [
|
||||
'week',
|
||||
'day',
|
||||
'hour',
|
||||
'minute',
|
||||
'second',
|
||||
];
|
||||
'week',
|
||||
'day',
|
||||
'hour',
|
||||
'minute',
|
||||
'second',
|
||||
];
|
||||
|
||||
foreach ($parts as $part) {
|
||||
$matches[$part] = isset($matches[$part]) && $matches[$part] ? (int) $matches[$part] : 0;
|
||||
|
|
|
|||
|
|
@ -2,8 +2,6 @@
|
|||
|
||||
namespace Sabre\VObject;
|
||||
|
||||
use Sabre\VObject;
|
||||
|
||||
/**
|
||||
* Document.
|
||||
*
|
||||
|
|
@ -18,7 +16,7 @@ use Sabre\VObject;
|
|||
* @author Evert Pot (http://evertpot.com/)
|
||||
* @license http://sabre.io/license/ Modified BSD License
|
||||
*
|
||||
* @property VObject\Property\FlatText VERSION
|
||||
* @property Property\FlatText VERSION
|
||||
*/
|
||||
abstract class Document extends Component
|
||||
{
|
||||
|
|
@ -140,7 +138,7 @@ abstract class Document extends Component
|
|||
* an iCalendar object, this may be something like CALSCALE:GREGORIAN. To
|
||||
* ensure that this does not happen, set $defaults to false.
|
||||
*/
|
||||
public function createComponent(string $name, array $children = null, bool $defaults = true): Component
|
||||
public function createComponent(string $name, ?array $children = null, bool $defaults = true): Component
|
||||
{
|
||||
$name = strtoupper($name);
|
||||
$class = Component::class;
|
||||
|
|
@ -169,7 +167,7 @@ abstract class Document extends Component
|
|||
*
|
||||
* @throws InvalidDataException
|
||||
*/
|
||||
public function createProperty(string $name, $value = null, array $parameters = null, string $valueType = null): Property
|
||||
public function createProperty(string $name, $value = null, ?array $parameters = null, ?string $valueType = null, ?int $lineIndex = null, ?string $lineString = null): Property
|
||||
{
|
||||
// If there's a . in the name, it means it's prefixed by a group name.
|
||||
if (false !== ($i = strpos($name, '.'))) {
|
||||
|
|
@ -203,7 +201,7 @@ abstract class Document extends Component
|
|||
$parameters = [];
|
||||
}
|
||||
|
||||
return new $class($this, $name, $value, $parameters, $group);
|
||||
return new $class($this, $name, $value, $parameters, $group, $lineIndex, $lineString);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ class FreeBusyGenerator
|
|||
* Check the setTimeRange and setObjects methods for details about the
|
||||
* arguments.
|
||||
*/
|
||||
public function __construct(\DateTimeInterface $start = null, \DateTimeInterface $end = null, $objects = null, \DateTimeZone $timeZone = null)
|
||||
public function __construct(?\DateTimeInterface $start = null, ?\DateTimeInterface $end = null, $objects = null, ?\DateTimeZone $timeZone = null)
|
||||
{
|
||||
$this->setTimeRange($start, $end);
|
||||
|
||||
|
|
@ -138,7 +138,7 @@ class FreeBusyGenerator
|
|||
*
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function setTimeRange(\DateTimeInterface $start = null, \DateTimeInterface $end = null): void
|
||||
public function setTimeRange(?\DateTimeInterface $start = null, ?\DateTimeInterface $end = null): void
|
||||
{
|
||||
if (!$start) {
|
||||
$start = new \DateTimeImmutable(Settings::$minDate);
|
||||
|
|
|
|||
|
|
@ -113,7 +113,7 @@ class Broker
|
|||
* @throws MaxInstancesExceededException
|
||||
* @throws NoInstancesException
|
||||
*/
|
||||
public function processMessage(Message $itipMessage, VCalendar $existingObject = null)
|
||||
public function processMessage(Message $itipMessage, ?VCalendar $existingObject = null)
|
||||
{
|
||||
// We only support events at the moment.
|
||||
if ('VEVENT' !== $itipMessage->component) {
|
||||
|
|
@ -268,7 +268,7 @@ class Broker
|
|||
* This is message from an organizer, and is either a new event
|
||||
* invite, or an update to an existing one.
|
||||
*/
|
||||
protected function processMessageRequest(Message $itipMessage, VCalendar $existingObject = null): ?VCalendar
|
||||
protected function processMessageRequest(Message $itipMessage, ?VCalendar $existingObject = null): ?VCalendar
|
||||
{
|
||||
if (!$existingObject) {
|
||||
// This is a new invite, and we're just going to copy over
|
||||
|
|
@ -296,7 +296,7 @@ class Broker
|
|||
* attendee got removed from an event, or an event got cancelled
|
||||
* altogether.
|
||||
*/
|
||||
protected function processMessageCancel(Message $itipMessage, VCalendar $existingObject = null): ?VCalendar
|
||||
protected function processMessageCancel(Message $itipMessage, ?VCalendar $existingObject = null): ?VCalendar
|
||||
{
|
||||
if (!$existingObject) {
|
||||
// The event didn't exist in the first place, so we're just
|
||||
|
|
@ -321,7 +321,7 @@ class Broker
|
|||
* @throws MaxInstancesExceededException
|
||||
* @throws NoInstancesException
|
||||
*/
|
||||
protected function processMessageReply(Message $itipMessage, VCalendar $existingObject = null): ?VCalendar
|
||||
protected function processMessageReply(Message $itipMessage, ?VCalendar $existingObject = null): ?VCalendar
|
||||
{
|
||||
// A reply can only be processed based on an existing object.
|
||||
// If the object is not available, the reply is ignored.
|
||||
|
|
@ -333,7 +333,10 @@ class Broker
|
|||
|
||||
// Finding all the instances the attendee replied to.
|
||||
foreach ($itipMessage->message->VEVENT as $vevent) {
|
||||
$recurId = isset($vevent->{'RECURRENCE-ID'}) ? $vevent->{'RECURRENCE-ID'}->getValue() : 'master';
|
||||
// Use the Unix timestamp returned by getTimestamp as a unique identifier for the recurrence.
|
||||
// The Unix timestamp will be the same for an event, even if the reply from the attendee
|
||||
// used a different format/timezone to express the event date-time.
|
||||
$recurId = isset($vevent->{'RECURRENCE-ID'}) ? $vevent->{'RECURRENCE-ID'}->getDateTime()->getTimestamp() : 'master';
|
||||
$attendee = $vevent->ATTENDEE;
|
||||
$instances[$recurId] = $attendee['PARTSTAT']->getValue();
|
||||
if (isset($vevent->{'REQUEST-STATUS'})) {
|
||||
|
|
@ -346,7 +349,8 @@ class Broker
|
|||
// all the instances where we have a reply for.
|
||||
$masterObject = null;
|
||||
foreach ($existingObject->VEVENT as $vevent) {
|
||||
$recurId = isset($vevent->{'RECURRENCE-ID'}) ? $vevent->{'RECURRENCE-ID'}->getValue() : 'master';
|
||||
// Use the Unix timestamp returned by getTimestamp as a unique identifier for the recurrence.
|
||||
$recurId = isset($vevent->{'RECURRENCE-ID'}) ? $vevent->{'RECURRENCE-ID'}->getDateTime()->getTimestamp() : 'master';
|
||||
if ('master' === $recurId) {
|
||||
$masterObject = $vevent;
|
||||
}
|
||||
|
|
@ -393,7 +397,10 @@ class Broker
|
|||
$newObject = $recurrenceIterator->getEventObject();
|
||||
$recurrenceIterator->next();
|
||||
|
||||
if (isset($newObject->{'RECURRENCE-ID'}) && $newObject->{'RECURRENCE-ID'}->getValue() === $recurId) {
|
||||
// Compare the Unix timestamp returned by getTimestamp with the previously calculated timestamp.
|
||||
// If they are the same, then this is a matching recurrence, even though its date-time may have
|
||||
// been expressed in a different format/timezone.
|
||||
if (isset($newObject->{'RECURRENCE-ID'}) && $newObject->{'RECURRENCE-ID'}->getDateTime()->getTimestamp() === $recurId) {
|
||||
$found = true;
|
||||
}
|
||||
--$iterations;
|
||||
|
|
@ -496,10 +503,11 @@ class Broker
|
|||
$icalMsg->add(clone $timezone);
|
||||
}
|
||||
|
||||
if (!$attendee['newInstances']) {
|
||||
// If there are no instances the attendee is a part of, it
|
||||
// means the attendee was removed and we need to send him a
|
||||
// CANCEL.
|
||||
if (!$attendee['newInstances'] || 'CANCELLED' === $eventInfo['status']) {
|
||||
// If there are no instances the attendee is a part of, it means
|
||||
// the attendee was removed and we need to send them a CANCEL message.
|
||||
// Also If the meeting STATUS property was changed to CANCELLED
|
||||
// we need to send the attendee a CANCEL message.
|
||||
$message->method = 'CANCEL';
|
||||
|
||||
$icalMsg->METHOD = $message->method;
|
||||
|
|
|
|||
|
|
@ -131,7 +131,7 @@ class Json extends Parser
|
|||
list(
|
||||
$propertyName,
|
||||
$parameters,
|
||||
$valueType
|
||||
$valueType,
|
||||
) = $jProp;
|
||||
|
||||
$propertyName = strtoupper($propertyName);
|
||||
|
|
|
|||
|
|
@ -79,6 +79,12 @@ class MimeDir extends Parser
|
|||
$this->setInput($input);
|
||||
}
|
||||
|
||||
if (!\is_resource($this->input)) {
|
||||
// Null was passed as input, but there was no existing input buffer
|
||||
// There is nothing to parse.
|
||||
throw new ParseException('No input provided to parse');
|
||||
}
|
||||
|
||||
if (0 !== $options) {
|
||||
$this->options = $options;
|
||||
}
|
||||
|
|
@ -445,7 +451,7 @@ class MimeDir extends Parser
|
|||
}
|
||||
}
|
||||
|
||||
$propObj = $this->root->createProperty($property['name'], null, $namedParameters);
|
||||
$propObj = $this->root->createProperty($property['name'], null, $namedParameters, null, $this->startLine, $line);
|
||||
|
||||
foreach ($namelessParameters as $namelessParameter) {
|
||||
$propObj->add(null, $namelessParameter);
|
||||
|
|
|
|||
|
|
@ -51,6 +51,20 @@ abstract class Property extends Node
|
|||
*/
|
||||
public string $delimiter = ';';
|
||||
|
||||
/**
|
||||
* The line number in the original iCalendar / vCard file
|
||||
* that corresponds with the current node
|
||||
* if the node was read from a file.
|
||||
*/
|
||||
public ?int $lineIndex;
|
||||
|
||||
/**
|
||||
* The line string from the original iCalendar / vCard file
|
||||
* that corresponds with the current node
|
||||
* if the node was read from a file.
|
||||
*/
|
||||
public ?string $lineString;
|
||||
|
||||
/**
|
||||
* Creates the generic property.
|
||||
*
|
||||
|
|
@ -61,7 +75,7 @@ abstract class Property extends Node
|
|||
* @param array $parameters List of parameters
|
||||
* @param string|null $group The vcard property group
|
||||
*/
|
||||
public function __construct(Component $root, ?string $name, $value = null, array $parameters = [], string $group = null)
|
||||
public function __construct(Component $root, ?string $name, $value = null, array $parameters = [], ?string $group = null, ?int $lineIndex = null, ?string $lineString = null)
|
||||
{
|
||||
$this->name = $name;
|
||||
$this->group = $group;
|
||||
|
|
@ -75,6 +89,14 @@ abstract class Property extends Node
|
|||
if (!is_null($value)) {
|
||||
$this->setValue($value);
|
||||
}
|
||||
|
||||
if (!is_null($lineIndex)) {
|
||||
$this->lineIndex = $lineIndex;
|
||||
}
|
||||
|
||||
if (!is_null($lineString)) {
|
||||
$this->lineString = $lineString;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -128,7 +128,7 @@ class DateTime extends Property
|
|||
*
|
||||
* @throws InvalidDataException
|
||||
*/
|
||||
public function getDateTime(\DateTimeZone $timeZone = null): ?\DateTimeImmutable
|
||||
public function getDateTime(?\DateTimeZone $timeZone = null): ?\DateTimeImmutable
|
||||
{
|
||||
$dt = $this->getDateTimes($timeZone);
|
||||
if (!$dt) {
|
||||
|
|
@ -149,7 +149,7 @@ class DateTime extends Property
|
|||
*
|
||||
* @throws InvalidDataException
|
||||
*/
|
||||
public function getDateTimes(\DateTimeZone $timeZone = null): array
|
||||
public function getDateTimes(?\DateTimeZone $timeZone = null): array
|
||||
{
|
||||
// Does the property have a TZID?
|
||||
/** @var Property\FlatText $tzid */
|
||||
|
|
|
|||
|
|
@ -189,7 +189,14 @@ class Recur extends Property
|
|||
if (empty($part)) {
|
||||
continue;
|
||||
}
|
||||
list($partName, $partValue) = explode('=', $part);
|
||||
|
||||
$parts = explode('=', $part);
|
||||
|
||||
if (2 !== count($parts)) {
|
||||
throw new InvalidDataException('The supplied iCalendar RRULE part is incorrect: '.$part);
|
||||
}
|
||||
|
||||
list($partName, $partValue) = $parts;
|
||||
|
||||
// The value itself had multiple values..
|
||||
if (false !== strpos($partValue, ',')) {
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ class Text extends Property
|
|||
* @param array $parameters List of parameters
|
||||
* @param string|null $group The vcard property group
|
||||
*/
|
||||
public function __construct(Component $root, string $name, $value = null, array $parameters = [], string $group = null)
|
||||
public function __construct(Component $root, string $name, $value = null, array $parameters = [], ?string $group = null)
|
||||
{
|
||||
// There's two types of multi-valued text properties:
|
||||
// 1. multivalue properties.
|
||||
|
|
|
|||
|
|
@ -89,7 +89,7 @@ class EventIterator implements \Iterator
|
|||
* @throws NoInstancesException
|
||||
* @throws InvalidDataException
|
||||
*/
|
||||
public function __construct($input, string $uid = null, \DateTimeZone $timeZone = null)
|
||||
public function __construct($input, ?string $uid = null, ?\DateTimeZone $timeZone = null)
|
||||
{
|
||||
if (is_null($timeZone)) {
|
||||
$timeZone = new \DateTimeZone('UTC');
|
||||
|
|
|
|||
|
|
@ -159,6 +159,14 @@ class RRuleIterator implements \Iterator
|
|||
*/
|
||||
protected ?\DateTimeInterface $currentDate;
|
||||
|
||||
/**
|
||||
* The number of hours that the next occurrence of an event
|
||||
* jumped forward, usually because summer time started and
|
||||
* the requested time-of-day like 0230 did not exist on that
|
||||
* day. And so the event was scheduled 1 hour later at 0330.
|
||||
*/
|
||||
protected int $hourJump = 0;
|
||||
|
||||
/**
|
||||
* Frequency is one of: secondly, minutely, hourly, daily, weekly, monthly,
|
||||
* yearly.
|
||||
|
|
@ -276,12 +284,65 @@ class RRuleIterator implements \Iterator
|
|||
|
||||
/* Functions that advance the iterator {{{ */
|
||||
|
||||
/**
|
||||
* Gets the original start time of the RRULE.
|
||||
*
|
||||
* The value is formatted as a string with 24-hour:minute:second
|
||||
*/
|
||||
protected function startTime(): string
|
||||
{
|
||||
return $this->startDate->format('H:i:s');
|
||||
}
|
||||
|
||||
/**
|
||||
* Advances currentDate by the interval.
|
||||
* The time is set from the original startDate.
|
||||
* If the recurrence is on a day when summer time started, then the
|
||||
* time on that day may have jumped forward, for example, from 0230 to 0330.
|
||||
* Using the original time means that the next recurrence will be calculated
|
||||
* based on the original start time and the day/week/month/year interval.
|
||||
* So the start time of the next occurrence can correctly revert to 0230.
|
||||
*/
|
||||
protected function advanceTheDate(string $interval): void
|
||||
{
|
||||
$this->currentDate = $this->currentDate->modify($interval.' '.$this->startTime());
|
||||
}
|
||||
|
||||
/**
|
||||
* Does the processing for adjusting the time of multi-hourly events when summer time starts.
|
||||
*/
|
||||
protected function adjustForTimeJumpsOfHourlyEvent(\DateTimeInterface $previousEventDateTime): void
|
||||
{
|
||||
if (0 === $this->hourJump) {
|
||||
// Remember if the clock time jumped forward on the next occurrence.
|
||||
// That happens if the next event time is on a day when summer time starts
|
||||
// and the event time is in the non-existent hour of the day.
|
||||
// For example, an event that normally starts at 02:30 will
|
||||
// have to start at 03:30 on that day.
|
||||
// If the interval is just 1 hour, then there is no "jumping back" to do.
|
||||
// The events that day will happen, for example, at 0030 0130 0330 0430 0530...
|
||||
if ($this->interval > 1) {
|
||||
$expectedHourOfNextDate = ((int) $previousEventDateTime->format('G') + $this->interval) % 24;
|
||||
$actualHourOfNextDate = (int) $this->currentDate->format('G');
|
||||
$this->hourJump = $actualHourOfNextDate - $expectedHourOfNextDate;
|
||||
}
|
||||
} else {
|
||||
// The hour "jumped" for the previous occurrence, to avoid the non-existent time.
|
||||
// currentDate got set ahead by (usually) 1 hour on that day.
|
||||
// Adjust it back for this next occurrence.
|
||||
$this->currentDate = $this->currentDate->sub(new \DateInterval('PT'.$this->hourJump.'H'));
|
||||
$this->hourJump = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Does the processing for advancing the iterator for hourly frequency.
|
||||
*/
|
||||
protected function nextHourly(): void
|
||||
{
|
||||
$previousEventDateTime = clone $this->currentDate;
|
||||
$this->currentDate = $this->currentDate->modify('+'.$this->interval.' hours');
|
||||
$this->adjustForTimeJumpsOfHourlyEvent($previousEventDateTime);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -290,7 +351,7 @@ class RRuleIterator implements \Iterator
|
|||
protected function nextDaily(): void
|
||||
{
|
||||
if (!$this->byHour && !$this->byDay) {
|
||||
$this->currentDate = $this->currentDate->modify('+'.$this->interval.' days');
|
||||
$this->advanceTheDate('+'.$this->interval.' days');
|
||||
|
||||
return;
|
||||
}
|
||||
|
|
@ -349,7 +410,7 @@ class RRuleIterator implements \Iterator
|
|||
protected function nextWeekly(): void
|
||||
{
|
||||
if (!$this->byHour && !$this->byDay) {
|
||||
$this->currentDate = $this->currentDate->modify('+'.$this->interval.' weeks');
|
||||
$this->advanceTheDate('+'.$this->interval.' weeks');
|
||||
|
||||
return;
|
||||
}
|
||||
|
|
@ -371,7 +432,7 @@ class RRuleIterator implements \Iterator
|
|||
if ($this->byHour) {
|
||||
$this->currentDate = $this->currentDate->modify('+1 hours');
|
||||
} else {
|
||||
$this->currentDate = $this->currentDate->modify('+1 days');
|
||||
$this->advanceTheDate('+1 days');
|
||||
}
|
||||
|
||||
// Current day of the week
|
||||
|
|
@ -408,13 +469,13 @@ class RRuleIterator implements \Iterator
|
|||
// occur to the next month. We Must skip these invalid
|
||||
// entries.
|
||||
if ($currentDayOfMonth < 29) {
|
||||
$this->currentDate = $this->currentDate->modify('+'.$this->interval.' months');
|
||||
$this->advanceTheDate('+'.$this->interval.' months');
|
||||
} else {
|
||||
$increase = 0;
|
||||
do {
|
||||
++$increase;
|
||||
$tempDate = clone $this->currentDate;
|
||||
$tempDate = $tempDate->modify('+ '.($this->interval * $increase).' months');
|
||||
$tempDate = $tempDate->modify('+ '.($this->interval * $increase).' months '.$this->startTime());
|
||||
} while ($tempDate->format('j') != $currentDayOfMonth);
|
||||
$this->currentDate = $tempDate;
|
||||
}
|
||||
|
|
@ -465,11 +526,15 @@ class RRuleIterator implements \Iterator
|
|||
}
|
||||
}
|
||||
|
||||
// Set the currentDate to the year and month that we are in, and the day of the month that we have selected.
|
||||
// That day could be a day when summer time starts, and if the time of the event is, for example, 0230,
|
||||
// then 0230 will not be a valid time on that day. So always apply the start time from the original startDate.
|
||||
// The "modify" method will set the time forward to 0330, for example, if needed.
|
||||
$this->currentDate = $this->currentDate->setDate(
|
||||
(int) $this->currentDate->format('Y'),
|
||||
(int) $this->currentDate->format('n'),
|
||||
(int) $occurrence
|
||||
);
|
||||
)->modify($this->startTime());
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -586,7 +651,7 @@ class RRuleIterator implements \Iterator
|
|||
}
|
||||
|
||||
// The easiest form
|
||||
$this->currentDate = $this->currentDate->modify('+'.$this->interval.' years');
|
||||
$this->advanceTheDate('+'.$this->interval.' years');
|
||||
|
||||
return;
|
||||
}
|
||||
|
|
@ -610,7 +675,11 @@ class RRuleIterator implements \Iterator
|
|||
// If we advanced to the next month or year, the first
|
||||
// occurrence is always correct.
|
||||
if ($occurrence > $currentDayOfMonth || $advancedToNewMonth) {
|
||||
break 2;
|
||||
// only consider byMonth matches,
|
||||
// otherwise, we don't follow RRule correctly
|
||||
if (in_array($currentMonth, $this->byMonth)) {
|
||||
break 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -646,7 +715,7 @@ class RRuleIterator implements \Iterator
|
|||
(int) $currentYear,
|
||||
(int) $currentMonth,
|
||||
(int) $occurrence
|
||||
);
|
||||
)->modify($this->startTime());
|
||||
|
||||
return;
|
||||
} else {
|
||||
|
|
@ -663,7 +732,7 @@ class RRuleIterator implements \Iterator
|
|||
(int) $currentYear,
|
||||
(int) $currentMonth,
|
||||
(int) $currentDayOfMonth
|
||||
);
|
||||
)->modify($this->startTime());
|
||||
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ class ICalendar implements SplitterInterface
|
|||
{
|
||||
$data = VObject\Reader::read($input, $options);
|
||||
|
||||
if (!$data instanceof VObject\Component\VCalendar) {
|
||||
if (!$data instanceof VCalendar) {
|
||||
throw new VObject\ParseException('Supplied input could not be parsed as VCALENDAR.');
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ class VCard implements SplitterInterface
|
|||
try {
|
||||
$object = $this->parser->parse();
|
||||
|
||||
if (!$object instanceof VObject\Component\VCard) {
|
||||
if (!$object instanceof Component\VCard) {
|
||||
throw new VObject\ParseException('The supplied input contained non-VCARD data.');
|
||||
}
|
||||
} catch (VObject\EofException $e) {
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ class TimeZoneUtil
|
|||
* Alternatively, if $failIfUncertain is set to true, it will throw an
|
||||
* exception if we cannot accurately determine the timezone.
|
||||
*/
|
||||
private function findTimeZone(string $tzid, Component $vcalendar = null, bool $failIfUncertain = false): \DateTimeZone
|
||||
private function findTimeZone(string $tzid, ?Component $vcalendar = null, bool $failIfUncertain = false): \DateTimeZone
|
||||
{
|
||||
foreach ($this->timezoneFinders as $timezoneFinder) {
|
||||
$timezone = $timezoneFinder->find($tzid, $failIfUncertain);
|
||||
|
|
@ -117,7 +117,7 @@ class TimeZoneUtil
|
|||
self::getInstance()->addFinder($key, $finder);
|
||||
}
|
||||
|
||||
public static function getTimeZone(string $tzid, Component $vcalendar = null, bool $failIfUncertain = false): \DateTimeZone
|
||||
public static function getTimeZone(string $tzid, ?Component $vcalendar = null, bool $failIfUncertain = false): \DateTimeZone
|
||||
{
|
||||
return self::getInstance()->findTimeZone($tzid, $vcalendar, $failIfUncertain);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -93,8 +93,8 @@ class VCardConverter
|
|||
);
|
||||
|
||||
if (Document::VCARD30 === $targetVersion) {
|
||||
if ($property instanceof Property\Uri && in_array($property->name, ['PHOTO', 'LOGO', 'SOUND'])) {
|
||||
/** @var Property\Uri $newProperty */
|
||||
if ($property instanceof Uri && in_array($property->name, ['PHOTO', 'LOGO', 'SOUND'])) {
|
||||
/** @var Uri $newProperty */
|
||||
$newProperty = $this->convertUriToBinary($output, $newProperty);
|
||||
} elseif ($property instanceof Property\VCard\DateAndOrTime) {
|
||||
// In vCard 4, the birth year may be optional. This is not the
|
||||
|
|
@ -153,8 +153,8 @@ class VCardConverter
|
|||
return;
|
||||
}
|
||||
|
||||
if ($property instanceof Property\Binary) {
|
||||
/** @var Property\Binary $newProperty */
|
||||
if ($property instanceof Binary) {
|
||||
/** @var Binary $newProperty */
|
||||
$newProperty = $this->convertBinaryToUri($output, $newProperty, $parameters);
|
||||
} elseif ($property instanceof Property\VCard\DateAndOrTime && isset($parameters['X-APPLE-OMIT-YEAR'])) {
|
||||
// If a property such as BDAY contained 'X-APPLE-OMIT-YEAR',
|
||||
|
|
@ -256,7 +256,7 @@ class VCardConverter
|
|||
*
|
||||
* @throws InvalidDataException
|
||||
*/
|
||||
protected function convertBinaryToUri(Component\VCard $output, Property\Binary $newProperty, array &$parameters): Uri
|
||||
protected function convertBinaryToUri(Component\VCard $output, Binary $newProperty, array &$parameters): Uri
|
||||
{
|
||||
$value = $newProperty->getValue();
|
||||
/** @var Uri $newProperty */
|
||||
|
|
@ -304,11 +304,11 @@ class VCardConverter
|
|||
* be valid in vCard 3.0 as well, we should convert those to BINARY if
|
||||
* possible, to improve compatibility.
|
||||
*
|
||||
* @return Property\Binary|Property\Uri|null
|
||||
* @return Binary|Uri|null
|
||||
*
|
||||
* @throws InvalidDataException
|
||||
*/
|
||||
protected function convertUriToBinary(Component\VCard $output, Property\Uri $newProperty): Property
|
||||
protected function convertUriToBinary(Component\VCard $output, Uri $newProperty): Property
|
||||
{
|
||||
$value = $newProperty->getValue();
|
||||
|
||||
|
|
|
|||
|
|
@ -11,142 +11,142 @@
|
|||
*/
|
||||
|
||||
return [
|
||||
'AUS Central Standard Time' => 'Australia/Darwin',
|
||||
'AUS Eastern Standard Time' => 'Australia/Sydney',
|
||||
'Afghanistan Standard Time' => 'Asia/Kabul',
|
||||
'Alaskan Standard Time' => 'America/Anchorage',
|
||||
'Aleutian Standard Time' => 'America/Adak',
|
||||
'Altai Standard Time' => 'Asia/Barnaul',
|
||||
'Arab Standard Time' => 'Asia/Riyadh',
|
||||
'Arabian Standard Time' => 'Asia/Dubai',
|
||||
'Arabic Standard Time' => 'Asia/Baghdad',
|
||||
'Argentina Standard Time' => 'America/Buenos_Aires',
|
||||
'Astrakhan Standard Time' => 'Europe/Astrakhan',
|
||||
'Atlantic Standard Time' => 'America/Halifax',
|
||||
'Aus Central W. Standard Time' => 'Australia/Eucla',
|
||||
'Azerbaijan Standard Time' => 'Asia/Baku',
|
||||
'Azores Standard Time' => 'Atlantic/Azores',
|
||||
'Bahia Standard Time' => 'America/Bahia',
|
||||
'Bangladesh Standard Time' => 'Asia/Dhaka',
|
||||
'Belarus Standard Time' => 'Europe/Minsk',
|
||||
'Bougainville Standard Time' => 'Pacific/Bougainville',
|
||||
'Canada Central Standard Time' => 'America/Regina',
|
||||
'Cape Verde Standard Time' => 'Atlantic/Cape_Verde',
|
||||
'Caucasus Standard Time' => 'Asia/Yerevan',
|
||||
'Cen. Australia Standard Time' => 'Australia/Adelaide',
|
||||
'Central America Standard Time' => 'America/Guatemala',
|
||||
'Central Asia Standard Time' => 'Asia/Almaty',
|
||||
'Central Brazilian Standard Time' => 'America/Cuiaba',
|
||||
'Central Europe Standard Time' => 'Europe/Budapest',
|
||||
'Central European Standard Time' => 'Europe/Warsaw',
|
||||
'Central Pacific Standard Time' => 'Pacific/Guadalcanal',
|
||||
'Central Standard Time' => 'America/Chicago',
|
||||
'Central Standard Time (Mexico)' => 'America/Mexico_City',
|
||||
'Chatham Islands Standard Time' => 'Pacific/Chatham',
|
||||
'China Standard Time' => 'Asia/Shanghai',
|
||||
'Cuba Standard Time' => 'America/Havana',
|
||||
'Dateline Standard Time' => 'Etc/GMT+12',
|
||||
'E. Africa Standard Time' => 'Africa/Nairobi',
|
||||
'E. Australia Standard Time' => 'Australia/Brisbane',
|
||||
'E. Europe Standard Time' => 'Europe/Chisinau',
|
||||
'E. South America Standard Time' => 'America/Sao_Paulo',
|
||||
'Easter Island Standard Time' => 'Pacific/Easter',
|
||||
'Eastern Standard Time' => 'America/New_York',
|
||||
'Eastern Standard Time (Mexico)' => 'America/Cancun',
|
||||
'Egypt Standard Time' => 'Africa/Cairo',
|
||||
'Ekaterinburg Standard Time' => 'Asia/Yekaterinburg',
|
||||
'FLE Standard Time' => 'Europe/Kiev',
|
||||
'Fiji Standard Time' => 'Pacific/Fiji',
|
||||
'GMT Standard Time' => 'Europe/London',
|
||||
'GTB Standard Time' => 'Europe/Bucharest',
|
||||
'Georgian Standard Time' => 'Asia/Tbilisi',
|
||||
'Greenland Standard Time' => 'America/Godthab',
|
||||
'Greenwich Standard Time' => 'Atlantic/Reykjavik',
|
||||
'Haiti Standard Time' => 'America/Port-au-Prince',
|
||||
'Hawaiian Standard Time' => 'Pacific/Honolulu',
|
||||
'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',
|
||||
'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',
|
||||
'Middle East Standard Time' => 'Asia/Beirut',
|
||||
'Montevideo Standard Time' => 'America/Montevideo',
|
||||
'Morocco Standard Time' => 'Africa/Casablanca',
|
||||
'Mountain Standard Time' => 'America/Denver',
|
||||
'Mountain Standard Time (Mexico)' => 'America/Chihuahua',
|
||||
'Myanmar Standard Time' => 'Asia/Rangoon',
|
||||
'N. Central Asia Standard Time' => 'Asia/Novosibirsk',
|
||||
'Namibia Standard Time' => 'Africa/Windhoek',
|
||||
'Nepal Standard Time' => 'Asia/Katmandu',
|
||||
'New Zealand Standard Time' => 'Pacific/Auckland',
|
||||
'Newfoundland Standard Time' => 'America/St_Johns',
|
||||
'Norfolk Standard Time' => 'Pacific/Norfolk',
|
||||
'North Asia East Standard Time' => 'Asia/Irkutsk',
|
||||
'North Asia Standard Time' => 'Asia/Krasnoyarsk',
|
||||
'North Korea Standard Time' => 'Asia/Pyongyang',
|
||||
'Omsk Standard Time' => 'Asia/Omsk',
|
||||
'Pacific SA Standard Time' => 'America/Santiago',
|
||||
'Pacific Standard Time' => 'America/Los_Angeles',
|
||||
'Pacific Standard Time (Mexico)' => 'America/Tijuana',
|
||||
'Pakistan Standard Time' => 'Asia/Karachi',
|
||||
'Paraguay Standard Time' => 'America/Asuncion',
|
||||
'Qyzylorda Standard Time' => 'Asia/Qyzylorda',
|
||||
'Romance Standard Time' => 'Europe/Paris',
|
||||
'Russia Time Zone 10' => 'Asia/Srednekolymsk',
|
||||
'Russia Time Zone 11' => 'Asia/Kamchatka',
|
||||
'Russia Time Zone 3' => 'Europe/Samara',
|
||||
'Russian Standard Time' => 'Europe/Moscow',
|
||||
'SA Eastern Standard Time' => 'America/Cayenne',
|
||||
'SA Pacific Standard Time' => 'America/Bogota',
|
||||
'SA Western Standard Time' => 'America/La_Paz',
|
||||
'SE Asia Standard Time' => 'Asia/Bangkok',
|
||||
'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',
|
||||
'Singapore Standard Time' => 'Asia/Singapore',
|
||||
'South Africa Standard Time' => 'Africa/Johannesburg',
|
||||
'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/Hobart',
|
||||
'Tocantins Standard Time' => 'America/Araguaina',
|
||||
'Tokyo Standard Time' => 'Asia/Tokyo',
|
||||
'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',
|
||||
'US Eastern Standard Time' => 'America/Indianapolis',
|
||||
'US Mountain Standard Time' => 'America/Phoenix',
|
||||
'UTC' => 'Etc/GMT',
|
||||
'UTC+12' => 'Etc/GMT-12',
|
||||
'UTC+13' => 'Etc/GMT-13',
|
||||
'UTC-02' => 'Etc/GMT+2',
|
||||
'UTC-08' => 'Etc/GMT+8',
|
||||
'UTC-09' => 'Etc/GMT+9',
|
||||
'UTC-11' => 'Etc/GMT+11',
|
||||
'Ulaanbaatar Standard Time' => 'Asia/Ulaanbaatar',
|
||||
'Venezuela Standard Time' => 'America/Caracas',
|
||||
'Vladivostok Standard Time' => 'Asia/Vladivostok',
|
||||
'Volgograd Standard Time' => 'Europe/Volgograd',
|
||||
'W. Australia Standard Time' => 'Australia/Perth',
|
||||
'W. Central Africa Standard Time' => 'Africa/Lagos',
|
||||
'W. Europe Standard Time' => 'Europe/Berlin',
|
||||
'W. Mongolia Standard Time' => 'Asia/Hovd',
|
||||
'West Asia Standard Time' => 'Asia/Tashkent',
|
||||
'West Bank Standard Time' => 'Asia/Hebron',
|
||||
'West Pacific Standard Time' => 'Pacific/Port_Moresby',
|
||||
'Yakutsk Standard Time' => 'Asia/Yakutsk',
|
||||
'Yukon Standard Time' => 'America/Whitehorse',
|
||||
'AUS Central Standard Time' => 'Australia/Darwin',
|
||||
'AUS Eastern Standard Time' => 'Australia/Sydney',
|
||||
'Afghanistan Standard Time' => 'Asia/Kabul',
|
||||
'Alaskan Standard Time' => 'America/Anchorage',
|
||||
'Aleutian Standard Time' => 'America/Adak',
|
||||
'Altai Standard Time' => 'Asia/Barnaul',
|
||||
'Arab Standard Time' => 'Asia/Riyadh',
|
||||
'Arabian Standard Time' => 'Asia/Dubai',
|
||||
'Arabic Standard Time' => 'Asia/Baghdad',
|
||||
'Argentina Standard Time' => 'America/Buenos_Aires',
|
||||
'Astrakhan Standard Time' => 'Europe/Astrakhan',
|
||||
'Atlantic Standard Time' => 'America/Halifax',
|
||||
'Aus Central W. Standard Time' => 'Australia/Eucla',
|
||||
'Azerbaijan Standard Time' => 'Asia/Baku',
|
||||
'Azores Standard Time' => 'Atlantic/Azores',
|
||||
'Bahia Standard Time' => 'America/Bahia',
|
||||
'Bangladesh Standard Time' => 'Asia/Dhaka',
|
||||
'Belarus Standard Time' => 'Europe/Minsk',
|
||||
'Bougainville Standard Time' => 'Pacific/Bougainville',
|
||||
'Canada Central Standard Time' => 'America/Regina',
|
||||
'Cape Verde Standard Time' => 'Atlantic/Cape_Verde',
|
||||
'Caucasus Standard Time' => 'Asia/Yerevan',
|
||||
'Cen. Australia Standard Time' => 'Australia/Adelaide',
|
||||
'Central America Standard Time' => 'America/Guatemala',
|
||||
'Central Asia Standard Time' => 'Asia/Almaty',
|
||||
'Central Brazilian Standard Time' => 'America/Cuiaba',
|
||||
'Central Europe Standard Time' => 'Europe/Budapest',
|
||||
'Central European Standard Time' => 'Europe/Warsaw',
|
||||
'Central Pacific Standard Time' => 'Pacific/Guadalcanal',
|
||||
'Central Standard Time' => 'America/Chicago',
|
||||
'Central Standard Time (Mexico)' => 'America/Mexico_City',
|
||||
'Chatham Islands Standard Time' => 'Pacific/Chatham',
|
||||
'China Standard Time' => 'Asia/Shanghai',
|
||||
'Cuba Standard Time' => 'America/Havana',
|
||||
'Dateline Standard Time' => 'Etc/GMT+12',
|
||||
'E. Africa Standard Time' => 'Africa/Nairobi',
|
||||
'E. Australia Standard Time' => 'Australia/Brisbane',
|
||||
'E. Europe Standard Time' => 'Europe/Chisinau',
|
||||
'E. South America Standard Time' => 'America/Sao_Paulo',
|
||||
'Easter Island Standard Time' => 'Pacific/Easter',
|
||||
'Eastern Standard Time' => 'America/New_York',
|
||||
'Eastern Standard Time (Mexico)' => 'America/Cancun',
|
||||
'Egypt Standard Time' => 'Africa/Cairo',
|
||||
'Ekaterinburg Standard Time' => 'Asia/Yekaterinburg',
|
||||
'FLE Standard Time' => 'Europe/Kiev',
|
||||
'Fiji Standard Time' => 'Pacific/Fiji',
|
||||
'GMT Standard Time' => 'Europe/London',
|
||||
'GTB Standard Time' => 'Europe/Bucharest',
|
||||
'Georgian Standard Time' => 'Asia/Tbilisi',
|
||||
'Greenland Standard Time' => 'America/Godthab',
|
||||
'Greenwich Standard Time' => 'Atlantic/Reykjavik',
|
||||
'Haiti Standard Time' => 'America/Port-au-Prince',
|
||||
'Hawaiian Standard Time' => 'Pacific/Honolulu',
|
||||
'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',
|
||||
'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',
|
||||
'Middle East Standard Time' => 'Asia/Beirut',
|
||||
'Montevideo Standard Time' => 'America/Montevideo',
|
||||
'Morocco Standard Time' => 'Africa/Casablanca',
|
||||
'Mountain Standard Time' => 'America/Denver',
|
||||
'Mountain Standard Time (Mexico)' => 'America/Chihuahua',
|
||||
'Myanmar Standard Time' => 'Asia/Rangoon',
|
||||
'N. Central Asia Standard Time' => 'Asia/Novosibirsk',
|
||||
'Namibia Standard Time' => 'Africa/Windhoek',
|
||||
'Nepal Standard Time' => 'Asia/Katmandu',
|
||||
'New Zealand Standard Time' => 'Pacific/Auckland',
|
||||
'Newfoundland Standard Time' => 'America/St_Johns',
|
||||
'Norfolk Standard Time' => 'Pacific/Norfolk',
|
||||
'North Asia East Standard Time' => 'Asia/Irkutsk',
|
||||
'North Asia Standard Time' => 'Asia/Krasnoyarsk',
|
||||
'North Korea Standard Time' => 'Asia/Pyongyang',
|
||||
'Omsk Standard Time' => 'Asia/Omsk',
|
||||
'Pacific SA Standard Time' => 'America/Santiago',
|
||||
'Pacific Standard Time' => 'America/Los_Angeles',
|
||||
'Pacific Standard Time (Mexico)' => 'America/Tijuana',
|
||||
'Pakistan Standard Time' => 'Asia/Karachi',
|
||||
'Paraguay Standard Time' => 'America/Asuncion',
|
||||
'Qyzylorda Standard Time' => 'Asia/Qyzylorda',
|
||||
'Romance Standard Time' => 'Europe/Paris',
|
||||
'Russia Time Zone 10' => 'Asia/Srednekolymsk',
|
||||
'Russia Time Zone 11' => 'Asia/Kamchatka',
|
||||
'Russia Time Zone 3' => 'Europe/Samara',
|
||||
'Russian Standard Time' => 'Europe/Moscow',
|
||||
'SA Eastern Standard Time' => 'America/Cayenne',
|
||||
'SA Pacific Standard Time' => 'America/Bogota',
|
||||
'SA Western Standard Time' => 'America/La_Paz',
|
||||
'SE Asia Standard Time' => 'Asia/Bangkok',
|
||||
'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',
|
||||
'Singapore Standard Time' => 'Asia/Singapore',
|
||||
'South Africa Standard Time' => 'Africa/Johannesburg',
|
||||
'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/Hobart',
|
||||
'Tocantins Standard Time' => 'America/Araguaina',
|
||||
'Tokyo Standard Time' => 'Asia/Tokyo',
|
||||
'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',
|
||||
'US Eastern Standard Time' => 'America/Indianapolis',
|
||||
'US Mountain Standard Time' => 'America/Phoenix',
|
||||
'UTC' => 'Etc/GMT',
|
||||
'UTC+12' => 'Etc/GMT-12',
|
||||
'UTC+13' => 'Etc/GMT-13',
|
||||
'UTC-02' => 'Etc/GMT+2',
|
||||
'UTC-08' => 'Etc/GMT+8',
|
||||
'UTC-09' => 'Etc/GMT+9',
|
||||
'UTC-11' => 'Etc/GMT+11',
|
||||
'Ulaanbaatar Standard Time' => 'Asia/Ulaanbaatar',
|
||||
'Venezuela Standard Time' => 'America/Caracas',
|
||||
'Vladivostok Standard Time' => 'Asia/Vladivostok',
|
||||
'Volgograd Standard Time' => 'Europe/Volgograd',
|
||||
'W. Australia Standard Time' => 'Australia/Perth',
|
||||
'W. Central Africa Standard Time' => 'Africa/Lagos',
|
||||
'W. Europe Standard Time' => 'Europe/Berlin',
|
||||
'W. Mongolia Standard Time' => 'Asia/Hovd',
|
||||
'West Asia Standard Time' => 'Asia/Tashkent',
|
||||
'West Bank Standard Time' => 'Asia/Hebron',
|
||||
'West Pacific Standard Time' => 'Pacific/Port_Moresby',
|
||||
'Yakutsk Standard Time' => 'Asia/Yakutsk',
|
||||
'Yukon Standard Time' => 'America/Whitehorse',
|
||||
];
|
||||
|
|
|
|||
|
|
@ -110,7 +110,7 @@ trait ContextStackTrait
|
|||
$this->elementMap,
|
||||
$this->contextUri,
|
||||
$this->namespaceMap,
|
||||
$this->classMap
|
||||
$this->classMap,
|
||||
) = array_pop($this->contextStack);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -109,10 +109,17 @@ class Service
|
|||
*/
|
||||
public function parse($input, ?string $contextUri = null, ?string &$rootElementName = null)
|
||||
{
|
||||
if (is_resource($input)) {
|
||||
if (!is_string($input)) {
|
||||
// Unfortunately the XMLReader doesn't support streams. When it
|
||||
// does, we can optimize this.
|
||||
$input = (string) stream_get_contents($input);
|
||||
if (is_resource($input)) {
|
||||
$input = (string) stream_get_contents($input);
|
||||
} else {
|
||||
// Input is not a string and not a resource.
|
||||
// Therefore, it has to be a closed resource.
|
||||
// Effectively empty input has been passed in.
|
||||
$input = '';
|
||||
}
|
||||
}
|
||||
|
||||
// If input is empty, then it's safe to throw an exception
|
||||
|
|
@ -153,10 +160,17 @@ class Service
|
|||
*/
|
||||
public function expect($rootElementName, $input, ?string $contextUri = null)
|
||||
{
|
||||
if (is_resource($input)) {
|
||||
if (!is_string($input)) {
|
||||
// Unfortunately the XMLReader doesn't support streams. When it
|
||||
// does, we can optimize this.
|
||||
$input = (string) stream_get_contents($input);
|
||||
if (is_resource($input)) {
|
||||
$input = (string) stream_get_contents($input);
|
||||
} else {
|
||||
// Input is not a string and not a resource.
|
||||
// Therefore, it has to be a closed resource.
|
||||
// Effectively empty input has been passed in.
|
||||
$input = '';
|
||||
}
|
||||
}
|
||||
|
||||
// If input is empty, then it's safe to throw an exception
|
||||
|
|
|
|||
|
|
@ -16,5 +16,5 @@ class Version
|
|||
/**
|
||||
* Full version number.
|
||||
*/
|
||||
public const VERSION = '4.0.5';
|
||||
public const VERSION = '4.0.6';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -236,7 +236,7 @@ class Writer extends \XMLWriter
|
|||
|
||||
list(
|
||||
$namespace,
|
||||
$localName
|
||||
$localName,
|
||||
) = Service::parseClarkNotation($name);
|
||||
|
||||
if (array_key_exists($namespace, $this->namespaceMap)) {
|
||||
|
|
|
|||
|
|
@ -130,6 +130,8 @@
|
|||
"QUOTA_SIZE": "Using <strong>%SIZE% (%PROC%%)<\/strong> of your <strong>%LIMIT%<\/strong>",
|
||||
"SORT": "Sort",
|
||||
"NO_SORT": "Mail server does not support sorting",
|
||||
"SORT_ARRIVAL_ASC": "Arrived ascending",
|
||||
"SORT_ARRIVAL_DESC": "Arrived descending",
|
||||
"SORT_DATE_ASC": "Date ascending",
|
||||
"SORT_DATE_DESC": "Date descending",
|
||||
"SORT_SIZE_ASC": "Size ascending",
|
||||
|
|
|
|||
|
|
@ -130,6 +130,8 @@
|
|||
"QUOTA_SIZE": "Выкарыстана <strong>%SIZE% (%PROC%%)<\/strong> з <strong>%LIMIT%<\/strong>",
|
||||
"SORT": "Сартаванне",
|
||||
"NO_SORT": "Паштовы сервер не падтрымлівае сартаванне",
|
||||
"SORT_ARRIVAL_ASC": "Arrived ascending",
|
||||
"SORT_ARRIVAL_DESC": "Arrived descending",
|
||||
"SORT_DATE_ASC": "Дата па ўзрастанні",
|
||||
"SORT_DATE_DESC": "Дата па спаданні",
|
||||
"SORT_SIZE_ASC": "Памер па ўзрастанні",
|
||||
|
|
|
|||
|
|
@ -130,6 +130,8 @@
|
|||
"QUOTA_SIZE": "Използване на <strong>%SIZE% (%PROC%%)<\/strong> от вашите <strong>%LIMIT%<\/strong>",
|
||||
"SORT": "Sort",
|
||||
"NO_SORT": "Mail server does not support sorting",
|
||||
"SORT_ARRIVAL_ASC": "Arrived ascending",
|
||||
"SORT_ARRIVAL_DESC": "Arrived descending",
|
||||
"SORT_DATE_ASC": "Date ascending",
|
||||
"SORT_DATE_DESC": "Date descending",
|
||||
"SORT_SIZE_ASC": "Size ascending",
|
||||
|
|
|
|||
|
|
@ -130,6 +130,8 @@
|
|||
"QUOTA_SIZE": "Používáte <strong>%SIZE% (%PROC%%)<\/strong> z vašich <strong>%LIMIT%<\/strong>",
|
||||
"SORT": "Sort",
|
||||
"NO_SORT": "Mail server does not support sorting",
|
||||
"SORT_ARRIVAL_ASC": "Arrived ascending",
|
||||
"SORT_ARRIVAL_DESC": "Arrived descending",
|
||||
"SORT_DATE_ASC": "Date ascending",
|
||||
"SORT_DATE_DESC": "Date descending",
|
||||
"SORT_SIZE_ASC": "Size ascending",
|
||||
|
|
|
|||
|
|
@ -130,6 +130,8 @@
|
|||
"QUOTA_SIZE": "Du bruger <strong>%SIZE% (%PROC%%)<\/strong> af <strong>%LIMIT%<\/strong>",
|
||||
"SORT": "Sort",
|
||||
"NO_SORT": "Mail server does not support sorting",
|
||||
"SORT_ARRIVAL_ASC": "Arrived ascending",
|
||||
"SORT_ARRIVAL_DESC": "Arrived descending",
|
||||
"SORT_DATE_ASC": "Date ascending",
|
||||
"SORT_DATE_DESC": "Date descending",
|
||||
"SORT_SIZE_ASC": "Size ascending",
|
||||
|
|
|
|||
|
|
@ -130,6 +130,8 @@
|
|||
"QUOTA_SIZE": "Sie nutzen <strong>%SIZE% (%PROC%%)<\/strong> von <strong>%LIMIT%<\/strong>.",
|
||||
"SORT": "Sortieren",
|
||||
"NO_SORT": "Ihr Mailserver unterstützt das Sortieren von E-Mails nicht",
|
||||
"SORT_ARRIVAL_ASC": "Arrived ascending",
|
||||
"SORT_ARRIVAL_DESC": "Arrived descending",
|
||||
"SORT_DATE_ASC": "Datum aufsteigend",
|
||||
"SORT_DATE_DESC": "Datum absteigend",
|
||||
"SORT_SIZE_ASC": "Größe aufsteigend",
|
||||
|
|
|
|||
|
|
@ -130,6 +130,8 @@
|
|||
"QUOTA_SIZE": "Χρησιμοποιούνται <strong>%SIZE% (%PROC%%)<\/strong> από <strong>%LIMIT%<\/strong>",
|
||||
"SORT": "Sort",
|
||||
"NO_SORT": "Mail server does not support sorting",
|
||||
"SORT_ARRIVAL_ASC": "Arrived ascending",
|
||||
"SORT_ARRIVAL_DESC": "Arrived descending",
|
||||
"SORT_DATE_ASC": "Date ascending",
|
||||
"SORT_DATE_DESC": "Date descending",
|
||||
"SORT_SIZE_ASC": "Size ascending",
|
||||
|
|
|
|||
|
|
@ -130,6 +130,8 @@
|
|||
"QUOTA_SIZE": "Using <strong>%SIZE% (%PROC%%)<\/strong> of your <strong>%LIMIT%<\/strong>",
|
||||
"SORT": "Sort",
|
||||
"NO_SORT": "Mail server does not support sorting",
|
||||
"SORT_ARRIVAL_ASC": "Arrived ascending",
|
||||
"SORT_ARRIVAL_DESC": "Arrived descending",
|
||||
"SORT_DATE_ASC": "Date ascending",
|
||||
"SORT_DATE_DESC": "Date descending",
|
||||
"SORT_SIZE_ASC": "Size ascending",
|
||||
|
|
|
|||
|
|
@ -130,6 +130,8 @@
|
|||
"QUOTA_SIZE": "Usando <strong>%SIZE% (%PROC%%)<\/strong> de sus <strong>%LIMIT%<\/strong>",
|
||||
"SORT": "Ordenar",
|
||||
"NO_SORT": "El servidor no soporta clasificación",
|
||||
"SORT_ARRIVAL_ASC": "Arrived ascending",
|
||||
"SORT_ARRIVAL_DESC": "Arrived descending",
|
||||
"SORT_DATE_ASC": "Fecha ascendente",
|
||||
"SORT_DATE_DESC": "Fecha descendente",
|
||||
"SORT_SIZE_ASC": "Tamaño ascendente",
|
||||
|
|
|
|||
|
|
@ -130,6 +130,8 @@
|
|||
"QUOTA_SIZE": "Kasutad <strong>%SIZE% (%PROC%%)<\/strong> lubatud kettamahust <strong>%LIMIT%<\/strong>",
|
||||
"SORT": "Sort",
|
||||
"NO_SORT": "Mail server does not support sorting",
|
||||
"SORT_ARRIVAL_ASC": "Arrived ascending",
|
||||
"SORT_ARRIVAL_DESC": "Arrived descending",
|
||||
"SORT_DATE_ASC": "Date ascending",
|
||||
"SORT_DATE_DESC": "Date descending",
|
||||
"SORT_SIZE_ASC": "Size ascending",
|
||||
|
|
|
|||
|
|
@ -130,6 +130,8 @@
|
|||
"QUOTA_SIZE": "<strong>%SIZE% (%PROC%%)<\/strong> erabiliz zure <strong>%LIMIT%<\/strong>-tik",
|
||||
"SORT": "Ordenatu",
|
||||
"NO_SORT": "Mail server does not support sorting",
|
||||
"SORT_ARRIVAL_ASC": "Arrived ascending",
|
||||
"SORT_ARRIVAL_DESC": "Arrived descending",
|
||||
"SORT_DATE_ASC": "Data goraka",
|
||||
"SORT_DATE_DESC": "Data beheraka",
|
||||
"SORT_SIZE_ASC": "Tamaina goraka",
|
||||
|
|
|
|||
|
|
@ -130,6 +130,8 @@
|
|||
"QUOTA_SIZE": "استفاده <strong>%SIZE% (%PROC%%)<\/strong> از<strong>%LIMIT%<\/strong> برای شما",
|
||||
"SORT": "Sort",
|
||||
"NO_SORT": "Mail server does not support sorting",
|
||||
"SORT_ARRIVAL_ASC": "Arrived ascending",
|
||||
"SORT_ARRIVAL_DESC": "Arrived descending",
|
||||
"SORT_DATE_ASC": "Date ascending",
|
||||
"SORT_DATE_DESC": "Date descending",
|
||||
"SORT_SIZE_ASC": "Size ascending",
|
||||
|
|
|
|||
|
|
@ -130,6 +130,8 @@
|
|||
"QUOTA_SIZE": "Käytät <strong>%SIZE% (%PROC%%)<\/strong> sinulle varatusta <strong>%LIMIT%<\/strong>",
|
||||
"SORT": "Sort",
|
||||
"NO_SORT": "Mail server does not support sorting",
|
||||
"SORT_ARRIVAL_ASC": "Arrived ascending",
|
||||
"SORT_ARRIVAL_DESC": "Arrived descending",
|
||||
"SORT_DATE_ASC": "Date ascending",
|
||||
"SORT_DATE_DESC": "Date descending",
|
||||
"SORT_SIZE_ASC": "Size ascending",
|
||||
|
|
|
|||
|
|
@ -130,6 +130,8 @@
|
|||
"QUOTA_SIZE": "Vous utilisez <strong>%SIZE% (%PROC%%)<\/strong> sur <strong>%LIMIT%<\/strong>",
|
||||
"SORT": "Trier",
|
||||
"NO_SORT": "Le serveur de messagerie ne prend pas en charge le tri",
|
||||
"SORT_ARRIVAL_ASC": "Arrivée ascendante",
|
||||
"SORT_ARRIVAL_DESC": "Arrivée décroissante",
|
||||
"SORT_DATE_ASC": "Date ascendante",
|
||||
"SORT_DATE_DESC": "Date décroissante",
|
||||
"SORT_SIZE_ASC": "Taille ascendante",
|
||||
|
|
|
|||
|
|
@ -130,6 +130,8 @@
|
|||
"QUOTA_SIZE": "Felhasználva <strong>%SIZE% (%PROC%%)<\/strong> ebből: <strong>%LIMIT%<\/strong>",
|
||||
"SORT": "Sort",
|
||||
"NO_SORT": "Mail server does not support sorting",
|
||||
"SORT_ARRIVAL_ASC": "Arrived ascending",
|
||||
"SORT_ARRIVAL_DESC": "Arrived descending",
|
||||
"SORT_DATE_ASC": "Date ascending",
|
||||
"SORT_DATE_DESC": "Date descending",
|
||||
"SORT_SIZE_ASC": "Size ascending",
|
||||
|
|
|
|||
|
|
@ -130,6 +130,8 @@
|
|||
"QUOTA_SIZE": "Menggunakan <strong>%SIZE% (%PROC%%)<\/strong> dari <strong>%LIMIT%<\/strong>",
|
||||
"SORT": "Sort",
|
||||
"NO_SORT": "Mail server does not support sorting",
|
||||
"SORT_ARRIVAL_ASC": "Arrived ascending",
|
||||
"SORT_ARRIVAL_DESC": "Arrived descending",
|
||||
"SORT_DATE_ASC": "Date ascending",
|
||||
"SORT_DATE_DESC": "Date descending",
|
||||
"SORT_SIZE_ASC": "Size ascending",
|
||||
|
|
|
|||
|
|
@ -130,6 +130,8 @@
|
|||
"QUOTA_SIZE": "Notar <strong>%SIZE% (%PROC%%)<\/strong> af <strong>%LIMIT%<\/strong>",
|
||||
"SORT": "Sort",
|
||||
"NO_SORT": "Mail server does not support sorting",
|
||||
"SORT_ARRIVAL_ASC": "Arrived ascending",
|
||||
"SORT_ARRIVAL_DESC": "Arrived descending",
|
||||
"SORT_DATE_ASC": "Date ascending",
|
||||
"SORT_DATE_DESC": "Date descending",
|
||||
"SORT_SIZE_ASC": "Size ascending",
|
||||
|
|
|
|||
|
|
@ -130,6 +130,8 @@
|
|||
"QUOTA_SIZE": "Stai usando <strong>%SIZE% (%PROC%%)<\/strong> su <strong>%LIMIT%<\/strong>",
|
||||
"SORT": "Riordina",
|
||||
"NO_SORT": "Il mail server non supporta l'ordinamento",
|
||||
"SORT_ARRIVAL_ASC": "Arrived ascending",
|
||||
"SORT_ARRIVAL_DESC": "Arrived descending",
|
||||
"SORT_DATE_ASC": "Data crescente",
|
||||
"SORT_DATE_DESC": "Data decrescente",
|
||||
"SORT_SIZE_ASC": "Dimensione crescente",
|
||||
|
|
|
|||
|
|
@ -130,6 +130,8 @@
|
|||
"QUOTA_SIZE": "<strong>%SIZE% (%PROC%%)<\/strong>\/<strong>%LIMIT%<\/strong>を使用中",
|
||||
"SORT": "Sort",
|
||||
"NO_SORT": "Mail server does not support sorting",
|
||||
"SORT_ARRIVAL_ASC": "Arrived ascending",
|
||||
"SORT_ARRIVAL_DESC": "Arrived descending",
|
||||
"SORT_DATE_ASC": "日付で昇順",
|
||||
"SORT_DATE_DESC": "日付で降順",
|
||||
"SORT_SIZE_ASC": "サイズで昇順",
|
||||
|
|
|
|||
|
|
@ -130,6 +130,8 @@
|
|||
"QUOTA_SIZE": "총 <strong>%LIMIT%<\/strong> 중 <strong>%PROC%%<\/strong>을(를) 사용 중입니다.",
|
||||
"SORT": "Sort",
|
||||
"NO_SORT": "Mail server does not support sorting",
|
||||
"SORT_ARRIVAL_ASC": "Arrived ascending",
|
||||
"SORT_ARRIVAL_DESC": "Arrived descending",
|
||||
"SORT_DATE_ASC": "Date ascending",
|
||||
"SORT_DATE_DESC": "Date descending",
|
||||
"SORT_SIZE_ASC": "Size ascending",
|
||||
|
|
|
|||
|
|
@ -130,6 +130,8 @@
|
|||
"QUOTA_SIZE": "Naudojate <strong>%SIZE% (%PROC%%)<\/strong> iš savo turimų <strong>%LIMIT%<\/strong>",
|
||||
"SORT": "Sort",
|
||||
"NO_SORT": "Mail server does not support sorting",
|
||||
"SORT_ARRIVAL_ASC": "Arrived ascending",
|
||||
"SORT_ARRIVAL_DESC": "Arrived descending",
|
||||
"SORT_DATE_ASC": "Date ascending",
|
||||
"SORT_DATE_DESC": "Date descending",
|
||||
"SORT_SIZE_ASC": "Size ascending",
|
||||
|
|
|
|||
|
|
@ -130,6 +130,8 @@
|
|||
"QUOTA_SIZE": "Izmantots <strong>%SIZE% (%PROC%%)<\/strong> no <strong>%LIMIT%<\/strong>",
|
||||
"SORT": "Sort",
|
||||
"NO_SORT": "Mail server does not support sorting",
|
||||
"SORT_ARRIVAL_ASC": "Arrived ascending",
|
||||
"SORT_ARRIVAL_DESC": "Arrived descending",
|
||||
"SORT_DATE_ASC": "Date ascending",
|
||||
"SORT_DATE_DESC": "Date descending",
|
||||
"SORT_SIZE_ASC": "Size ascending",
|
||||
|
|
|
|||
|
|
@ -130,6 +130,8 @@
|
|||
"QUOTA_SIZE": "Bruker <strong> %SIZE% ( %PROC%% ) <\/strong> av <strong> %LIMIT% <\/strong>",
|
||||
"SORT": "Sort",
|
||||
"NO_SORT": "Mail server does not support sorting",
|
||||
"SORT_ARRIVAL_ASC": "Arrived ascending",
|
||||
"SORT_ARRIVAL_DESC": "Arrived descending",
|
||||
"SORT_DATE_ASC": "Dato, stigende",
|
||||
"SORT_DATE_DESC": "Dato, synkende",
|
||||
"SORT_SIZE_ASC": "Størrelse, stigende",
|
||||
|
|
|
|||
|
|
@ -130,6 +130,8 @@
|
|||
"QUOTA_SIZE": "U gebruikt <strong>%SIZE% (%PROC%%)<\/strong> van <strong>%LIMIT%<\/strong>",
|
||||
"SORT": "Sorteren",
|
||||
"NO_SORT": "Mail server ondersteund sorteren niet",
|
||||
"SORT_ARRIVAL_ASC": "Ontvangen oplopend",
|
||||
"SORT_ARRIVAL_DESC": "Ontvangen aflopend",
|
||||
"SORT_DATE_ASC": "Datum oplopend",
|
||||
"SORT_DATE_DESC": "Datum aflopend",
|
||||
"SORT_SIZE_ASC": "Grootte oplopend",
|
||||
|
|
|
|||
|
|
@ -136,11 +136,11 @@
|
|||
"SECURE_OPTION_NONE": "Bez zabezpieczeń",
|
||||
"LABEL_ALLOW_SIEVE_SCRIPTS": "Zezwól na skrypty sieve",
|
||||
"LABEL_USE_SHORT_LOGIN": "Użyj krótkiego loginu",
|
||||
"LABEL_LOWERCASE_LOGIN": "Lowercase login (case-insensitive)",
|
||||
"LABEL_LOWERCASE_LOGIN": "Logowanie małymi literami (bez uwzględniania ich wielkości)",
|
||||
"LABEL_USE_AUTH": "Użyj uwierzytelnienia",
|
||||
"LABEL_SET_SENDER": "Użyj loginu jako nadawcy",
|
||||
"LABEL_AUTH_PLAIN_LINE": "Wymuś logowanie pojedynczą komendą AUTH PLAIN",
|
||||
"LABEL_USE_PHP_MAIL": "Użyj funkcji php 'mail()'",
|
||||
"LABEL_USE_PHP_MAIL": "Użyj funkcji PHP 'mail()'",
|
||||
"BUTTON_WHITE_LIST": "Biała lista",
|
||||
"NEW_DOMAIN_DESC": "Konfiguracja tej domeny pozwala na pracę z adresami e-mail: <strong>%NAME%<\/strong>",
|
||||
"WHITE_LIST_ALERT": "Lista użytkowników domeny, którzy mogą uzyskać dostęp z tego webmaila.\nUżyj spacji do rozdzielenia.\n",
|
||||
|
|
|
|||
|
|
@ -89,18 +89,18 @@
|
|||
"ARCHIVE_NAME": "Archiwum"
|
||||
},
|
||||
"IMAP": {
|
||||
"ACL_RIGHTS": "Rights",
|
||||
"ACL_A": "Administer",
|
||||
"ACL_E": "Expunge",
|
||||
"ACL_I": "Insert messages",
|
||||
"ACL_K": "Create sub-folders",
|
||||
"ACL_L": "Show messages",
|
||||
"ACL_P": "Send mail",
|
||||
"ACL_R": "Select folder",
|
||||
"ACL_S": "\\SEEN tag",
|
||||
"ACL_T": "\\DELETED tag",
|
||||
"ACL_W": "Other tags",
|
||||
"ACL_X": "Delete\/Rename folder"
|
||||
"ACL_RIGHTS": "Uprawnienia",
|
||||
"ACL_A": "Administracja",
|
||||
"ACL_E": "Usuń",
|
||||
"ACL_I": "Wstaw wiadomości",
|
||||
"ACL_K": "Utwórz podfoldery",
|
||||
"ACL_L": "Pokaż wiadomości",
|
||||
"ACL_P": "Wyślij mail",
|
||||
"ACL_R": "Wybierz folder",
|
||||
"ACL_S": "\\Tag przeczytane",
|
||||
"ACL_T": "\\Tag usunięte",
|
||||
"ACL_W": "Inne tagi",
|
||||
"ACL_X": "Usuń\/Zmień nazwę folder(-u)"
|
||||
},
|
||||
"MESSAGE_LIST": {
|
||||
"BUTTON_RELOAD": "Odśwież listę wiadomości",
|
||||
|
|
@ -130,6 +130,8 @@
|
|||
"QUOTA_SIZE": "Wykorzystano <strong>%SIZE% (%PROC%%)<\/strong> z <strong>%LIMIT%<\/strong>",
|
||||
"SORT": "Sortowanie",
|
||||
"NO_SORT": "Serwer pocztowy nie wspiera sortowania",
|
||||
"SORT_ARRIVAL_ASC": "Odebrano rosnąco",
|
||||
"SORT_ARRIVAL_DESC": "Odebrano malejąco",
|
||||
"SORT_DATE_ASC": "Data rosnąco",
|
||||
"SORT_DATE_DESC": "Data malejąco",
|
||||
"SORT_SIZE_ASC": "Wielkość rosnąco",
|
||||
|
|
@ -219,9 +221,9 @@
|
|||
"DISPLAY_NAME": "Nazwa wyświetlana",
|
||||
"LAST_NAME": "Nazwisko",
|
||||
"FIRST_NAME": "Imię",
|
||||
"MIDDLE_NAME": "Middle name",
|
||||
"NAME_PREFIX": "Name prefix",
|
||||
"NAME_SUFFIX": "Name suffix",
|
||||
"MIDDLE_NAME": "Drugie imię",
|
||||
"NAME_PREFIX": "Przedrostek imienia",
|
||||
"NAME_SUFFIX": "Przyrostek imienia",
|
||||
"NICK_NAME": "Pseudonim",
|
||||
"LABEL_READ_ONLY": "Tylko do odczytu",
|
||||
"ADD_MENU_LABEL": "Dodaj",
|
||||
|
|
@ -537,7 +539,7 @@
|
|||
"TO_MANY_FOLDERS_DESC_2": "Pokazaliśmy tylko część z nich aby uniknąć problemów z wydajnością.",
|
||||
"HELP_DELETE_FOLDER": "Usuń folder",
|
||||
"HELP_SHOW_HIDE_FOLDER": "Pokaż\/ukryj folder",
|
||||
"HELP_CHECK_FOR_NEW_MESSAGES": "Sprawdzaj nowych wiadomości",
|
||||
"HELP_CHECK_FOR_NEW_MESSAGES": "Sprawdź czy są nowe wiadomości",
|
||||
"HIDE_UNSUBSCRIBED": "Ukryj niesubskrybowane foldery",
|
||||
"UNHIDE_KOLAB_FOLDERS": "Pokaż foldery Kolab",
|
||||
"TYPE_CALENDAR": "Kalendarz",
|
||||
|
|
|
|||
|
|
@ -130,6 +130,8 @@
|
|||
"QUOTA_SIZE": "Utilizando <strong>%SIZE% (%PROC%%)<\/strong> de <strong>%LIMIT%<\/strong>",
|
||||
"SORT": "Sort",
|
||||
"NO_SORT": "Mail server does not support sorting",
|
||||
"SORT_ARRIVAL_ASC": "Arrived ascending",
|
||||
"SORT_ARRIVAL_DESC": "Arrived descending",
|
||||
"SORT_DATE_ASC": "Date ascending",
|
||||
"SORT_DATE_DESC": "Date descending",
|
||||
"SORT_SIZE_ASC": "Size ascending",
|
||||
|
|
@ -219,9 +221,9 @@
|
|||
"DISPLAY_NAME": "Nome de exibição",
|
||||
"LAST_NAME": "Sobrenome",
|
||||
"FIRST_NAME": "Primeiro nome",
|
||||
"MIDDLE_NAME": "Middle name",
|
||||
"NAME_PREFIX": "Name prefix",
|
||||
"NAME_SUFFIX": "Name suffix",
|
||||
"MIDDLE_NAME": "Nomes adicionais",
|
||||
"NAME_PREFIX": "Prefixo do nome",
|
||||
"NAME_SUFFIX": "Sufixo do nome",
|
||||
"NICK_NAME": "Apelido",
|
||||
"LABEL_READ_ONLY": "Somente Leitura",
|
||||
"ADD_MENU_LABEL": "Adicionar",
|
||||
|
|
|
|||
|
|
@ -130,6 +130,8 @@
|
|||
"QUOTA_SIZE": "Está a usar <strong>%SIZE% (%PROC%%)<\/strong> dos <strong>%LIMIT%<\/strong>",
|
||||
"SORT": "Ordenar",
|
||||
"NO_SORT": "O servidor de correio não suporta ordenação",
|
||||
"SORT_ARRIVAL_ASC": "Arrived ascending",
|
||||
"SORT_ARRIVAL_DESC": "Arrived descending",
|
||||
"SORT_DATE_ASC": "Data ascendente",
|
||||
"SORT_DATE_DESC": "Data descendente",
|
||||
"SORT_SIZE_ASC": "Tamanho ascendente",
|
||||
|
|
|
|||
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