Merge branch 'master' into dev/sync-nextcloud-addressbook

This commit is contained in:
Fahim Salam Chowdhury 2024-10-16 08:51:43 +00:00 committed by GitHub
commit 721a849254
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
122 changed files with 1816 additions and 1677 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -45,4 +45,8 @@
content: ')';
}
.identities-list tr + tr .identity-default {
display: none;
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -4,7 +4,7 @@ RUN mkdir -p /app/code
WORKDIR /app/code
# If you change the extraction below, be sure to test on scaleway
VERSION=2.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 && \

View file

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

View file

@ -20,7 +20,7 @@ return "SnappyMail Webmail is a browser-based multilingual IMAP client with an a
# script_snappymail_versions()
sub script_snappymail_versions
{
return ( "2.38.0" );
return ( "2.38.2" );
}
sub script_snappymail_version_desc

View file

@ -3,7 +3,7 @@
"title": "SnappyMail",
"description": "Simple, modern & fast web-based email client",
"private": true,
"version": "2.38.0",
"version": "2.38.2",
"homepage": "https://snappymail.eu",
"author": {
"name": "DJ Maze",

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,20 @@
The MIT License (MIT)
Copyright (c) 2016 RainLoop Team
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

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

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

View file

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

View file

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

View file

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

View file

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

View 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 = "Код підтвердження"

View file

@ -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 = "代码"

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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 = [];

View file

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

View file

@ -104,7 +104,7 @@ class VTodo extends VObject\Component
'LAST-MODIFIED' => '?',
'LOCATION' => '?',
'ORGANIZER' => '?',
'PERCENT' => '?',
'PERCENT-COMPLETE' => '?',
'PRIORITY' => '?',
'RECURRENCE-ID' => '?',
'SEQUENCE' => '?',

View file

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

View file

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

View file

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

View file

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

View file

@ -131,7 +131,7 @@ class Json extends Parser
list(
$propertyName,
$parameters,
$valueType
$valueType,
) = $jProp;
$propertyName = strtoupper($propertyName);

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -110,7 +110,7 @@ trait ContextStackTrait
$this->elementMap,
$this->contextUri,
$this->namespaceMap,
$this->classMap
$this->classMap,
) = array_pop($this->contextStack);
}
}

View file

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

View file

@ -16,5 +16,5 @@ class Version
/**
* Full version number.
*/
public const VERSION = '4.0.5';
public const VERSION = '4.0.6';
}

View file

@ -236,7 +236,7 @@ class Writer extends \XMLWriter
list(
$namespace,
$localName
$localName,
) = Service::parseClarkNotation($name);
if (array_key_exists($namespace, $this->namespaceMap)) {

View file

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

View file

@ -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": "Памер па ўзрастанні",

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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": "サイズで昇順",

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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