Merge branch 'master' into patch-1

This commit is contained in:
Maarten 2026-10-21 15:44:30 +02:00 • committed by GitHub
commit 86dc155d1c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
125 changed files with 2005 additions and 851 deletions

View file

@ -232,6 +232,7 @@ export const
detectHiddenImages = true, // !!SettingsGet('try_to_detect_hidden_images'), detectHiddenImages = true, // !!SettingsGet('try_to_detect_hidden_images'),
bqLevel = parseInt(SettingsUserStore.maxBlockquotesLevel()), bqLevel = parseInt(SettingsUserStore.maxBlockquotesLevel()),
minFontSize = parseInt(SettingsUserStore.minFontSize()),
result = { result = {
hasExternals: false, hasExternals: false,
@ -625,12 +626,13 @@ export const
if (urls_broken.length) { if (urls_broken.length) {
setAttribute('data-x-style-broken-urls', JSON.stringify(urls_broken)); setAttribute('data-x-style-broken-urls', JSON.stringify(urls_broken));
} }
/*
// https://github.com/the-djmaze/snappymail/issues/1082 // https://github.com/the-djmaze/snappymail/issues/1082
if (11 > pInt(oStyle.fontSize)) { if (minFontSize > pInt(oStyle.fontSize)) {
oStyle.removeProperty('font-size'); oStyle.fontSize = minFontSize + "px";
// oStyle.removeProperty('font-size');
} }
*/
// Removes background and color // Removes background and color
// Many e-mails incorrectly only define one, not both // Many e-mails incorrectly only define one, not both
// And in dark theme mode this kills the readability // And in dark theme mode this kills the readability

View file

@ -100,7 +100,7 @@ export class UserSettingsGeneral extends AbstractViewSettings {
['defaultSort', 'useThreads', 'threadAlgorithm', ['defaultSort', 'useThreads', 'threadAlgorithm',
// These use addSetting() // These use addSetting()
'layout', 'messageReadDelay', 'messagesPerPage', 'checkMailInterval', 'layout', 'messageReadDelay', 'messagesPerPage', 'checkMailInterval',
'editorDefaultType', 'editorWysiwyg', 'msgDefaultAction', 'maxBlockquotesLevel', 'editorDefaultType', 'editorWysiwyg', 'msgDefaultAction', 'maxBlockquotesLevel', 'minFontSize',
// These are in addSettings() // These are in addSettings()
'requestReadReceipt', 'requestDsn', 'requireTLS', 'pgpSign', 'pgpEncrypt', 'requestReadReceipt', 'requestDsn', 'requireTLS', 'pgpSign', 'pgpEncrypt',
'viewHTML', 'viewImages', 'viewImagesWhitelist', 'removeColors', 'allowStyles', 'allowDraftAutosave', 'viewHTML', 'viewImages', 'viewImagesWhitelist', 'removeColors', 'allowStyles', 'allowDraftAutosave',
@ -161,6 +161,7 @@ export class UserSettingsGeneral extends AbstractViewSettings {
this.addSetting('CheckMailInterval'); this.addSetting('CheckMailInterval');
this.addSetting('Layout'); this.addSetting('Layout');
this.addSetting('MaxBlockquotesLevel'); this.addSetting('MaxBlockquotesLevel');
this.addSetting('minFontSize');
this.addSetting('defaultSort'); this.addSetting('defaultSort');
this.addSettings([ this.addSettings([

View file

@ -58,10 +58,9 @@ const
*/ */
publicKeysItem = 'openpgp-public-keys', publicKeysItem = 'openpgp-public-keys',
privateKeysItem = 'openpgp-private-keys', privateKeysItem = 'openpgp-private-keys',
storage = window.localStorage,
loadOpenPgpKeys = async itemname => { loadOpenPgpKeys = async itemname => {
let keys = [], key, let keys = [], key,
armoredKeys = JSON.parse(storage.getItem(itemname)), armoredKeys = JSON.parse(localStorage.getItem(itemname)),
i = arrayLength(armoredKeys); i = arrayLength(armoredKeys);
while (i--) try { while (i--) try {
key = await openpgp.readKey({armoredKey:armoredKeys[i]}); key = await openpgp.readKey({armoredKey:armoredKeys[i]});
@ -74,9 +73,9 @@ const
storeOpenPgpKeys = (keys, section) => { storeOpenPgpKeys = (keys, section) => {
let armoredKeys = keys.map(item => item.armor); let armoredKeys = keys.map(item => item.armor);
if (armoredKeys.length) { if (armoredKeys.length) {
storage.setItem(section, JSON.stringify(armoredKeys)); localStorage.setItem(section, JSON.stringify(armoredKeys));
} else { } else {
storage.removeItem(section); localStorage.removeItem(section);
} }
}; };

View file

@ -23,6 +23,7 @@ export const SettingsUserStore = new class {
allowStyles: 0, allowStyles: 0,
collapseBlockquotes: 1, collapseBlockquotes: 1,
maxBlockquotesLevel: 0, maxBlockquotesLevel: 0,
minFontSize: 0,
listInlineAttachments: 0, listInlineAttachments: 0,
simpleAttachmentsList: 0, simpleAttachmentsList: 0,
useCheckboxesInList: 1, useCheckboxesInList: 1,
@ -98,6 +99,7 @@ export const SettingsUserStore = new class {
'AllowStyles', 'AllowStyles',
'CollapseBlockquotes', 'CollapseBlockquotes',
'MaxBlockquotesLevel', 'MaxBlockquotesLevel',
'minFontSize',
'ListInlineAttachments', 'ListInlineAttachments',
'simpleAttachmentsList', 'simpleAttachmentsList',
'UseCheckboxesInList', 'UseCheckboxesInList',

View file

@ -1,6 +1,6 @@
#V-PopupsLanguages { #V-PopupsLanguages {
max-width: 710px; max-width: 720px;
label { label {
display: inline-block; display: inline-block;

View file

@ -591,6 +591,7 @@ class Actions
'ListInlineAttachments' => false, 'ListInlineAttachments' => false,
'CollapseBlockquotes' => $oConfig->Get('defaults', 'collapse_blockquotes', true), 'CollapseBlockquotes' => $oConfig->Get('defaults', 'collapse_blockquotes', true),
'MaxBlockquotesLevel' => 0, 'MaxBlockquotesLevel' => 0,
'minFontSize' => 0,
'simpleAttachmentsList' => false, 'simpleAttachmentsList' => false,
'listGrouped' => $oConfig->Get('defaults', 'mail_list_grouped', false), 'listGrouped' => $oConfig->Get('defaults', 'mail_list_grouped', false),
'MessagesPerPage' => \max(10, \intval($oConfig->Get('webmail', 'messages_per_page', 25)) ?: 25), 'MessagesPerPage' => \max(10, \intval($oConfig->Get('webmail', 'messages_per_page', 25)) ?: 25),
@ -687,6 +688,7 @@ class Actions
$aResult['ListInlineAttachments'] = (bool)$oSettings->GetConf('ListInlineAttachments', $aResult['ListInlineAttachments']); $aResult['ListInlineAttachments'] = (bool)$oSettings->GetConf('ListInlineAttachments', $aResult['ListInlineAttachments']);
$aResult['CollapseBlockquotes'] = (bool)$oSettings->GetConf('CollapseBlockquotes', $aResult['CollapseBlockquotes']); $aResult['CollapseBlockquotes'] = (bool)$oSettings->GetConf('CollapseBlockquotes', $aResult['CollapseBlockquotes']);
$aResult['MaxBlockquotesLevel'] = (int)$oSettings->GetConf('MaxBlockquotesLevel', $aResult['MaxBlockquotesLevel']); $aResult['MaxBlockquotesLevel'] = (int)$oSettings->GetConf('MaxBlockquotesLevel', $aResult['MaxBlockquotesLevel']);
$aResult['minFontSize'] = (int)$oSettings->GetConf('minFontSize', $aResult['minFontSize']);
$aResult['simpleAttachmentsList'] = (bool)$oSettings->GetConf('simpleAttachmentsList', $aResult['simpleAttachmentsList']); $aResult['simpleAttachmentsList'] = (bool)$oSettings->GetConf('simpleAttachmentsList', $aResult['simpleAttachmentsList']);
$aResult['listGrouped'] = (bool)$oSettings->GetConf('listGrouped', $aResult['listGrouped']); $aResult['listGrouped'] = (bool)$oSettings->GetConf('listGrouped', $aResult['listGrouped']);
$aResult['ContactsAutosave'] = (bool)$oSettings->GetConf('ContactsAutosave', $aResult['ContactsAutosave']); $aResult['ContactsAutosave'] = (bool)$oSettings->GetConf('ContactsAutosave', $aResult['ContactsAutosave']);

View file

@ -183,6 +183,7 @@ trait User
$this->setSettingsFromParams($oSettings, 'ListInlineAttachments', 'bool'); $this->setSettingsFromParams($oSettings, 'ListInlineAttachments', 'bool');
$this->setSettingsFromParams($oSettings, 'CollapseBlockquotes', 'bool'); $this->setSettingsFromParams($oSettings, 'CollapseBlockquotes', 'bool');
$this->setSettingsFromParams($oSettings, 'MaxBlockquotesLevel', 'int'); $this->setSettingsFromParams($oSettings, 'MaxBlockquotesLevel', 'int');
$this->setSettingsFromParams($oSettings, 'minFontSize', 'int');
$this->setSettingsFromParams($oSettings, 'simpleAttachmentsList', 'bool'); $this->setSettingsFromParams($oSettings, 'simpleAttachmentsList', 'bool');
$this->setSettingsFromParams($oSettings, 'listGrouped', 'bool'); $this->setSettingsFromParams($oSettings, 'listGrouped', 'bool');
$this->setSettingsFromParams($oSettings, 'ContactsAutosave', 'bool'); $this->setSettingsFromParams($oSettings, 'ContactsAutosave', 'bool');

View file

@ -95,7 +95,7 @@ class BirthdayCalendarGenerator
// We've seen clients (ez-vcard) putting "BDAY:" properties // We've seen clients (ez-vcard) putting "BDAY:" properties
// without a value into vCards. If we come across those, we'll // without a value into vCards. If we come across those, we'll
// skip them. // skip them.
if (empty($object->BDAY->getValue())) { if ('' === $object->BDAY->getValue()) {
continue; continue;
} }
@ -116,7 +116,7 @@ class BirthdayCalendarGenerator
// Skip if we can't parse the BDAY value. // Skip if we can't parse the BDAY value.
try { try {
$dateParts = DateTimeParser::parseVCardDateTime($object->BDAY->getValue()); $dateParts = DateTimeParser::parseVCardDateTime($object->BDAY->getValue());
} catch (InvalidDataException $e) { } catch (InvalidDataException) {
continue; continue;
} }

View file

@ -92,7 +92,7 @@ class Cli
// @codeCoverageIgnoreEnd // @codeCoverageIgnoreEnd
try { try {
list($options, $positional) = $this->parseArguments($argv); [$options, $positional] = $this->parseArguments($argv);
if (isset($options['q'])) { if (isset($options['q'])) {
$this->quiet = true; $this->quiet = true;
@ -110,27 +110,10 @@ class Cli
return 0; return 0;
case 'format': case 'format':
switch ($value) { $this->format = match ($value) {
// jcard/jcal documents 'jcard', 'jcal', 'vcard21', 'vcard30', 'vcard40', 'icalendar20', 'json', 'mimedir', 'icalendar', 'vcard' => $value,
case 'jcard': default => throw new \InvalidArgumentException('Unknown format: '.$value),
case 'jcal': };
// specific document versions
case 'vcard21':
case 'vcard30':
case 'vcard40':
case 'icalendar20':
// specific formats
case 'json':
case 'mimedir':
// icalendar/vcard
case 'icalendar':
case 'vcard':
$this->format = $value;
break;
default:
throw new \InvalidArgumentException('Unknown format: '.$value);
}
break; break;
case 'pretty': case 'pretty':
$this->pretty = true; $this->pretty = true;
@ -139,28 +122,11 @@ class Cli
$this->forgiving = true; $this->forgiving = true;
break; break;
case 'inputformat': case 'inputformat':
switch ($value) { $this->inputFormat = match ($value) {
// json formats 'jcard', 'jcal', 'json' => 'json',
case 'jcard': 'mimedir', 'icalendar', 'vcard', 'vcard21', 'vcard30', 'vcard40', 'icalendar20' => 'mimedir',
case 'jcal': default => throw new \InvalidArgumentException('Unknown format: '.$value),
case 'json': };
$this->inputFormat = 'json';
break;
// mimedir formats
case 'mimedir':
case 'icalendar':
case 'vcard':
case 'vcard21':
case 'vcard30':
case 'vcard40':
case 'icalendar20':
$this->inputFormat = 'mimedir';
break;
default:
throw new \InvalidArgumentException('Unknown format: '.$value);
}
break; break;
default: default:
throw new \InvalidArgumentException('Unknown option: '.$name); throw new \InvalidArgumentException('Unknown option: '.$name);
@ -181,7 +147,7 @@ class Cli
throw new \InvalidArgumentException('Too many arguments'); throw new \InvalidArgumentException('Too many arguments');
} }
if (!in_array($positional[0], ['validate', 'repair', 'convert', 'color'])) { if (!in_array($positional[0], ['validate', 'repair', 'convert', 'color'], true)) {
throw new \InvalidArgumentException('Unknown command: '.$positional[0]); throw new \InvalidArgumentException('Unknown command: '.$positional[0]);
} }
} catch (\InvalidArgumentException $e) { } catch (\InvalidArgumentException $e) {
@ -201,14 +167,14 @@ class Cli
} }
if (null === $this->inputFormat) { if (null === $this->inputFormat) {
if ('.json' === substr($this->inputPath, -5)) { if (str_ends_with($this->inputPath, '.json')) {
$this->inputFormat = 'json'; $this->inputFormat = 'json';
} else { } else {
$this->inputFormat = 'mimedir'; $this->inputFormat = 'mimedir';
} }
} }
if (null === $this->format) { if (null === $this->format) {
if ('.json' === substr($this->outputPath, -5)) { if (str_ends_with($this->outputPath, '.json')) {
$this->format = 'json'; $this->format = 'json';
} else { } else {
$this->format = 'mimedir'; $this->format = 'mimedir';
@ -219,12 +185,13 @@ class Cli
try { try {
while ($input = $this->readInput()) { while ($input = $this->readInput()) {
// @phpstan-ignore method.dynamicName
$returnCode = $this->$command($input); $returnCode = $this->$command($input);
if (0 !== $returnCode) { if (0 !== $returnCode) {
$realCode = $returnCode; $realCode = $returnCode;
} }
} }
} catch (EofException $e) { } catch (EofException) {
// end of file // end of file
} catch (\Exception $e) { } catch (\Exception $e) {
$this->log('Error: '.$e->getMessage(), 'red'); $this->log('Error: '.$e->getMessage(), 'red');
@ -408,7 +375,7 @@ HELP
} }
fwrite($this->stdout, json_encode($vObj->jsonSerialize(), $jsonOptions)); fwrite($this->stdout, json_encode($vObj->jsonSerialize(), $jsonOptions));
} else { } else {
fwrite($this->stdout, $vObj->serialize()); fwrite($this->stdout, (string) $vObj->serialize());
} }
return 0; return 0;
@ -538,7 +505,7 @@ HELP
$this->cWrite('red', ':'); $this->cWrite('red', ':');
if ($property instanceof Property\Binary) { if ($property instanceof Property\Binary) {
$this->cWrite('default', 'embedded binary stripped. ('.strlen($property->getValue()).' bytes)'); $this->cWrite('default', 'embedded binary stripped. ('.strlen((string) $property->getValue()).' bytes)');
} else { } else {
$parts = $property->getParts(); $parts = $property->getParts();
$first1 = true; $first1 = true;
@ -593,17 +560,17 @@ HELP
$v = $argv[$ii]; $v = $argv[$ii];
if ('--' === substr($v, 0, 2)) { if (str_starts_with((string) $v, '--')) {
// This is a long-form option. // This is a long-form option.
$optionName = substr($v, 2); $optionName = substr((string) $v, 2);
$optionValue = true; $optionValue = true;
if (strpos($optionName, '=')) { if (strpos($optionName, '=')) {
list($optionName, $optionValue) = explode('=', $optionName); [$optionName, $optionValue] = explode('=', $optionName);
} }
$options[$optionName] = $optionValue; $options[$optionName] = $optionValue;
} elseif ('-' === substr($v, 0, 1) && strlen($v) > 1) { } elseif (str_starts_with((string) $v, '-') && strlen((string) $v) > 1) {
// This is a short-form option. // This is a short-form option.
foreach (str_split(substr($v, 1)) as $option) { foreach (str_split(substr((string) $v, 1)) as $option) {
$options[$option] = true; $options[$option] = true;
} }
} else { } else {
@ -628,6 +595,9 @@ HELP
if (!$this->parser) { if (!$this->parser) {
if ('-' !== $this->inputPath) { if ('-' !== $this->inputPath) {
$this->stdin = fopen($this->inputPath, 'r'); $this->stdin = fopen($this->inputPath, 'r');
if (false === $this->stdin) {
throw new InvalidDataException('Cannot open "'.$this->inputPath);
}
} }
if ('mimedir' === $this->inputFormat) { if ('mimedir' === $this->inputFormat) {

View file

@ -140,7 +140,7 @@ class Component extends Node
// If there's no dot in the name, it's an exact property name, // If there's no dot in the name, it's an exact property name,
// we can just wipe out all those properties. // we can just wipe out all those properties.
// //
if (false === strpos($item, '.')) { if (!str_contains($item, '.')) {
unset($this->children[strtoupper($item)]); unset($this->children[strtoupper($item)]);
return; return;
@ -210,8 +210,8 @@ class Component extends Node
{ {
$group = null; $group = null;
$name = strtoupper($name); $name = strtoupper($name);
if (false !== strpos($name, '.')) { if (str_contains($name, '.')) {
list($group, $name) = explode('.', $name, 2); [$group, $name] = explode('.', $name, 2);
} }
if ('' === $name) { if ('' === $name) {
$name = null; $name = null;
@ -222,16 +222,14 @@ class Component extends Node
if (is_null($group)) { if (is_null($group)) {
return $result; return $result;
} else {
// If we have a group filter as well, we need to narrow it down
// more.
return array_filter(
$result,
function ($child) use ($group) {
return $child instanceof Property && (null !== $child->group ? strtoupper($child->group) : '') === $group;
}
);
} }
// If we have a group filter as well, we need to narrow it down
// more.
return array_filter(
$result,
fn ($child) => $child instanceof Property && (null !== $child->group ? strtoupper($child->group) : '') === $group
);
} }
// If we got to this point, it means there was no 'name' specified for // If we got to this point, it means there was no 'name' specified for
@ -368,7 +366,7 @@ class Component extends Node
$writer->startElement(strtolower($this->name)); $writer->startElement(strtolower($this->name));
if (!empty($properties)) { if ([] !== $properties) {
$writer->startElement('properties'); $writer->startElement('properties');
foreach ($properties as $property) { foreach ($properties as $property) {
@ -378,7 +376,7 @@ class Component extends Node
$writer->endElement(); $writer->endElement();
} }
if (!empty($components)) { if ([] !== $components) {
$writer->startElement('components'); $writer->startElement('components');
foreach ($components as $component) { foreach ($components as $component) {
@ -422,13 +420,12 @@ class Component extends Node
$matches = $this->select($name); $matches = $this->select($name);
if (0 === count($matches)) { if (0 === count($matches)) {
return null; return null;
} else {
$firstMatch = current($matches);
/* @var $firstMatch Property */
$firstMatch->setIterator(new ElementList(array_values($matches)));
return $firstMatch;
} }
$firstMatch = current($matches);
/* @var $firstMatch Property */
$firstMatch->setIterator(new ElementList(array_values($matches)));
return $firstMatch;
} }
/** /**
@ -542,7 +539,7 @@ class Component extends Node
$messages = []; $messages = [];
foreach ($this->children() as $child) { foreach ($this->children() as $child) {
$name = strtoupper($child->name); $name = strtoupper((string) $child->name);
if (!isset($propertyCounters[$name])) { if (!isset($propertyCounters[$name])) {
$propertyCounters[$name] = 1; $propertyCounters[$name] = 1;
} else { } else {

View file

@ -32,9 +32,9 @@ class VAlarm extends VObject\Component
public function getEffectiveTriggerTime(): \DateTimeImmutable public function getEffectiveTriggerTime(): \DateTimeImmutable
{ {
$trigger = $this->TRIGGER; $trigger = $this->TRIGGER;
if (!isset($trigger['VALUE']) || ($trigger['VALUE'] && 'DURATION' === strtoupper($trigger['VALUE']))) { if (!isset($trigger['VALUE']) || ($trigger['VALUE'] && 'DURATION' === strtoupper((string) $trigger['VALUE']))) {
$triggerDuration = VObject\DateTimeParser::parseDuration($this->TRIGGER); $triggerDuration = VObject\DateTimeParser::parseDuration($this->TRIGGER);
$related = (isset($trigger['RELATED']) && 'END' == strtoupper($trigger['RELATED'])) ? 'END' : 'START'; $related = (isset($trigger['RELATED']) && 'END' === strtoupper($trigger['RELATED'])) ? 'END' : 'START';
/** @var VEvent|VTodo $parentComponent */ /** @var VEvent|VTodo $parentComponent */
$parentComponent = $this->parent; $parentComponent = $this->parent;
@ -96,9 +96,9 @@ class VAlarm extends VObject\Component
} }
return false; return false;
} else {
return $start <= $effectiveTrigger && $end > $effectiveTrigger;
} }
return $start <= $effectiveTrigger && $end > $effectiveTrigger;
} }
/** /**

View file

@ -33,7 +33,7 @@ class VAvailability extends VObject\Component
*/ */
public function isInTimeRange(\DateTimeInterface $start, \DateTimeInterface $end): bool public function isInTimeRange(\DateTimeInterface $start, \DateTimeInterface $end): bool
{ {
list($effectiveStart, $effectiveEnd) = $this->getEffectiveStartEnd(); [$effectiveStart, $effectiveEnd] = $this->getEffectiveStartEnd();
return return
(is_null($effectiveStart) || $start < $effectiveEnd) (is_null($effectiveStart) || $start < $effectiveEnd)

View file

@ -145,6 +145,10 @@ class VCalendar extends VObject\Document
// Additions from draft-daboo-calendar-availability-05 // Additions from draft-daboo-calendar-availability-05
'BUSYTYPE' => Property\Text::class, 'BUSYTYPE' => Property\Text::class,
// RFC 7986 properties
'NAME' => Property\FlatText::class,
'COLOR' => Property\FlatText::class,
]; ];
/** /**
@ -164,7 +168,7 @@ class VCalendar extends VObject\Document
* *
* @param string|null $componentName filter by component name * @param string|null $componentName filter by component name
* *
* @return VObject\Component[] * @return Component[]
*/ */
public function getBaseComponents(?string $componentName = null): array public function getBaseComponents(?string $componentName = null): array
{ {
@ -327,7 +331,7 @@ class VCalendar extends VObject\Document
foreach ($recurringEvents as $events) { foreach ($recurringEvents as $events) {
try { try {
$it = new EventIterator($events, null, $timeZone); $it = new EventIterator($events, null, $timeZone);
} catch (NoInstancesException $e) { } catch (NoInstancesException) {
// This event is recurring, but it doesn't have a single // This event is recurring, but it doesn't have a single
// instance. We are skipping this event from the output // instance. We are skipping this event from the output
// entirely. // entirely.
@ -379,6 +383,18 @@ class VCalendar extends VObject\Document
'CALSCALE' => '?', 'CALSCALE' => '?',
'METHOD' => '?', 'METHOD' => '?',
// RFC 7986 calendar properties
'UID' => '?',
'LAST-MODIFIED' => '?',
'URL' => '?',
'REFRESH-INTERVAL' => '?',
'SOURCE' => '?',
'COLOR' => '?',
'NAME' => '*',
'DESCRIPTION' => '*',
'CATEGORIES' => '*',
'IMAGE' => '*',
]; ];
} }
@ -424,7 +440,7 @@ class VCalendar extends VObject\Document
if ($child instanceof Component) { if ($child instanceof Component) {
++$componentsFound; ++$componentsFound;
if (!in_array($child->name, ['VEVENT', 'VTODO', 'VJOURNAL'])) { if (!in_array($child->name, ['VEVENT', 'VTODO', 'VJOURNAL'], true)) {
continue; continue;
} }
$componentTypes[] = $child->name; $componentTypes[] = $child->name;

View file

@ -132,6 +132,13 @@ class VCard extends VObject\Document
'ORG-DIRECTORY' => VObject\Property\FlatText::class, 'ORG-DIRECTORY' => VObject\Property\FlatText::class,
'X-CRYPTO' => VObject\Property\XCrypto::class, 'X-CRYPTO' => VObject\Property\XCrypto::class,
// rfc9554 properties
'CREATED' => VObject\Property\VCard\TimeStamp::class,
'GRAMGENDER' => VObject\Property\FlatText::class,
'LANGUAGE' => VObject\Property\VCard\LanguageTag::class,
'PRONOUNS' => VObject\Property\FlatText::class,
'SOCIALPROFILE' => VObject\Property\Uri::class,
]; ];
/** /**
@ -326,6 +333,11 @@ class VCard extends VObject\Document
'CALURI' => '*', 'CALURI' => '*',
'CATEGORIES' => '*', 'CATEGORIES' => '*',
'CLIENTPIDMAP' => '*', 'CLIENTPIDMAP' => '*',
'CREATED' => '?',
'GRAMGENDER' => '*',
'LANGUAGE' => '?',
'PRONOUNS' => '*',
'SOCIALPROFILE' => '*',
'EMAIL' => '*', 'EMAIL' => '*',
'FBURL' => '*', 'FBURL' => '*',
'IMPP' => '*', 'IMPP' => '*',
@ -411,6 +423,34 @@ class VCard extends VObject\Document
return null; return null;
} }
/**
* Returns a property with a specific TYPE value (ADR, TEL, or EMAIL).
*
* This function will return null if the exact property list does not exist.
*
* For example to get the property of `TEL;TYPE=HOME,CELL`
* you would call `getByTypes('TEL', ['HOME', 'CELL'])`
*
* @param string[] $types
*
* @return \ArrayAccess|array|null
*/
public function getByTypes(string $propertyName, array $types)
{
$types = array_map(strtolower(...), $types);
foreach ($this->select($propertyName) as $field) {
if (isset($field['TYPE'])) {
$parts = array_map(strtolower(...), $field['TYPE']->getParts());
if (!array_diff($types, $parts) && !array_diff($parts, $types)) {
return $field;
}
}
}
return null;
}
/** /**
* This method should return a list of default property values. * This method should return a list of default property values.
*/ */
@ -453,11 +493,9 @@ class VCard extends VObject\Document
$propertiesByGroup = []; $propertiesByGroup = [];
foreach ($this->children() as $property) { foreach ($this->children() as $property) {
$group = $property->group; $group = $property->group ?? '';
if (!isset($propertiesByGroup[$group])) { $propertiesByGroup[$group] ??= [];
$propertiesByGroup[$group] = [];
}
$propertiesByGroup[$group][] = $property; $propertiesByGroup[$group][] = $property;
} }
@ -465,9 +503,9 @@ class VCard extends VObject\Document
$writer->startElement(strtolower($this->name)); $writer->startElement(strtolower($this->name));
foreach ($propertiesByGroup as $group => $properties) { foreach ($propertiesByGroup as $group => $properties) {
if (!empty($group)) { if ('' !== $group) {
$writer->startElement('group'); $writer->startElement('group');
$writer->writeAttribute('name', strtolower($group)); $writer->writeAttribute('name', strtolower((string) $group));
} }
foreach ($properties as $property) { foreach ($properties as $property) {
@ -487,7 +525,7 @@ class VCard extends VObject\Document
} }
} }
if (!empty($group)) { if ('' !== $group) {
$writer->endElement(); $writer->endElement();
} }
} }
@ -503,7 +541,7 @@ class VCard extends VObject\Document
$className = parent::getClassNameForPropertyName($propertyName); $className = parent::getClassNameForPropertyName($propertyName);
// In vCard 4, BINARY no longer exists, and we need URI instead. // In vCard 4, BINARY no longer exists, and we need URI instead.
if (VObject\Property\Binary::class == $className && self::VCARD40 === $this->getDocumentType()) { if (VObject\Property\Binary::class === $className && self::VCARD40 === $this->getDocumentType()) {
return VObject\Property\Uri::class; return VObject\Property\Uri::class;
} }

View file

@ -41,10 +41,10 @@ class VEvent extends VObject\Component
*/ */
public function isInTimeRange(\DateTimeInterface $start, \DateTimeInterface $end): bool public function isInTimeRange(\DateTimeInterface $start, \DateTimeInterface $end): bool
{ {
if ($this->RRULE) { if ($this->RRULE || $this->RDATE) {
try { try {
$it = new EventIterator($this, null, $start->getTimezone()); $it = new EventIterator($this, null, $start->getTimezone());
} catch (NoInstancesException $e) { } catch (NoInstancesException) {
// If we've caught this exception, there are no instances // If we've caught this exception, there are no instances
// for the event that fall into the specified time-range. // for the event that fall into the specified time-range.
return false; return false;
@ -117,6 +117,7 @@ class VEvent extends VObject\Component
'DTSTART' => $hasMethod ? '?' : '1', 'DTSTART' => $hasMethod ? '?' : '1',
'CLASS' => '?', 'CLASS' => '?',
'CREATED' => '?', 'CREATED' => '?',
'COLOR' => '?',
'DESCRIPTION' => '?', 'DESCRIPTION' => '?',
'GEO' => '?', 'GEO' => '?',
'LAST-MODIFIED' => '?', 'LAST-MODIFIED' => '?',
@ -137,7 +138,9 @@ class VEvent extends VObject\Component
'ATTENDEE' => '*', 'ATTENDEE' => '*',
'CATEGORIES' => '*', 'CATEGORIES' => '*',
'COMMENT' => '*', 'COMMENT' => '*',
'CONFERENCE' => '*',
'CONTACT' => '*', 'CONTACT' => '*',
'IMAGE' => '*',
'EXDATE' => '*', 'EXDATE' => '*',
'REQUEST-STATUS' => '*', 'REQUEST-STATUS' => '*',
'RELATED-TO' => '*', 'RELATED-TO' => '*',

View file

@ -41,7 +41,7 @@ class VFreeBusy extends VObject\Component
// Every period is formatted as [start]/[end]. The start is an // Every period is formatted as [start]/[end]. The start is an
// absolute UTC time, the end may be an absolute UTC time, or // absolute UTC time, the end may be an absolute UTC time, or
// duration (relative) value. // duration (relative) value.
list($busyStart, $busyEnd) = explode('/', $period); [$busyStart, $busyEnd] = explode('/', $period);
$busyStart = VObject\DateTimeParser::parse($busyStart); $busyStart = VObject\DateTimeParser::parse($busyStart);
$busyEnd = VObject\DateTimeParser::parse($busyEnd); $busyEnd = VObject\DateTimeParser::parse($busyEnd);

View file

@ -62,6 +62,7 @@ class VJournal extends VObject\Component
'CLASS' => '?', 'CLASS' => '?',
'CREATED' => '?', 'CREATED' => '?',
'COLOR' => '?',
'DTSTART' => '?', 'DTSTART' => '?',
'LAST-MODIFIED' => '?', 'LAST-MODIFIED' => '?',
'ORGANIZER' => '?', 'ORGANIZER' => '?',
@ -78,6 +79,7 @@ class VJournal extends VObject\Component
'CATEGORIES' => '*', 'CATEGORIES' => '*',
'COMMENT' => '*', 'COMMENT' => '*',
'CONTACT' => '*', 'CONTACT' => '*',
'IMAGE' => '*',
'DESCRIPTION' => '*', 'DESCRIPTION' => '*',
'EXDATE' => '*', 'EXDATE' => '*',
'RELATED-TO' => '*', 'RELATED-TO' => '*',

View file

@ -54,9 +54,9 @@ class VTodo extends VObject\Component
return return
($start < $due || $start <= $dtstart) ($start < $due || $start <= $dtstart)
&& ($end > $dtstart || $end >= $due); && ($end > $dtstart || $end >= $due);
} else {
return $start <= $dtstart && $end > $dtstart;
} }
return $start <= $dtstart && $end > $dtstart;
} }
if ($due) { if ($due) {
return $start < $due && $end >= $due; return $start < $due && $end >= $due;
@ -98,6 +98,7 @@ class VTodo extends VObject\Component
'CLASS' => '?', 'CLASS' => '?',
'COMPLETED' => '?', 'COMPLETED' => '?',
'CREATED' => '?', 'CREATED' => '?',
'COLOR' => '?',
'DESCRIPTION' => '?', 'DESCRIPTION' => '?',
'DTSTART' => '?', 'DTSTART' => '?',
'GEO' => '?', 'GEO' => '?',
@ -120,7 +121,9 @@ class VTodo extends VObject\Component
'ATTENDEE' => '*', 'ATTENDEE' => '*',
'CATEGORIES' => '*', 'CATEGORIES' => '*',
'COMMENT' => '*', 'COMMENT' => '*',
'CONFERENCE' => '*',
'CONTACT' => '*', 'CONTACT' => '*',
'IMAGE' => '*',
'EXDATE' => '*', 'EXDATE' => '*',
'REQUEST-STATUS' => '*', 'REQUEST-STATUS' => '*',
'RELATED-TO' => '*', 'RELATED-TO' => '*',

View file

@ -42,7 +42,7 @@ class DateTimeParser
try { try {
$date = new \DateTimeImmutable($matches[1].'-'.$matches[2].'-'.$matches[3].' '.$matches[4].':'.$matches[5].':'.$matches[6], $tz); $date = new \DateTimeImmutable($matches[1].'-'.$matches[2].'-'.$matches[3].' '.$matches[4].':'.$matches[5].':'.$matches[6], $tz);
} catch (\Exception $e) { } catch (\Exception) {
throw new InvalidDataException('The supplied iCalendar datetime value is incorrect: '.$dt); throw new InvalidDataException('The supplied iCalendar datetime value is incorrect: '.$dt);
} }
@ -63,13 +63,11 @@ class DateTimeParser
throw new InvalidDataException('The supplied iCalendar date value is incorrect: '.$date); throw new InvalidDataException('The supplied iCalendar date value is incorrect: '.$date);
} }
if (is_null($tz)) { $tz ??= new \DateTimeZone('UTC');
$tz = new \DateTimeZone('UTC');
}
try { try {
$date = new \DateTimeImmutable($matches[1].'-'.$matches[2].'-'.$matches[3], $tz); $date = new \DateTimeImmutable($matches[1].'-'.$matches[2].'-'.$matches[3], $tz);
} catch (\Exception $e) { } catch (\Exception) {
throw new InvalidDataException('The supplied iCalendar date value is incorrect: '.$date); throw new InvalidDataException('The supplied iCalendar date value is incorrect: '.$date);
} }
@ -205,9 +203,9 @@ class DateTimeParser
return self::parseDuration($date); return self::parseDuration($date);
} elseif (8 === strlen($date)) { } elseif (8 === strlen($date)) {
return self::parseDate($date, $referenceTz); return self::parseDate($date, $referenceTz);
} else {
return self::parseDateTime($date, $referenceTz);
} }
return self::parseDateTime($date, $referenceTz);
} }
/** /**
@ -329,7 +327,7 @@ class DateTimeParser
$result = []; $result = [];
foreach ($parts as $part) { foreach ($parts as $part) {
if (empty($matches[$part])) { if (!array_key_exists($part, $matches) || '' === $matches[$part]) {
$result[$part] = null; $result[$part] = null;
} elseif ('-' === $matches[$part] || '--' === $matches[$part]) { } elseif ('-' === $matches[$part] || '--' === $matches[$part]) {
$result[$part] = null; $result[$part] = null;
@ -424,7 +422,7 @@ class DateTimeParser
$result = []; $result = [];
foreach ($parts as $part) { foreach ($parts as $part) {
if (empty($matches[$part])) { if (!array_key_exists($part, $matches) || '' === $matches[$part]) {
$result[$part] = null; $result[$part] = null;
} elseif ('-' === $matches[$part]) { } elseif ('-' === $matches[$part]) {
$result[$part] = null; $result[$part] = null;
@ -541,7 +539,7 @@ class DateTimeParser
$parts['year0'] = &$parts['year']; $parts['year0'] = &$parts['year'];
foreach ($parts as $part => &$value) { foreach ($parts as $part => &$value) {
if (!empty($matches[$part])) { if (array_key_exists($part, $matches) && '' !== $matches[$part]) {
$value = $matches[$part]; $value = $matches[$part];
} }
} }

View file

@ -118,10 +118,10 @@ abstract class Document extends Component
public function create(string $name) public function create(string $name)
{ {
if (isset(static::$componentMap[strtoupper($name)])) { if (isset(static::$componentMap[strtoupper($name)])) {
return call_user_func_array([$this, 'createComponent'], func_get_args()); return $this->createComponent(...func_get_args());
} else {
return call_user_func_array([$this, 'createProperty'], func_get_args());
} }
return $this->createProperty(...func_get_args());
} }
/** /**
@ -146,9 +146,7 @@ abstract class Document extends Component
if (isset(static::$componentMap[$name])) { if (isset(static::$componentMap[$name])) {
$class = static::$componentMap[$name]; $class = static::$componentMap[$name];
} }
if (is_null($children)) { $children ??= [];
$children = [];
}
return new $class($this, $name, $children, $defaults); return new $class($this, $name, $children, $defaults);
} }
@ -180,26 +178,29 @@ abstract class Document extends Component
$class = null; $class = null;
// If a VALUE parameter is supplied, we have to use that
// According to https://datatracker.ietf.org/doc/html/rfc5545#section-3.2.20
// If the property's value is the default value type, then this
// parameter need not be specified. However, if the property's
// default value type is overridden by some other allowable value
// type, then this parameter MUST be specified.
if (!$valueType) {
$valueType = $parameters['VALUE'] ?? null;
}
if ($valueType) { if ($valueType) {
// The valueType argument comes first to figure out the correct // The valueType argument comes first to figure out the correct
// class. // class.
$class = $this->getClassNameForPropertyValue($valueType); $class = $this->getClassNameForPropertyValue($valueType);
} }
if (is_null($class)) { // If the value parameter is not set or set to something we do not recognize
// If a VALUE parameter is supplied, we should use that. // we do not attempt to interpret or parse the datass value as specified in
if (isset($parameters['VALUE'])) { // https://datatracker.ietf.org/doc/html/rfc5545#section-3.2.20
$class = $this->getClassNameForPropertyValue($parameters['VALUE']); // So when we so far did not get a class-name, we use the default for the property
if (is_null($class)) { $class ??= $this->getClassNameForPropertyName($name);
throw new InvalidDataException('Unsupported VALUE parameter for '.$name.' property. You supplied "'.$parameters['VALUE'].'"');
} $parameters ??= [];
} else {
$class = $this->getClassNameForPropertyName($name);
}
}
if (is_null($parameters)) {
$parameters = [];
}
return new $class($this, $name, $value, $parameters, $group, $lineIndex, $lineString); return new $class($this, $name, $value, $parameters, $group, $lineIndex, $lineString);
} }

View file

@ -11,25 +11,19 @@ namespace Sabre\VObject;
*/ */
class FreeBusyData class FreeBusyData
{ {
/**
* Start timestamp.
*/
protected int $start;
/**
* End timestamp.
*/
protected int $end;
/** /**
* A list of free-busy times. * A list of free-busy times.
*/ */
protected array $data; protected array $data;
public function __construct(int $start, int $end) public function __construct(/**
* Start timestamp.
*/
protected int $start, /**
* End timestamp.
*/
protected int $end)
{ {
$this->start = $start;
$this->end = $end;
$this->data = []; $this->data = [];
$this->data[] = [ $this->data[] = [

View file

@ -79,9 +79,7 @@ class FreeBusyGenerator
if ($objects) { if ($objects) {
$this->setObjects($objects); $this->setObjects($objects);
} }
if (is_null($timeZone)) { $timeZone ??= new \DateTimeZone('UTC');
$timeZone = new \DateTimeZone('UTC');
}
$this->setTimeZone($timeZone); $this->setTimeZone($timeZone);
} }
@ -217,7 +215,7 @@ class FreeBusyGenerator
$new = []; $new = [];
foreach ($old as $vavail) { foreach ($old as $vavail) {
list($compStart, $compEnd) = $vavail->getEffectiveStartEnd(); [$compStart, $compEnd] = $vavail->getEffectiveStartEnd();
// We don't care about date-times that are earlier or later than the // We don't care about date-times that are earlier or later than the
// start and end of the freebusy report, so this gets normalized // start and end of the freebusy report, so this gets normalized
@ -237,7 +235,7 @@ class FreeBusyGenerator
// Going through our existing list of components to see if there's // Going through our existing list of components to see if there's
// a higher priority component that already fully covers this one. // a higher priority component that already fully covers this one.
foreach ($new as $higherVavail) { foreach ($new as $higherVavail) {
list($higherStart, $higherEnd) = $higherVavail->getEffectiveStartEnd(); [$higherStart, $higherEnd] = $higherVavail->getEffectiveStartEnd();
if ( if (
(is_null($higherStart) || $higherStart < $compStart) (is_null($higherStart) || $higherStart < $compStart)
&& (is_null($higherEnd) || $higherEnd > $compEnd) && (is_null($higherEnd) || $higherEnd > $compEnd)
@ -259,7 +257,7 @@ class FreeBusyGenerator
// priority components to override the lower ones. // priority components to override the lower ones.
foreach (array_reverse($new) as $vavail) { foreach (array_reverse($new) as $vavail) {
$busyType = isset($vavail->BUSYTYPE) ? strtoupper($vavail->BUSYTYPE) : 'BUSY-UNAVAILABLE'; $busyType = isset($vavail->BUSYTYPE) ? strtoupper($vavail->BUSYTYPE) : 'BUSY-UNAVAILABLE';
list($vavailStart, $vavailEnd) = $vavail->getEffectiveStartEnd(); [$vavailStart, $vavailEnd] = $vavail->getEffectiveStartEnd();
// Making the component size no larger than the requested free-busy // Making the component size no larger than the requested free-busy
// report range. // report range.
@ -281,7 +279,7 @@ class FreeBusyGenerator
// Looping over the AVAILABLE components. // Looping over the AVAILABLE components.
if (isset($vavail->AVAILABLE)) { if (isset($vavail->AVAILABLE)) {
foreach ($vavail->AVAILABLE as $available) { foreach ($vavail->AVAILABLE as $available) {
list($availStart, $availEnd) = $available->getEffectiveStartEnd(); [$availStart, $availEnd] = $available->getEffectiveStartEnd();
$fbData->add( $fbData->add(
$availStart->getTimeStamp(), $availStart->getTimeStamp(),
$availEnd->getTimeStamp(), $availEnd->getTimeStamp(),
@ -362,7 +360,7 @@ class FreeBusyGenerator
if ($component->RRULE) { if ($component->RRULE) {
try { try {
$iterator = new EventIterator($object, (string) $component->UID, $this->timeZone); $iterator = new EventIterator($object, (string) $component->UID, $this->timeZone);
} catch (NoInstancesException $e) { } catch (NoInstancesException) {
// This event is recurring, but it doesn't have a single // This event is recurring, but it doesn't have a single
// instance. We are skipping this event from the output // instance. We are skipping this event from the output
// entirely. // entirely.
@ -437,12 +435,12 @@ class FreeBusyGenerator
continue; continue;
} }
$values = explode(',', $freebusy); $values = explode(',', (string) $freebusy);
foreach ($values as $value) { foreach ($values as $value) {
list($startTime, $endTime) = explode('/', $value); [$startTime, $endTime] = explode('/', $value);
$startTime = DateTimeParser::parseDateTime($startTime); $startTime = DateTimeParser::parseDateTime($startTime);
if ('P' === substr($endTime, 0, 1) || '-P' === substr($endTime, 0, 2)) { if (str_starts_with($endTime, 'P') || str_starts_with($endTime, '-P')) {
$duration = DateTimeParser::parseDuration($endTime); $duration = DateTimeParser::parseDuration($endTime);
$endTime = clone $startTime; $endTime = clone $startTime;
$endTime = $endTime->add($duration); $endTime = $endTime->add($duration);
@ -507,7 +505,7 @@ class FreeBusyGenerator
$vfreebusy->add($dtstamp); $vfreebusy->add($dtstamp);
foreach ($fbData->getData() as $busyTime) { foreach ($fbData->getData() as $busyTime) {
$busyType = strtoupper($busyTime['type']); $busyType = strtoupper((string) $busyTime['type']);
// Ignoring all the FREE parts, because those are already assumed. // Ignoring all the FREE parts, because those are already assumed.
if ('FREE' === $busyType) { if ('FREE' === $busyType) {

View file

@ -228,7 +228,7 @@ class Broker
$eventInfo = $oldEventInfo; $eventInfo = $oldEventInfo;
if (in_array($eventInfo['organizer'], $userHref)) { if (in_array($eventInfo['organizer'], $userHref, true)) {
// This is an organizer deleting the event. // This is an organizer deleting the event.
$eventInfo['attendees'] = []; $eventInfo['attendees'] = [];
// Increasing the sequence, but only if the organizer deleted // Increasing the sequence, but only if the organizer deleted
@ -237,7 +237,7 @@ class Broker
} else { } else {
// This is an attendee deleting the event. // This is an attendee deleting the event.
foreach ($eventInfo['attendees'] as $key => $attendee) { foreach ($eventInfo['attendees'] as $key => $attendee) {
if (in_array($attendee['href'], $userHref)) { if (in_array($attendee['href'], $userHref, true)) {
$eventInfo['attendees'][$key]['instances'] = ['master' => ['id' => 'master', 'partstat' => 'DECLINED'], $eventInfo['attendees'][$key]['instances'] = ['master' => ['id' => 'master', 'partstat' => 'DECLINED'],
]; ];
} }
@ -246,16 +246,29 @@ class Broker
$baseCalendar = $oldCalendar; $baseCalendar = $oldCalendar;
} }
if (in_array($eventInfo['organizer'], $userHref)) { // Check if the user is the organizer
if (in_array($eventInfo['organizer'], $userHref, true)) {
return $this->parseEventForOrganizer($baseCalendar, $eventInfo, $oldEventInfo); return $this->parseEventForOrganizer($baseCalendar, $eventInfo, $oldEventInfo);
} elseif ($oldCalendar) { }
// We need to figure out if the user is an attendee, but we're only
// doing so if there's an oldCalendar, because we only want to // Check if the user is an attendee
// process updates, not creation of new events. foreach ($eventInfo['attendees'] as $attendee) {
foreach ($eventInfo['attendees'] as $attendee) { if (in_array($attendee['href'], $userHref, true)) {
if (in_array($attendee['href'], $userHref)) { // If this is a event update, we always generate a reply
if ($oldCalendar) {
return $this->parseEventForAttendee($baseCalendar, $eventInfo, $oldEventInfo, $attendee['href']); return $this->parseEventForAttendee($baseCalendar, $eventInfo, $oldEventInfo, $attendee['href']);
} }
// If this is a new event, we only generate a reply if the participation status is set
foreach ($attendee['instances'] as $instance) {
if (isset($instance['partstat']) && 'NEEDS-ACTION' !== $instance['partstat']) {
// Attendee has responded (ACCEPTED/DECLINED/TENTATIVE) - generate REPLY
return $this->parseEventForAttendee($baseCalendar, $eventInfo, $oldEventInfo, $attendee['href']);
}
}
// User is attendee but no response to process
break;
} }
} }
@ -341,7 +354,7 @@ class Broker
$instances[$recurId] = $attendee['PARTSTAT']->getValue(); $instances[$recurId] = $attendee['PARTSTAT']->getValue();
if (isset($vevent->{'REQUEST-STATUS'})) { if (isset($vevent->{'REQUEST-STATUS'})) {
$requestStatus = $vevent->{'REQUEST-STATUS'}->getValue(); $requestStatus = $vevent->{'REQUEST-STATUS'}->getValue();
list($requestStatus) = explode(';', $requestStatus); [$requestStatus] = explode(';', $requestStatus);
} }
} }
@ -479,7 +492,6 @@ class Broker
} }
$messages = []; $messages = [];
foreach ($attendees as $attendee) { foreach ($attendees as $attendee) {
// An organizer can also be an attendee. We should not generate any // An organizer can also be an attendee. We should not generate any
// messages for those. // messages for those.
@ -554,7 +566,7 @@ class Broker
$message->significantChange = $message->significantChange =
'REQUEST' === $attendee['forceSend'] 'REQUEST' === $attendee['forceSend']
|| count($oldAttendeeInstances) != count($newAttendeeInstances) || count($oldAttendeeInstances) !== count($newAttendeeInstances)
|| count(array_diff($oldAttendeeInstances, $newAttendeeInstances)) > 0 || count(array_diff($oldAttendeeInstances, $newAttendeeInstances)) > 0
|| $oldEventInfo['significantChangeHash'] !== $eventInfo['significantChangeHash']; || $oldEventInfo['significantChangeHash'] !== $eventInfo['significantChangeHash'];
@ -564,9 +576,9 @@ class Broker
// We need to find a list of events that the attendee // We need to find a list of events that the attendee
// is not a part of to add to the list of exceptions. // is not a part of to add to the list of exceptions.
$exceptions = []; $exceptions = [];
foreach ($eventInfo['instances'] as $instanceId => $vevent) { foreach ($eventInfo['instances'] as $eventInstanceId => $vevent) {
if (!isset($attendee['newInstances'][$instanceId])) { if (!isset($attendee['newInstances'][$eventInstanceId])) {
$exceptions[] = $instanceId; $exceptions[] = $eventInstanceId;
} }
} }
@ -580,6 +592,9 @@ class Broker
)); ));
} else { } else {
$currentEvent->EXDATE = $exceptions; $currentEvent->EXDATE = $exceptions;
if ($currentEvent->DTSTART['TZID']) {
$currentEvent->EXDATE['TZID'] = clone $currentEvent->DTSTART['TZID'];
}
} }
} }
@ -588,15 +603,13 @@ class Broker
unset($currentEvent->ORGANIZER['SCHEDULE-FORCE-SEND']); unset($currentEvent->ORGANIZER['SCHEDULE-FORCE-SEND']);
unset($currentEvent->ORGANIZER['SCHEDULE-STATUS']); unset($currentEvent->ORGANIZER['SCHEDULE-STATUS']);
foreach ($currentEvent->ATTENDEE as $attendee) { foreach ($currentEvent->ATTENDEE as $currentEventAttendee) {
unset($attendee['SCHEDULE-FORCE-SEND']); unset($currentEventAttendee['SCHEDULE-FORCE-SEND']);
unset($attendee['SCHEDULE-STATUS']); unset($currentEventAttendee['SCHEDULE-STATUS']);
// We're adding PARTSTAT=NEEDS-ACTION to ensure that // We're adding PARTSTAT=NEEDS-ACTION to ensure that
// iOS shows an "Inbox Item" // iOS shows an "Inbox Item"
if (!isset($attendee['PARTSTAT'])) { $currentEventAttendee['PARTSTAT'] ??= 'NEEDS-ACTION';
$attendee['PARTSTAT'] = 'NEEDS-ACTION';
}
} }
} }
@ -633,7 +646,12 @@ class Broker
return []; return [];
} }
$oldInstances = !empty($oldEventInfo['attendees'][$attendee]['instances']) ? $oldInstances =
is_array($oldEventInfo['attendees'])
&& array_key_exists($attendee, $oldEventInfo['attendees'])
&& is_array($oldEventInfo['attendees'][$attendee])
&& array_key_exists('instances', $oldEventInfo['attendees'][$attendee])
&& is_array($oldEventInfo['attendees'][$attendee]['instances']) ?
$oldEventInfo['attendees'][$attendee]['instances'] : $oldEventInfo['attendees'][$attendee]['instances'] :
[]; [];
@ -664,7 +682,7 @@ class Broker
// We only need to do that though, if the master event is not declined. // We only need to do that though, if the master event is not declined.
if (isset($instances['master']) && 'DECLINED' !== $instances['master']['newstatus']) { if (isset($instances['master']) && 'DECLINED' !== $instances['master']['newstatus']) {
foreach ($eventInfo['exdate'] as $exDate) { foreach ($eventInfo['exdate'] as $exDate) {
if (!in_array($exDate, $oldEventInfo['exdate'])) { if (!in_array($exDate, $oldEventInfo['exdate'] ?? [], true)) {
if (isset($instances[$exDate])) { if (isset($instances[$exDate])) {
$instances[$exDate]['newstatus'] = 'DECLINED'; $instances[$exDate]['newstatus'] = 'DECLINED';
} else { } else {
@ -707,7 +725,7 @@ class Broker
$hasReply = false; $hasReply = false;
foreach ($instances as $instance) { foreach ($instances as $instance) {
if ($instance['oldstatus'] == $instance['newstatus'] && 'REPLY' !== $eventInfo['organizerForceSend']) { if ($instance['oldstatus'] === $instance['newstatus'] && 'REPLY' !== $eventInfo['organizerForceSend']) {
// Skip // Skip
continue; continue;
} }
@ -739,7 +757,7 @@ class Broker
// EXDATE // EXDATE
$dt = DateTimeParser::parse($instance['id'], $eventInfo['timezone']); $dt = DateTimeParser::parse($instance['id'], $eventInfo['timezone']);
// Treat is as a DATE field // Treat is as a DATE field
if (strlen($instance['id']) <= 8) { if (strlen((string) $instance['id']) <= 8) {
$event->add('DTSTART', $dt, ['VALUE' => 'DATE']); $event->add('DTSTART', $dt, ['VALUE' => 'DATE']);
} else { } else {
$event->add('DTSTART', $dt); $event->add('DTSTART', $dt);
@ -751,7 +769,7 @@ class Broker
if ('master' !== $instance['id']) { if ('master' !== $instance['id']) {
$dt = DateTimeParser::parse($instance['id'], $eventInfo['timezone']); $dt = DateTimeParser::parse($instance['id'], $eventInfo['timezone']);
// Treat is as a DATE field // Treat is as a DATE field
if (strlen($instance['id']) <= 8) { if (strlen((string) $instance['id']) <= 8) {
$event->add('RECURRENCE-ID', $dt, ['VALUE' => 'DATE']); $event->add('RECURRENCE-ID', $dt, ['VALUE' => 'DATE']);
} else { } else {
$event->add('RECURRENCE-ID', $dt); $event->add('RECURRENCE-ID', $dt);
@ -774,9 +792,9 @@ class Broker
$message->message = $icalMsg; $message->message = $icalMsg;
return [$message]; return [$message];
} else {
return [];
} }
return [];
} }
/** /**
@ -843,7 +861,7 @@ class Broker
$organizer = $vevent->ORGANIZER->getNormalizedValue(); $organizer = $vevent->ORGANIZER->getNormalizedValue();
$organizerName = $vevent->ORGANIZER['CN'] ?? null; $organizerName = $vevent->ORGANIZER['CN'] ?? null;
} else { } else {
if (strtoupper($organizer) !== strtoupper($vevent->ORGANIZER->getNormalizedValue())) { if (strtoupper($organizer) !== strtoupper((string) $vevent->ORGANIZER->getNormalizedValue())) {
throw new SameOrganizerForAllComponentsException('Every instance of the event must have the same organizer.'); throw new SameOrganizerForAllComponentsException('Every instance of the event must have the same organizer.');
} }
} }
@ -869,7 +887,7 @@ class Broker
foreach ($vevent->select('RRULE') as $rr) { foreach ($vevent->select('RRULE') as $rr) {
foreach ($rr->getParts() as $key => $val) { foreach ($rr->getParts() as $key => $val) {
// ignore default values (https://github.com/sabre-io/vobject/issues/126) // ignore default values (https://github.com/sabre-io/vobject/issues/126)
if ('INTERVAL' === $key && 1 == $val) { if ('INTERVAL' === $key && '1' === (string) $val) {
continue; continue;
} }
if (is_array($val)) { if (is_array($val)) {
@ -881,7 +899,7 @@ class Broker
sort($rrule); sort($rrule);
} }
if (isset($vevent->STATUS)) { if (isset($vevent->STATUS)) {
$status = strtoupper($vevent->STATUS->getValue()); $status = strtoupper((string) $vevent->STATUS->getValue());
} }
$recurId = isset($vevent->{'RECURRENCE-ID'}) ? $vevent->{'RECURRENCE-ID'}->getValue() : 'master'; $recurId = isset($vevent->{'RECURRENCE-ID'}) ? $vevent->{'RECURRENCE-ID'}->getValue() : 'master';
@ -892,11 +910,14 @@ class Broker
$timezone = $vevent->{'RECURRENCE-ID'}->getDateTime()->getTimeZone(); $timezone = $vevent->{'RECURRENCE-ID'}->getDateTime()->getTimeZone();
} }
} }
$instances[$recurId] = $vevent;
if (isset($vevent->ATTENDEE)) { if (isset($vevent->ATTENDEE)) {
foreach ($vevent->ATTENDEE as $attendee) { foreach ($vevent->ATTENDEE as $attendee) {
if ($this->scheduleAgentServerRules if ($this->scheduleAgentServerRules
&& isset($attendee['SCHEDULE-AGENT']) && isset($attendee['SCHEDULE-AGENT'])
&& 'CLIENT' === strtoupper($attendee['SCHEDULE-AGENT']->getValue()) && 'CLIENT' === strtoupper((string) $attendee['SCHEDULE-AGENT']->getValue())
) { ) {
continue; continue;
} }
@ -930,10 +951,10 @@ class Broker
]; ];
} }
} }
$instances[$recurId] = $vevent;
} }
foreach ($this->significantChangeProperties as $prop) { foreach ($this->significantChangeProperties as $prop) {
// @phpstan-ignore property.dynamicName
if (isset($vevent->$prop)) { if (isset($vevent->$prop)) {
$propertyValues = $vevent->select($prop); $propertyValues = $vevent->select($prop);

View file

@ -32,12 +32,12 @@ class Message
* Contains the ITip method, which is something like REQUEST, REPLY or * Contains the ITip method, which is something like REQUEST, REPLY or
* CANCEL. * CANCEL.
*/ */
public ?string $method; public ?string $method = null;
/** /**
* The current sequence number for the event. * The current sequence number for the event.
*/ */
public ?int $sequence; public ?int $sequence = null;
/** /**
* The senders' email address. * The senders' email address.
@ -52,7 +52,7 @@ class Message
* The name of the sender. This is often populated from a CN parameter from * The name of the sender. This is often populated from a CN parameter from
* either the ORGANIZER or ATTENDEE, depending on the message. * either the ORGANIZER or ATTENDEE, depending on the message.
*/ */
public ?string $senderName; public ?string $senderName = null;
/** /**
* The recipient's email address. * The recipient's email address.
@ -63,7 +63,7 @@ class Message
* The name of the recipient. This is usually populated with the CN * The name of the recipient. This is usually populated with the CN
* parameter from the ATTENDEE or ORGANIZER property, if it's available. * parameter from the ATTENDEE or ORGANIZER property, if it's available.
*/ */
public ?string $recipientName; public ?string $recipientName = null;
/** /**
* After the message has been delivered, this should contain a string such * After the message has been delivered, this should contain a string such
@ -107,10 +107,9 @@ class Message
{ {
if (!$this->scheduleStatus) { if (!$this->scheduleStatus) {
return false; return false;
} else {
list($scheduleStatus) = explode(';', $this->scheduleStatus);
return $scheduleStatus;
} }
[$scheduleStatus] = explode(';', $this->scheduleStatus);
return $scheduleStatus;
} }
} }

View file

@ -42,7 +42,7 @@ abstract class Node implements \IteratorAggregate, \ArrayAccess, \Countable, \Js
/** /**
* Reference to the parent object, if this is not the top object. * Reference to the parent object, if this is not the top object.
*/ */
public ?Node $parent; public ?Node $parent = null;
/** /**
* Iterator override. * Iterator override.
@ -52,7 +52,7 @@ abstract class Node implements \IteratorAggregate, \ArrayAccess, \Countable, \Js
/** /**
* The root document. * The root document.
*/ */
protected ?Component $root; protected ?Component $root = null;
/** /**
* Serializes the node into a mimedir format. * Serializes the node into a mimedir format.

View file

@ -41,7 +41,7 @@ trait PHPUnitAssertions
$input = Reader::read($input); $input = Reader::read($input);
} }
if (!$input instanceof Component) { if (!$input instanceof Component) {
$this->fail('Input must be a string, stream or VObject component'); self::fail('Input must be a string, stream or VObject component');
} }
unset($input->PRODID); unset($input->PRODID);
if ($input instanceof Component\VCalendar && 'GREGORIAN' === (string) $input->CALSCALE) { if ($input instanceof Component\VCalendar && 'GREGORIAN' === (string) $input->CALSCALE) {
@ -51,23 +51,28 @@ trait PHPUnitAssertions
return $input; return $input;
}; };
$expected = $getObj($expected)->serialize(); /**
$actual = $getObj($actual)->serialize(); * @var string $expectedSerialized
*/
$expectedSerialized = $getObj($expected)->serialize();
$actualSerialized = $getObj($actual)->serialize();
// Finding wildcards in expected. // Finding wildcards in expected.
preg_match_all('|^([A-Z]+):\\*\\*ANY\\*\\*\r$|m', $expected, $matches, PREG_SET_ORDER); $result = preg_match_all('|^([A-Z]+):\\*\\*ANY\\*\\*\r$|m', $expectedSerialized, $matches, PREG_SET_ORDER);
self::assertNotFalse($result);
foreach ($matches as $match) { foreach ($matches as $match) {
$actual = preg_replace( $actualSerialized = preg_replace(
'|^'.preg_quote($match[1], '|').':(.*)\r$|m', '|^'.preg_quote($match[1], '|').':(.*)\r$|m',
$match[1].':**ANY**'."\r", $match[1].':**ANY**'."\r",
$actual (string) $actualSerialized
); );
} }
$this->assertEquals( self::assertEquals(
$expected, $expectedSerialized,
$actual, $actualSerialized,
$message $message
); );
} }

View file

@ -16,7 +16,7 @@ use Sabre\Xml;
* @author Evert Pot (http://evertpot.com/) * @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License * @license http://sabre.io/license/ Modified BSD License
*/ */
class Parameter extends Node class Parameter extends Node implements \Stringable
{ {
/** /**
* Parameter name. * Parameter name.
@ -74,83 +74,12 @@ class Parameter extends Node
*/ */
public static function guessParameterNameByValue(string $value): string public static function guessParameterNameByValue(string $value): string
{ {
switch (strtoupper($value)) { $name = match (strtoupper($value)) {
// Encodings '7-BIT', 'QUOTED-PRINTABLE', 'BASE64' => 'ENCODING',
case '7-BIT': 'WORK', 'HOME', 'PREF', 'DOM', 'INTL', 'POSTAL', 'PARCEL', 'VOICE', 'FAX', 'MSG', 'CELL', 'PAGER', 'BBS', 'MODEM', 'CAR', 'ISDN', 'VIDEO', 'AOL', 'APPLELINK', 'ATTMAIL', 'CIS', 'EWORLD', 'INTERNET', 'IBMMAIL', 'MCIMAIL', 'POWERSHARE', 'PRODIGY', 'TLX', 'X400', 'GIF', 'CGM', 'WMF', 'BMP', 'DIB', 'PICT', 'TIFF', 'PDF', 'PS', 'JPEG', 'MPEG', 'MPEG2', 'AVI', 'QTIME', 'WAVE', 'PCM', 'AIFF', 'X509', 'PGP' => 'TYPE',
case 'QUOTED-PRINTABLE': 'INLINE', 'URL', 'CONTENT-ID', 'CID' => 'VALUE',
case 'BASE64': default => '',
$name = 'ENCODING'; };
break;
// Common types
case 'WORK':
case 'HOME':
case 'PREF':
// Delivery Label Type
case 'DOM':
case 'INTL':
case 'POSTAL':
case 'PARCEL':
// Telephone types
case 'VOICE':
case 'FAX':
case 'MSG':
case 'CELL':
case 'PAGER':
case 'BBS':
case 'MODEM':
case 'CAR':
case 'ISDN':
case 'VIDEO':
// EMAIL types (lol)
case 'AOL':
case 'APPLELINK':
case 'ATTMAIL':
case 'CIS':
case 'EWORLD':
case 'INTERNET':
case 'IBMMAIL':
case 'MCIMAIL':
case 'POWERSHARE':
case 'PRODIGY':
case 'TLX':
case 'X400':
// Photo / Logo format types
case 'GIF':
case 'CGM':
case 'WMF':
case 'BMP':
case 'DIB':
case 'PICT':
case 'TIFF':
case 'PDF':
case 'PS':
case 'JPEG':
case 'MPEG':
case 'MPEG2':
case 'AVI':
case 'QTIME':
// Sound Digital Audio Type
case 'WAVE':
case 'PCM':
case 'AIFF':
// Key types
case 'X509':
case 'PGP':
$name = 'TYPE';
break;
// Value types
case 'INLINE':
case 'URL':
case 'CONTENT-ID':
case 'CID':
$name = 'VALUE';
break;
default:
$name = '';
}
return $name; return $name;
} }
@ -177,9 +106,9 @@ class Parameter extends Node
{ {
if (is_array($this->value)) { if (is_array($this->value)) {
return implode(',', $this->value); return implode(',', $this->value);
} else {
return $this->value;
} }
return $this->value;
} }
/** /**
@ -201,9 +130,9 @@ class Parameter extends Node
return $this->value; return $this->value;
} elseif (is_null($this->value)) { } elseif (is_null($this->value)) {
return []; return [];
} else {
return [$this->value];
} }
return [$this->value];
} }
/** /**
@ -234,7 +163,8 @@ class Parameter extends Node
{ {
return in_array( return in_array(
strtolower($value), strtolower($value),
array_map('strtolower', (array) $this->value) array_map(strtolower(...), (array) $this->value),
true
); );
} }
@ -278,20 +208,19 @@ class Parameter extends Node
// added + as well. // added + as well.
if (!preg_match('#(?: [\n":;\^,\+] )#x', $item)) { if (!preg_match('#(?: [\n":;\^,\+] )#x', $item)) {
return $out.$item; return $out.$item;
} else {
// Enclosing in double-quotes, and using RFC6868 for encoding any
// special characters
$out .= '"'.strtr(
$item,
[
'^' => '^^',
"\n" => '^n',
'"' => '^\'',
]
).'"';
return $out;
} }
// Enclosing in double-quotes, and using RFC6868 for encoding any
// special characters
$out .= '"'.strtr(
$item,
[
'^' => '^^',
"\n" => '^n',
'"' => '^\'',
]
).'"';
return $out;
} }
); );
} }

View file

@ -27,12 +27,12 @@ class Json extends Parser
/** /**
* The input data. * The input data.
*/ */
protected ?array $input; protected ?array $input = null;
/** /**
* Root component. * Root component.
*/ */
protected ?Document $root; protected ?Document $root = null;
/** /**
* This method starts the parsing process. * This method starts the parsing process.
@ -61,16 +61,11 @@ class Json extends Parser
$this->options = $options; $this->options = $options;
} }
switch ($this->input[0]) { $this->root = match ($this->input[0]) {
case 'vcalendar': 'vcalendar' => new VCalendar([], false),
$this->root = new VCalendar([], false); 'vcard' => new VCard([], false),
break; default => throw new ParseException('The root component must either be a vcalendar, or a vcard'),
case 'vcard': };
$this->root = new VCard([], false);
break;
default:
throw new ParseException('The root component must either be a vcalendar, or a vcard');
}
foreach ($this->input[1] as $prop) { foreach ($this->input[1] as $prop) {
$this->root->add($this->parseProperty($prop)); $this->root->add($this->parseProperty($prop));
} }
@ -97,17 +92,13 @@ class Json extends Parser
$self = $this; $self = $this;
$properties = array_map( $properties = array_map(
function ($jProp) use ($self) { $self->parseProperty(...),
return $self->parseProperty($jProp);
},
$jComp[1] $jComp[1]
); );
if (isset($jComp[2])) { if (isset($jComp[2])) {
$components = array_map( $components = array_map(
function ($jComp) use ($self) { $self->parseComponent(...),
return $self->parseComponent($jComp);
},
$jComp[2] $jComp[2]
); );
} else { } else {
@ -134,7 +125,7 @@ class Json extends Parser
$valueType, $valueType,
) = $jProp; ) = $jProp;
$propertyName = strtoupper($propertyName); $propertyName = strtoupper((string) $propertyName);
// This is the default class we would be using if we didn't know the // This is the default class we would be using if we didn't know the
// value type. We're using this value later in this function. // value type. We're using this value later in this function.
@ -144,7 +135,7 @@ class Json extends Parser
$value = array_slice($jProp, 3); $value = array_slice($jProp, 3);
$valueType = strtoupper($valueType); $valueType = strtoupper((string) $valueType);
if (isset($parameters['group'])) { if (isset($parameters['group'])) {
$propertyName = $parameters['group'].'.'.$propertyName; $propertyName = $parameters['group'].'.'.$propertyName;
@ -169,7 +160,7 @@ class Json extends Parser
// If the value type we received (e.g.: TEXT) was not the default value // If the value type we received (e.g.: TEXT) was not the default value
// type for the given property (e.g.: BDAY), we need to add a VALUE= // type for the given property (e.g.: BDAY), we need to add a VALUE=
// parameter. // parameter.
if ($defaultPropertyClass !== get_class($prop)) { if ($defaultPropertyClass !== $prop::class) {
$prop['VALUE'] = $valueType; $prop['VALUE'] = $valueType;
} }

View file

@ -26,6 +26,11 @@ use Sabre\VObject\Property;
*/ */
class MimeDir extends Parser class MimeDir extends Parser
{ {
public const TOKEN_PROPNAME = 1;
public const TOKEN_PROPVALUE = 2;
public const TOKEN_PARAMNAME = 3;
public const TOKEN_PARAMVALUE = 4;
/** /**
* The input stream. * The input stream.
* *
@ -36,7 +41,7 @@ class MimeDir extends Parser
/** /**
* Root component. * Root component.
*/ */
protected ?Document $root; protected ?Document $root = null;
/** /**
* By default, all input will be assumed to be UTF-8. * By default, all input will be assumed to be UTF-8.
@ -106,7 +111,7 @@ class MimeDir extends Parser
*/ */
public function setCharset(string $charset): void public function setCharset(string $charset): void
{ {
if (!in_array($charset, self::$SUPPORTED_CHARSETS)) { if (!in_array($charset, self::$SUPPORTED_CHARSETS, true)) {
throw new \InvalidArgumentException('Unsupported encoding. (Supported encodings: '.implode(', ', self::$SUPPORTED_CHARSETS).')'); throw new \InvalidArgumentException('Unsupported encoding. (Supported encodings: '.implode(', ', self::$SUPPORTED_CHARSETS).')');
} }
$this->charset = $charset; $this->charset = $charset;
@ -150,23 +155,18 @@ class MimeDir extends Parser
// BOM is ZERO WIDTH NO-BREAK SPACE (U+FEFF). // BOM is ZERO WIDTH NO-BREAK SPACE (U+FEFF).
// It's 0xEF 0xBB 0xBF in UTF-8 hex. // It's 0xEF 0xBB 0xBF in UTF-8 hex.
if (3 <= strlen($line) if (3 <= strlen((string) $line)
&& 0xEF === ord($line[0]) && 0xEF === ord($line[0])
&& 0xBB === ord($line[1]) && 0xBB === ord($line[1])
&& 0xBF === ord($line[2])) { && 0xBF === ord($line[2])) {
$line = \substr($line, 3); $line = \substr((string) $line, 3);
} }
switch (strtoupper($line)) { $class = match (strtoupper((string) $line)) {
case 'BEGIN:VCALENDAR': 'BEGIN:VCALENDAR' => VCalendar::$componentMap['VCALENDAR'],
$class = VCalendar::$componentMap['VCALENDAR']; 'BEGIN:VCARD' => VCard::$componentMap['VCARD'],
break; default => throw new ParseException('This parser only supports VCARD and VCALENDAR files'),
case 'BEGIN:VCARD': };
$class = VCard::$componentMap['VCARD'];
break;
default:
throw new ParseException('This parser only supports VCARD and VCALENDAR files');
}
$this->root = new $class([], false); $this->root = new $class([], false);
@ -174,10 +174,10 @@ class MimeDir extends Parser
// Reading until we hit END: // Reading until we hit END:
try { try {
$line = $this->readLine(); $line = $this->readLine();
} catch (EofException $oEx) { } catch (EofException) {
$line = 'END:'.$this->root->name; $line = 'END:'.$this->root->name;
} }
if ('END:' === strtoupper(\substr($line, 0, 4))) { if ('END:' === strtoupper(\substr((string) $line, 0, 4))) {
break; break;
} }
$result = $this->parseLine($line); $result = $this->parseLine($line);
@ -186,7 +186,7 @@ class MimeDir extends Parser
} }
} }
$name = strtoupper(\substr($line, 4)); $name = strtoupper(\substr((string) $line, 4));
if ($name !== $this->root->name) { if ($name !== $this->root->name) {
throw new ParseException('Invalid MimeDir file. expected: "END:'.$this->root->name.'" got: "END:'.$name.'"'); throw new ParseException('Invalid MimeDir file. expected: "END:'.$this->root->name.'" got: "END:'.$name.'"');
} }
@ -215,7 +215,7 @@ class MimeDir extends Parser
while (true) { while (true) {
// Reading until we hit END: // Reading until we hit END:
$line = $this->readLine(); $line = $this->readLine();
if ('END:' === strtoupper(\substr($line, 0, 4))) { if ('END:' === strtoupper(\substr((string) $line, 0, 4))) {
break; break;
} }
$result = $this->parseLine($line); $result = $this->parseLine($line);
@ -224,22 +224,21 @@ class MimeDir extends Parser
} }
} }
$name = strtoupper(\substr($line, 4)); $name = strtoupper(\substr((string) $line, 4));
if ($name !== $component->name) { if ($name !== $component->name) {
throw new ParseException('Invalid MimeDir file. expected: "END:'.$component->name.'" got: "END:'.$name.'"'); throw new ParseException('Invalid MimeDir file. expected: "END:'.$component->name.'" got: "END:'.$name.'"');
} }
return $component; return $component;
} else {
// Property reader
$property = $this->readProperty($line);
if (!$property) {
// Ignored line
return false;
}
return $property;
} }
// Property reader
$property = $this->readProperty($line);
if (!$property) {
// Ignored line
return false;
}
return $property;
} }
/** /**
@ -360,6 +359,12 @@ class MimeDir extends Parser
'value' => null, 'value' => null,
]; ];
/*
* Keep track on the last token we parsed in order to do
* better error checking
*/
$lastToken = null;
$lastParam = null; $lastParam = null;
/* /*
@ -385,10 +390,16 @@ class MimeDir extends Parser
// option is set to ignore invalid lines, we ignore this line // option is set to ignore invalid lines, we ignore this line
// This can happen when servers provide faulty data as iCloud // This can happen when servers provide faulty data as iCloud
// frequently does with X-APPLE-STRUCTURED-LOCATION // frequently does with X-APPLE-STRUCTURED-LOCATION
$lastToken = self::TOKEN_PARAMVALUE;
continue; continue;
} }
throw new ParseException('Invalid Mimedir file. Line starting at '.$this->startLine.' did not follow iCalendar/vCard conventions'); throw new ParseException('Invalid Mimedir file. Line starting at '.$this->startLine.' did not follow iCalendar/vCard conventions');
} }
if ('=' === $match[0][0] && self::TOKEN_PARAMNAME !== $lastToken) {
throw new ParseException('Invalid Mimedir file. Line starting at '.$this->startLine.': Missing parameter name for parameter value "'.$match['paramValue'].'"');
}
if (\is_null($property['parameters'][$lastParam])) { if (\is_null($property['parameters'][$lastParam])) {
$property['parameters'][$lastParam] = $value; $property['parameters'][$lastParam] = $value;
} elseif (is_array($property['parameters'][$lastParam])) { } elseif (is_array($property['parameters'][$lastParam])) {
@ -402,21 +413,23 @@ class MimeDir extends Parser
$value, $value,
]; ];
} }
$lastToken = self::TOKEN_PARAMVALUE;
continue; continue;
} }
if (isset($match['paramName'])) { if (isset($match['paramName'])) {
$lastParam = strtoupper($match['paramName']); $lastParam = strtoupper($match['paramName']);
if (!isset($property['parameters'][$lastParam])) { $property['parameters'][$lastParam] ??= null;
$property['parameters'][$lastParam] = null; $lastToken = self::TOKEN_PARAMNAME;
}
continue; continue;
} }
if (isset($match['propValue'])) { if (isset($match['propValue'])) {
$property['value'] = $match['propValue']; $property['value'] = $match['propValue'];
$lastToken = self::TOKEN_PROPVALUE;
continue; continue;
} }
if (isset($match['name']) && $match['name']) { if (isset($match['name']) && 0 < strlen($match['name'])) {
$property['name'] = strtoupper($match['name']); $property['name'] = strtoupper($match['name']);
$lastToken = self::TOKEN_PROPNAME;
continue; continue;
} }
@ -425,10 +438,8 @@ class MimeDir extends Parser
// @codeCoverageIgnoreEnd // @codeCoverageIgnoreEnd
} }
if (\is_null($property['value'])) { $property['value'] ??= '';
$property['value'] = ''; if (!isset($property['name']) || 0 === strlen($property['name'])) {
}
if (!$property['name']) {
if ($this->options & self::OPTION_IGNORE_INVALID_LINES) { if ($this->options & self::OPTION_IGNORE_INVALID_LINES) {
return false; return false;
} }
@ -671,7 +682,7 @@ class MimeDir extends Parser
// missing a whitespace. So if 'forgiving' is turned on, we will take // missing a whitespace. So if 'forgiving' is turned on, we will take
// those as well. // those as well.
if ($this->options & self::OPTION_FORGIVING) { if ($this->options & self::OPTION_FORGIVING) {
while ('=' === \substr($value, -1) && $this->lineBuffer) { while (str_ends_with($value, '=') && $this->lineBuffer) {
// Reading the line // Reading the line
$this->readLine(); $this->readLine();
// Grabbing the raw form // Grabbing the raw form

View file

@ -30,11 +30,6 @@ abstract class Parser
*/ */
public const OPTION_IGNORE_INVALID_LINES = 2; public const OPTION_IGNORE_INVALID_LINES = 2;
/**
* Bitmask of parser options.
*/
protected int $options;
/** /**
* Creates the parser. * Creates the parser.
* *
@ -42,12 +37,11 @@ abstract class Parser
* *
* @param int $options any parser options (OPTION constants) * @param int $options any parser options (OPTION constants)
*/ */
public function __construct($input = null, int $options = 0) public function __construct($input = null, protected int $options = 0)
{ {
if (!is_null($input)) { if (!is_null($input)) {
$this->setInput($input); $this->setInput($input);
} }
$this->options = $options;
} }
/** /**

View file

@ -28,17 +28,17 @@ class XML extends Parser
/** /**
* The input data. * The input data.
*/ */
protected ?array $input; protected ?array $input = null;
/** /**
* A pointer/reference to the input. * A pointer/reference to the input.
*/ */
private ?array $pointer; private ?array $pointer = null;
/** /**
* Document, root component. * Document, root component.
*/ */
protected ?Document $root; protected ?Document $root = null;
/** /**
* Creates the parser. * Creates the parser.
@ -112,7 +112,7 @@ class XML extends Parser
*/ */
protected function parseVCalendarComponents(Component $parentComponent): void protected function parseVCalendarComponents(Component $parentComponent): void
{ {
foreach ($this->pointer['value'] ?: [] as $children) { foreach ($this->pointer['value'] ?? [] as $children) {
switch (static::getTagName($children['name'])) { switch (static::getTagName($children['name'])) {
case 'properties': case 'properties':
$this->pointer = &$children['value']; $this->pointer = &$children['value'];
@ -145,8 +145,8 @@ class XML extends Parser
*/ */
protected function parseProperties(Component $parentComponent, string $propertyNamePrefix = ''): void protected function parseProperties(Component $parentComponent, string $propertyNamePrefix = ''): void
{ {
foreach ($this->pointer ?: [] as $xmlProperty) { foreach ($this->pointer ?? [] as $xmlProperty) {
list($namespace, $tagName) = SabreXml\Service::parseClarkNotation($xmlProperty['name']); [$namespace, $tagName] = SabreXml\Service::parseClarkNotation($xmlProperty['name']);
$propertyName = $tagName; $propertyName = $tagName;
$propertyValue = []; $propertyValue = [];
@ -322,7 +322,7 @@ class XML extends Parser
*/ */
protected function parseComponent(Component $parentComponent): void protected function parseComponent(Component $parentComponent): void
{ {
$components = $this->pointer['value'] ?: []; $components = $this->pointer['value'] ?? [];
foreach ($components as $component) { foreach ($components as $component) {
$componentName = static::getTagName($component['name']); $componentName = static::getTagName($component['name']);

View file

@ -14,27 +14,8 @@ use Sabre\Xml;
* @author Evert Pot (http://evertpot.com/) * @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License * @license http://sabre.io/license/ Modified BSD License
*/ */
abstract class Property extends Node abstract class Property extends Node implements \Stringable
{ {
/**
* The root document.
*/
public ?Component $root;
/**
* Property name.
*
* This will contain a string such as DTSTART, SUMMARY, FN.
*/
public ?string $name;
/**
* Property group.
*
* This is only used in vcards
*/
public ?string $group;
/** /**
* List of parameters. * List of parameters.
*/ */
@ -75,13 +56,13 @@ abstract class Property extends Node
* @param array $parameters List of parameters * @param array $parameters List of parameters
* @param string|null $group The vcard property group * @param string|null $group The vcard property group
*/ */
public function __construct(Component $root, ?string $name, $value = null, array $parameters = [], ?string $group = null, ?int $lineIndex = null, ?string $lineString = null) public function __construct(public ?Component $root, /**
* Property name.
*
* This will contain a string such as DTSTART, SUMMARY, FN.
*/
public ?string $name, $value = null, array $parameters = [], public ?string $group = null, ?int $lineIndex = null, ?string $lineString = null)
{ {
$this->name = $name;
$this->group = $group;
$this->root = $root;
foreach ($parameters as $k => $v) { foreach ($parameters as $k => $v) {
$this->add($k, $v); $this->add($k, $v);
} }
@ -123,7 +104,7 @@ abstract class Property extends Node
public function getValue() public function getValue()
{ {
if (is_array($this->value)) { if (is_array($this->value)) {
if (0 == count($this->value)) { if (0 === count($this->value)) {
return null; return null;
} elseif (1 === count($this->value)) { } elseif (1 === count($this->value)) {
return $this->value[0]; return $this->value[0];
@ -155,9 +136,9 @@ abstract class Property extends Node
return []; return [];
} elseif (is_array($this->value)) { } elseif (is_array($this->value)) {
return $this->value; return $this->value;
} else {
return [$this->value];
} }
return [$this->value];
} }
/** /**
@ -242,7 +223,7 @@ abstract class Property extends Node
); );
// remove single space after last CRLF // remove single space after last CRLF
return \substr($str, 0, -1); return \substr((string) $str, 0, -1);
} }
/** /**
@ -282,7 +263,7 @@ abstract class Property extends Node
if ('VALUE' === $parameter->name) { if ('VALUE' === $parameter->name) {
continue; continue;
} }
$parameters[strtolower($parameter->name)] = $parameter->jsonSerialize(); $parameters[strtolower((string) $parameter->name)] = $parameter->jsonSerialize();
} }
// In jCard, we need to encode the property-group as a separate 'group' // In jCard, we need to encode the property-group as a separate 'group'
// parameter. // parameter.
@ -292,7 +273,7 @@ abstract class Property extends Node
return array_merge( return array_merge(
[ [
strtolower($this->name), strtolower((string) $this->name),
(object) $parameters, (object) $parameters,
strtolower($this->getValueType()), strtolower($this->getValueType()),
], ],
@ -329,13 +310,13 @@ abstract class Property extends Node
$parameters[] = $parameter; $parameters[] = $parameter;
} }
$writer->startElement(strtolower($this->name)); $writer->startElement(strtolower((string) $this->name));
if (!empty($parameters)) { if ([] !== $parameters) {
$writer->startElement('parameters'); $writer->startElement('parameters');
foreach ($parameters as $parameter) { foreach ($parameters as $parameter) {
$writer->startElement(strtolower($parameter->name)); $writer->startElement(strtolower((string) $parameter->name));
$writer->write($parameter); $writer->write($parameter);
$writer->endElement(); $writer->endElement();
} }
@ -520,7 +501,7 @@ abstract class Property extends Node
} }
// Checking if the property name does not contain any invalid bytes. // Checking if the property name does not contain any invalid bytes.
if (!preg_match('/^([A-Z0-9-]+)$/', $this->name)) { if (!preg_match('/^([A-Z0-9-]+)$/', (string) $this->name)) {
$warnings[] = [ $warnings[] = [
'level' => $options & self::REPAIR ? 1 : 3, 'level' => $options & self::REPAIR ? 1 : 3,
'message' => 'The property name: '.$this->name.' contains invalid characters. Only A-Z, 0-9 and - are allowed', 'message' => 'The property name: '.$this->name.' contains invalid characters. Only A-Z, 0-9 and - are allowed',
@ -572,7 +553,7 @@ abstract class Property extends Node
} }
break; break;
} }
if ($allowedEncoding && !in_array(strtoupper($encoding), $allowedEncoding)) { if ($allowedEncoding && !in_array(strtoupper($encoding), $allowedEncoding, true)) {
$warnings[] = [ $warnings[] = [
'level' => 3, 'level' => 3,
'message' => 'ENCODING='.strtoupper($encoding).' is not valid for this document type.', 'message' => 'ENCODING='.strtoupper($encoding).' is not valid for this document type.',

View file

@ -54,7 +54,7 @@ class Binary extends Property
*/ */
public function setRawMimeDirValue(string $val): void public function setRawMimeDirValue(string $val): void
{ {
$this->value = base64_decode($val); $this->value = base64_decode($val, true);
} }
/** /**
@ -62,7 +62,7 @@ class Binary extends Property
*/ */
public function getRawMimeDirValue(): string public function getRawMimeDirValue(): string
{ {
return base64_encode($this->value); return base64_encode((string) $this->value);
} }
/** /**
@ -83,7 +83,7 @@ class Binary extends Property
*/ */
public function getJsonValue(): array public function getJsonValue(): array
{ {
return [base64_encode($this->getValue())]; return [base64_encode((string) $this->getValue())];
} }
/** /**
@ -93,7 +93,7 @@ class Binary extends Property
*/ */
public function setJsonValue(array $value): void public function setJsonValue(array $value): void
{ {
$value = array_map('base64_decode', $value); $value = array_map(base64_decode(...), $value);
parent::setJsonValue($value); parent::setJsonValue($value);
} }
} }

View file

@ -56,9 +56,7 @@ class Boolean extends Property
public function setXmlValue(array $value): void public function setXmlValue(array $value): void
{ {
$value = array_map( $value = array_map(
function ($value) { fn ($value) => 'true' === $value,
return 'true' === $value;
},
$value $value
); );
parent::setXmlValue($value); parent::setXmlValue($value);

View file

@ -67,7 +67,7 @@ class FloatValue extends Property
*/ */
public function getJsonValue(): array public function getJsonValue(): array
{ {
$val = array_map('floatval', $this->getParts()); $val = array_map(floatval(...), $this->getParts());
// Special-casing the GEO property. // Special-casing the GEO property.
// //
@ -86,7 +86,7 @@ class FloatValue extends Property
*/ */
public function setXmlValue(array $value): void public function setXmlValue(array $value): void
{ {
$value = array_map('floatval', $value); $value = array_map(floatval(...), $value);
parent::setXmlValue($value); parent::setXmlValue($value);
} }
@ -101,7 +101,7 @@ class FloatValue extends Property
// See: // See:
// http://tools.ietf.org/html/rfc6321#section-3.4.1.2 // http://tools.ietf.org/html/rfc6321#section-3.4.1.2
if ('GEO' === $this->name) { if ('GEO' === $this->name) {
$value = array_map('floatval', $this->getParts()); $value = array_map(floatval(...), $this->getParts());
$writer->writeElement('latitude', $value[0]); $writer->writeElement('latitude', $value[0]);
$writer->writeElement('longitude', $value[1]); $writer->writeElement('longitude', $value[1]);

View file

@ -43,10 +43,10 @@ class CalAddress extends Text
public function getNormalizedValue(): string public function getNormalizedValue(): string
{ {
$input = $this->getValue(); $input = $this->getValue();
if (!strpos($input, ':')) { if (!strpos((string) $input, ':')) {
return $input; return $input;
} }
list($schema, $everythingElse) = explode(':', $input, 2); [$schema, $everythingElse] = explode(':', (string) $input, 2);
$schema = strtolower($schema); $schema = strtolower($schema);
if ('mailto' === $schema) { if ('mailto' === $schema) {
$everythingElse = strtolower($everythingElse); $everythingElse = strtolower($everythingElse);

View file

@ -111,7 +111,7 @@ class DateTime extends Property
!$this->hasTime() !$this->hasTime()
|| ( || (
!isset($this['TZID']) !isset($this['TZID'])
&& false === strpos($this->getValue(), 'Z') && !str_contains((string) $this->getValue(), 'Z')
); );
} }
@ -205,7 +205,7 @@ class DateTime extends Property
} }
if (is_null($tz)) { if (is_null($tz)) {
$tz = $d->getTimeZone(); $tz = $d->getTimeZone();
$isUtc = in_array($tz->getName(), ['UTC', 'GMT', 'Z', '+00:00']); $isUtc = in_array($tz->getName(), ['UTC', 'GMT', 'Z', '+00:00'], true);
if (!$isUtc) { if (!$isUtc) {
$this->offsetSet('TZID', $tz->getName()); $this->offsetSet('TZID', $tz->getName());
} }
@ -257,15 +257,15 @@ class DateTime extends Property
$isFloating = $this->isFloating(); $isFloating = $this->isFloating();
$tz = $dts[0]->getTimeZone(); $tz = $dts[0]->getTimeZone();
$isUtc = !$isFloating && in_array($tz->getName(), ['UTC', 'GMT', 'Z']); $isUtc = !$isFloating && in_array($tz->getName(), ['UTC', 'GMT', 'Z'], true);
return array_map( return array_map(
function (\DateTimeInterface $dt) use ($hasTime, $isUtc) { function (\DateTimeInterface $dt) use ($hasTime, $isUtc) {
if ($hasTime) { if ($hasTime) {
return $dt->format('Y-m-d\\TH:i:s').($isUtc ? 'Z' : ''); return $dt->format('Y-m-d\\TH:i:s').($isUtc ? 'Z' : '');
} else {
return $dt->format('Y-m-d');
} }
return $dt->format('Y-m-d');
}, },
$dts $dts
); );
@ -286,9 +286,7 @@ class DateTime extends Property
// those. // those.
$this->setValue( $this->setValue(
array_map( array_map(
function ($item) { fn ($item) => strtr($item, [':' => '', '-' => '']),
return strtr($item, [':' => '', '-' => '']);
},
$value $value
) )
); );
@ -306,7 +304,7 @@ class DateTime extends Property
public function offsetSet($offset, $value): void public function offsetSet($offset, $value): void
{ {
parent::offsetSet($offset, $value); parent::offsetSet($offset, $value);
if ('VALUE' !== strtoupper($offset)) { if ('VALUE' !== strtoupper((string) $offset)) {
return; return;
} }
@ -347,7 +345,7 @@ class DateTime extends Property
DateTimeParser::parseDateTime($value); DateTimeParser::parseDateTime($value);
break; break;
} }
} catch (InvalidDataException $e) { } catch (InvalidDataException) {
$messages[] = [ $messages[] = [
'level' => 3, 'level' => 3,
'message' => 'The supplied value ('.$value.') is not a correct '.$valueType, 'message' => 'The supplied value ('.$value.') is not a correct '.$valueType,

View file

@ -64,14 +64,17 @@ class Period extends Property
public function setJsonValue(array $value): void public function setJsonValue(array $value): void
{ {
$value = array_map( $value = array_map(
function ($item) { fn ($item) => strtr(implode('/', $item), [':' => '', '-' => '']),
return strtr(implode('/', $item), [':' => '', '-' => '']);
},
$value $value
); );
parent::setJsonValue($value); parent::setJsonValue($value);
} }
public function appendUtc(string $strDate)
{
return !str_ends_with($strDate, 'Z') ? '' : 'Z';
}
/** /**
* Returns the value, in the format it should be encoded for json. * Returns the value, in the format it should be encoded for json.
* *
@ -83,21 +86,21 @@ class Period extends Property
{ {
$return = []; $return = [];
foreach ($this->getParts() as $item) { foreach ($this->getParts() as $item) {
list($start, $end) = explode('/', $item, 2); [$start, $end] = explode('/', (string) $item, 2);
$start = DateTimeParser::parseDateTime($start); $startDt = DateTimeParser::parseDateTime($start)->format('Y-m-d\\TH:i:s').$this->appendUtc($start);
// This is a duration value. // This is a duration value.
if ('P' === $end[0]) { if ('P' === $end[0]) {
$return[] = [ $return[] = [
$start->format('Y-m-d\\TH:i:s'), $startDt,
$end, $end,
]; ];
} else { } else {
$end = DateTimeParser::parseDateTime($end); $endDt = DateTimeParser::parseDateTime($end)->format('Y-m-d\\TH:i:s').$this->appendUtc($end);
$return[] = [ $return[] = [
$start->format('Y-m-d\\TH:i:s'), $startDt,
$end->format('Y-m-d\\TH:i:s'), $endDt,
]; ];
} }
} }

View file

@ -29,14 +29,14 @@ class Recur extends Property
/** /**
* Reference to the parent object, if this is not the top object. * Reference to the parent object, if this is not the top object.
*/ */
public ?Node $parent; public ?Node $parent = null;
/** /**
* Updates the current value. * Updates the current value.
* *
* This may be either a single, or multiple strings in an array. * This may be either a single, or multiple strings in an array.
* *
* @param string|array $value * @param string|array|object $value
*/ */
public function setValue($value): void public function setValue($value): void
{ {
@ -52,17 +52,17 @@ class Recur extends Property
$v = strtoupper($v); $v = strtoupper($v);
// The value had multiple sub-values // The value had multiple sub-values
if (false !== strpos($v, ',')) { if (str_contains($v, ',')) {
$v = explode(',', $v); $v = explode(',', $v);
} }
if (0 === strcmp($k, 'until')) { if (0 === strcmp((string) $k, 'until')) {
$v = strtr($v, [':' => '', '-' => '']); $v = strtr($v, [':' => '', '-' => '']);
} }
} elseif (is_array($v)) { } elseif (is_array($v)) {
$v = array_map('strtoupper', $v); $v = array_map(strtoupper(...), $v);
} }
$newVal[strtoupper($k)] = $v; $newVal[strtoupper((string) $k)] = $v;
} }
$this->value = $newVal; $this->value = $newVal;
} elseif (is_string($value)) { } elseif (is_string($value)) {
@ -151,13 +151,13 @@ class Recur extends Property
{ {
$values = []; $values = [];
foreach ($this->getParts() as $k => $v) { foreach ($this->getParts() as $k => $v) {
if (0 === strcmp($k, 'UNTIL')) { if (0 === strcmp((string) $k, 'UNTIL')) {
$date = new DateTime($this->root, null, $v); $date = new DateTime($this->root, null, $v);
$values[strtolower($k)] = $date->getJsonValue()[0]; $values[strtolower((string) $k)] = $date->getJsonValue()[0];
} elseif (0 === strcmp($k, 'COUNT')) { } elseif (0 === strcmp((string) $k, 'COUNT')) {
$values[strtolower($k)] = intval($v); $values[strtolower((string) $k)] = intval($v);
} else { } else {
$values[strtolower($k)] = $v; $values[strtolower((string) $k)] = $v;
} }
} }
@ -186,7 +186,7 @@ class Recur extends Property
$newValue = []; $newValue = [];
foreach (explode(';', $value) as $part) { foreach (explode(';', $value) as $part) {
// Skipping empty parts. // Skipping empty parts.
if (empty($part)) { if ('' === $part) {
continue; continue;
} }
@ -196,10 +196,10 @@ class Recur extends Property
throw new InvalidDataException('The supplied iCalendar RRULE part is incorrect: '.$part); throw new InvalidDataException('The supplied iCalendar RRULE part is incorrect: '.$part);
} }
list($partName, $partValue) = $parts; [$partName, $partValue] = $parts;
// The value itself had multiple values.. // The value itself had multiple values..
if (false !== strpos($partValue, ',')) { if (str_contains($partValue, ',')) {
$partValue = explode(',', $partValue); $partValue = explode(',', $partValue);
} }
$newValue[$partName] = $partValue; $newValue[$partName] = $partValue;
@ -243,7 +243,7 @@ class Recur extends Property
if ($repair) { if ($repair) {
unset($values[$key]); unset($values[$key]);
} }
} elseif ('BYMONTH' == $key) { } elseif ('BYMONTH' === $key) {
$byMonth = (array) $value; $byMonth = (array) $value;
foreach ($byMonth as $i => $v) { foreach ($byMonth as $i => $v) {
if (!is_numeric($v) || (int) $v < 1 || (int) $v > 12) { if (!is_numeric($v) || (int) $v < 1 || (int) $v > 12) {
@ -262,13 +262,13 @@ class Recur extends Property
} }
} }
// if there is no valid entry left, remove the whole value // if there is no valid entry left, remove the whole value
if (is_array($value) && empty($values[$key])) { if (is_array($value) && ([] === $values[$key])) {
unset($values[$key]); unset($values[$key]);
} }
} elseif ('BYWEEKNO' == $key) { } elseif ('BYWEEKNO' === $key) {
$byWeekNo = (array) $value; $byWeekNo = (array) $value;
foreach ($byWeekNo as $i => $v) { foreach ($byWeekNo as $i => $v) {
if (!is_numeric($v) || (int) $v < -53 || 0 == (int) $v || (int) $v > 53) { if (!is_numeric($v) || (int) $v < -53 || 0 === (int) $v || (int) $v > 53) {
$warnings[] = [ $warnings[] = [
'level' => $repair ? 1 : 3, 'level' => $repair ? 1 : 3,
'message' => 'BYWEEKNO in RRULE must have value(s) from -53 to -1, or 1 to 53!', 'message' => 'BYWEEKNO in RRULE must have value(s) from -53 to -1, or 1 to 53!',
@ -284,13 +284,13 @@ class Recur extends Property
} }
} }
// if there is no valid entry left, remove the whole value // if there is no valid entry left, remove the whole value
if (is_array($value) && empty($values[$key])) { if (is_array($value) && ([] === $values[$key])) {
unset($values[$key]); unset($values[$key]);
} }
} elseif ('BYYEARDAY' == $key) { } elseif ('BYYEARDAY' === $key) {
$byYearDay = (array) $value; $byYearDay = (array) $value;
foreach ($byYearDay as $i => $v) { foreach ($byYearDay as $i => $v) {
if (!is_numeric($v) || (int) $v < -366 || 0 == (int) $v || (int) $v > 366) { if (!is_numeric($v) || (int) $v < -366 || 0 === (int) $v || (int) $v > 366) {
$warnings[] = [ $warnings[] = [
'level' => $repair ? 1 : 3, 'level' => $repair ? 1 : 3,
'message' => 'BYYEARDAY in RRULE must have value(s) from -366 to -1, or 1 to 366!', 'message' => 'BYYEARDAY in RRULE must have value(s) from -366 to -1, or 1 to 366!',
@ -306,7 +306,7 @@ class Recur extends Property
} }
} }
// if there is no valid entry left, remove the whole value // if there is no valid entry left, remove the whole value
if (is_array($value) && empty($values[$key])) { if (is_array($value) && ([] === $values[$key])) {
unset($values[$key]); unset($values[$key]);
} }
} }

View file

@ -62,7 +62,7 @@ class IntegerValue extends Property
*/ */
public function setXmlValue(array $value): void public function setXmlValue(array $value): void
{ {
$value = array_map('intval', $value); $value = array_map(intval(...), $value);
parent::setXmlValue($value); parent::setXmlValue($value);
} }
} }

View file

@ -71,7 +71,7 @@ class Text extends Property
// 2. structured value properties // 2. structured value properties
// //
// The former is always separated by a comma, the latter by semicolon. // The former is always separated by a comma, the latter by semicolon.
if (in_array($name, $this->structuredValues)) { if (in_array($name, $this->structuredValues, true)) {
$this->delimiter = ';'; $this->delimiter = ';';
} }
@ -155,7 +155,7 @@ class Text extends Property
// Structured text values should always be returned as a single // Structured text values should always be returned as a single
// array-item. Multi-value text should be returned as multiple items in // array-item. Multi-value text should be returned as multiple items in
// the top-array. // the top-array.
if (in_array($this->name, $this->structuredValues)) { if (in_array($this->name, $this->structuredValues, true)) {
return [$this->getParts()]; return [$this->getParts()];
} }
@ -213,7 +213,7 @@ class Text extends Property
// If the resulting value contains a \n, we must encode it as // If the resulting value contains a \n, we must encode it as
// quoted-printable. // quoted-printable.
if (false !== \strpos($val, "\n")) { if (str_contains((string) $val, "\n")) {
$str .= ';ENCODING=QUOTED-PRINTABLE:'; $str .= ';ENCODING=QUOTED-PRINTABLE:';
$lastLine = $str; $lastLine = $str;
$out = ''; $out = '';
@ -222,13 +222,13 @@ class Text extends Property
// encode newlines for us. Specifically, the \r\n sequence must in // encode newlines for us. Specifically, the \r\n sequence must in
// vcards be encoded as =0D=OA and we must insert soft-newlines // vcards be encoded as =0D=OA and we must insert soft-newlines
// every 75 bytes. // every 75 bytes.
for ($ii = 0; $ii < \strlen($val); ++$ii) { for ($ii = 0; $ii < \strlen((string) $val); ++$ii) {
$ord = \ord($val[$ii]); $ord = \ord($val[$ii]);
// These characters are encoded as themselves. // These characters are encoded as themselves.
if ($ord >= 32 && $ord <= 126) { if ($ord >= 32 && $ord <= 126) {
$lastLine .= $val[$ii]; $lastLine .= $val[$ii];
} else { } else {
$lastLine .= '='.\strtoupper(\bin2hex($val[$ii])); $lastLine .= '='.\strtoupper(\bin2hex((string) $val[$ii]));
} }
if (\strlen($lastLine) >= 75) { if (\strlen($lastLine) >= 75) {
// Soft line break // Soft line break
@ -241,22 +241,21 @@ class Text extends Property
} }
return $out; return $out;
} else { }
$str .= ':'.$val; $str .= ':'.$val;
$str = \preg_replace( $str = \preg_replace(
'/( '/(
(?:^.)? # 1 additional byte in first line because of missing single space (see next line) (?:^.)? # 1 additional byte in first line because of missing single space (see next line)
.{1,74} # max 75 bytes per line (1 byte is used for a single space added after every CRLF) .{1,74} # max 75 bytes per line (1 byte is used for a single space added after every CRLF)
(?![\x80-\xbf]) # prevent splitting multibyte characters (?![\x80-\xbf]) # prevent splitting multibyte characters
)/x', )/x',
"$1\r\n ", "$1\r\n ",
$str $str
); );
// remove single space after last CRLF // remove single space after last CRLF
return \substr($str, 0, -1); return \substr((string) $str, 0, -1);
}
} }
/** /**
@ -271,7 +270,7 @@ class Text extends Property
foreach ($items as $i => $item) { foreach ($items as $i => $item) {
$writer->writeElement( $writer->writeElement(
$item, $item,
!empty($values[$i]) ? $values[$i] : null ('' !== $values[$i] && [] !== $values[$i]) ? $values[$i] : null
); );
} }
}; };

View file

@ -104,7 +104,7 @@ class Time extends Text
$timeStr .= 'Z'; $timeStr .= 'Z';
} else { } else {
$timeStr .= $timeStr .=
preg_replace('/([0-9]{2})([0-9]{2})$/', '$1:$2', $parts['timezone']); preg_replace('/([0-9]{2})([0-9]{2})$/', '$1:$2', (string) $parts['timezone']);
} }
} }
@ -118,9 +118,7 @@ class Time extends Text
public function setXmlValue(array $value): void public function setXmlValue(array $value): void
{ {
$value = array_map( $value = array_map(
function ($value) { fn ($value) => str_replace(':', '', $value),
return str_replace(':', '', $value);
},
$value $value
); );
parent::setXmlValue($value); parent::setXmlValue($value);

View file

@ -38,7 +38,7 @@ class Uri extends Text
public function parameters(): array public function parameters(): array
{ {
$parameters = parent::parameters(); $parameters = parent::parameters();
if (!isset($parameters['VALUE']) && in_array($this->name, ['URL', 'PHOTO'])) { if (!isset($parameters['VALUE']) && in_array($this->name, ['URL', 'PHOTO'], true)) {
// If we are encoding a URI value, and this URI value has no // If we are encoding a URI value, and this URI value has no
// VALUE=URI parameter, we add it anyway. // VALUE=URI parameter, we add it anyway.
// //
@ -74,14 +74,10 @@ class Uri extends Text
$matches = preg_split($regex, $val, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY); $matches = preg_split($regex, $val, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY);
$newVal = ''; $newVal = '';
foreach ($matches as $match) { foreach ($matches as $match) {
switch ($match) { match ($match) {
case '\:': '\:' => $newVal .= ':',
$newVal .= ':'; default => $newVal .= $match,
break; };
default:
$newVal .= $match;
break;
}
} }
$this->value = $newVal; $this->value = $newVal;
} else { } else {

View file

@ -42,9 +42,7 @@ class UtcOffset extends Text
public function setJsonValue(array $value): void public function setJsonValue(array $value): void
{ {
$value = array_map( $value = array_map(
function ($value) { fn ($value) => str_replace(':', '', $value),
return str_replace(':', '', $value);
},
$value $value
); );
parent::setJsonValue($value); parent::setJsonValue($value);
@ -58,10 +56,8 @@ class UtcOffset extends Text
public function getJsonValue(): array public function getJsonValue(): array
{ {
return array_map( return array_map(
function ($value) { fn ($value) => substr((string) $value, 0, -2).':'.
return substr($value, 0, -2).':'. substr((string) $value, -2),
substr($value, -2);
},
parent::getJsonValue() parent::getJsonValue()
); );
} }

View file

@ -77,7 +77,7 @@ class DateAndOrTime extends Property
public function setDateTime(\DateTimeInterface $dt): void public function setDateTime(\DateTimeInterface $dt): void
{ {
$tz = $dt->getTimeZone(); $tz = $dt->getTimeZone();
$isUtc = in_array($tz->getName(), ['UTC', 'GMT', 'Z']); $isUtc = in_array($tz->getName(), ['UTC', 'GMT', 'Z'], true);
if ($isUtc) { if ($isUtc) {
$value = $dt->format('Ymd\\THis\\Z'); $value = $dt->format('Ymd\\THis\\Z');
@ -232,14 +232,10 @@ class DateAndOrTime extends Property
$value = ''; $value = '';
// $d = defined // $d = defined
$d = function ($part) use ($parts): bool { $d = (fn ($part): bool => !is_null($parts[$part]));
return !is_null($parts[$part]);
};
// $r = read // $r = read
$r = function ($part) use ($parts) { $r = (fn ($part) => $parts[$part]);
return $parts[$part];
};
// From the Relax NG Schema. // From the Relax NG Schema.
// //
@ -341,7 +337,7 @@ class DateAndOrTime extends Property
try { try {
DateTimeParser::parseVCardDateTime($value); DateTimeParser::parseVCardDateTime($value);
} catch (InvalidDataException $e) { } catch (InvalidDataException) {
$messages[] = [ $messages[] = [
'level' => 3, 'level' => 3,
'message' => 'The supplied value ('.$value.') is not a correct DATE-AND-OR-TIME property', 'message' => 'The supplied value ('.$value.') is not a correct DATE-AND-OR-TIME property',

View file

@ -91,9 +91,7 @@ class EventIterator implements \Iterator
*/ */
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');
$timeZone = new \DateTimeZone('UTC');
}
$this->timeZone = $timeZone; $this->timeZone = $timeZone;
if (is_array($input)) { if (is_array($input)) {
@ -164,8 +162,12 @@ class EventIterator implements \Iterator
} }
if (isset($this->masterEvent->RDATE)) { if (isset($this->masterEvent->RDATE)) {
$rdateValues = [];
foreach ($this->masterEvent->RDATE as $rdate) {
$rdateValues = array_merge($rdateValues, $rdate->getParts());
}
$this->recurIterator = new RDateIterator( $this->recurIterator = new RDateIterator(
$this->masterEvent->RDATE->getParts(), $rdateValues,
$this->startDate $this->startDate
); );
} elseif (isset($this->masterEvent->RRULE)) { } elseif (isset($this->masterEvent->RRULE)) {
@ -228,11 +230,10 @@ class EventIterator implements \Iterator
} }
if ($this->currentOverriddenEvent && $this->currentOverriddenEvent->DTEND) { if ($this->currentOverriddenEvent && $this->currentOverriddenEvent->DTEND) {
return $this->currentOverriddenEvent->DTEND->getDateTime($this->timeZone); return $this->currentOverriddenEvent->DTEND->getDateTime($this->timeZone);
} else {
$end = clone $this->currentDate;
return $end->modify('+'.$this->eventDuration.' seconds');
} }
$end = clone $this->currentDate;
return $end->modify('+'.$this->eventDuration.' seconds');
} }
/** /**

View file

@ -26,9 +26,13 @@ class RDateIterator implements \Iterator
* *
* @param string|array $rrule * @param string|array $rrule
*/ */
public function __construct($rrule, \DateTimeInterface $start) public function __construct($rrule, /**
* The reference start date/time for the rrule.
*
* All calculations are based on this initial date.
*/
protected \DateTimeInterface $startDate)
{ {
$this->startDate = $start;
$this->parseRDate($rrule); $this->parseRDate($rrule);
$this->currentDate = clone $this->startDate; $this->currentDate = clone $this->startDate;
} }
@ -117,13 +121,6 @@ class RDateIterator implements \Iterator
} }
} }
/**
* The reference start date/time for the rrule.
*
* All calculations are based on this initial date.
*/
protected \DateTimeInterface $startDate;
/** /**
* The date of the current iteration. You can get this by calling * The date of the current iteration. You can get this by calling
* ->current(). * ->current().

View file

@ -37,9 +37,13 @@ class RRuleIterator implements \Iterator
* *
* @throws InvalidDataException * @throws InvalidDataException
*/ */
public function __construct($rrule, \DateTimeInterface $start) public function __construct($rrule, /**
* The reference start date/time for the rrule.
*
* All calculations are based on this initial date.
*/
protected \DateTimeInterface $startDate)
{ {
$this->startDate = $start;
$this->parseRRule($rrule); $this->parseRRule($rrule);
$this->currentDate = clone $this->startDate; $this->currentDate = clone $this->startDate;
} }
@ -146,13 +150,6 @@ class RRuleIterator implements \Iterator
} }
} }
/**
* The reference start date/time for the rrule.
*
* All calculations are based on this initial date.
*/
protected \DateTimeInterface $startDate;
/** /**
* The date of the current iteration. You can get this by calling * The date of the current iteration. You can get this by calling
* ->current(). * ->current().
@ -357,23 +354,23 @@ class RRuleIterator implements \Iterator
} }
$recurrenceHours = []; $recurrenceHours = [];
if (!empty($this->byHour)) { if ($this->byHour) {
$recurrenceHours = $this->getHours(); $recurrenceHours = $this->getHours();
} }
$recurrenceDays = []; $recurrenceDays = [];
if (!empty($this->byDay)) { if ($this->byDay) {
$recurrenceDays = $this->getDays(); $recurrenceDays = $this->getDays();
} }
$recurrenceMonths = []; $recurrenceMonths = [];
if (!empty($this->byMonth)) { if ($this->byMonth) {
$recurrenceMonths = $this->getMonths(); $recurrenceMonths = $this->getMonths();
} }
do { do {
if ($this->byHour) { if ($this->byHour) {
if ('23' == $this->currentDate->format('G')) { if ('23' === $this->currentDate->format('G')) {
// to obey the interval rule // to obey the interval rule
$this->currentDate = $this->currentDate->modify('+'.($this->interval - 1).' days'); $this->currentDate = $this->currentDate->modify('+'.($this->interval - 1).' days');
} }
@ -398,9 +395,9 @@ class RRuleIterator implements \Iterator
return; return;
} }
} while ( } while (
($this->byDay && !in_array($currentDay, $recurrenceDays)) ($this->byDay && !in_array((int) $currentDay, $recurrenceDays, true))
|| ($this->byHour && !in_array($currentHour, $recurrenceHours)) || ($this->byHour && !in_array($currentHour, $recurrenceHours, true))
|| ($this->byMonth && !in_array($currentMonth, $recurrenceMonths)) || ($this->byMonth && !in_array($currentMonth, $recurrenceMonths, true))
); );
} }
@ -442,18 +439,18 @@ class RRuleIterator implements \Iterator
$currentHour = (int) $this->currentDate->format('G'); $currentHour = (int) $this->currentDate->format('G');
// We need to roll over to the next week // We need to roll over to the next week
if ($currentDay === $firstDay && (!$this->byHour || '0' == $currentHour)) { if ($currentDay === $firstDay && (!$this->byHour || 0 === $currentHour)) {
$this->currentDate = $this->currentDate->modify('+'.($this->interval - 1).' weeks'); $this->currentDate = $this->currentDate->modify('+'.($this->interval - 1).' weeks');
// We need to go to the first day of this week, but only if we // We need to go to the first day of this week, but only if we
// are not already on this first day of this week. // are not already on this first day of this week.
if ($this->currentDate->format('w') != $firstDay) { if ((int) $this->currentDate->format('w') !== $firstDay) {
$this->currentDate = $this->currentDate->modify('last '.$this->dayNames[$this->dayMap[$this->weekStart]]); $this->currentDate = $this->currentDate->modify('last '.$this->dayNames[$this->dayMap[$this->weekStart]]);
} }
} }
// We have a match // We have a match
} while (($this->byDay && !in_array($currentDay, $recurrenceDays)) || ($this->byHour && !in_array($currentHour, $recurrenceHours))); } while (($this->byDay && !in_array($currentDay, $recurrenceDays, true)) || ($this->byHour && !in_array((string) $currentHour, $recurrenceHours, true)));
} }
/** /**
@ -476,14 +473,14 @@ class RRuleIterator implements \Iterator
++$increase; ++$increase;
$tempDate = clone $this->currentDate; $tempDate = clone $this->currentDate;
$tempDate = $tempDate->modify('+ '.($this->interval * $increase).' months '.$this->startTime()); $tempDate = $tempDate->modify('+ '.($this->interval * $increase).' months '.$this->startTime());
} while ($tempDate->format('j') != $currentDayOfMonth); } while ($tempDate->format('j') !== $currentDayOfMonth);
$this->currentDate = $tempDate; $this->currentDate = $tempDate;
} }
return; return;
} }
$occurrence = -1; $selectedOccurrence = -1;
while (true) { while (true) {
$occurrences = $this->getMonthlyOccurrences(); $occurrences = $this->getMonthlyOccurrences();
@ -491,6 +488,7 @@ class RRuleIterator implements \Iterator
// The first occurrence that's higher than the current // The first occurrence that's higher than the current
// day of the month wins. // day of the month wins.
if ($occurrence > $currentDayOfMonth) { if ($occurrence > $currentDayOfMonth) {
$selectedOccurrence = $occurrence;
break 2; break 2;
} }
} }
@ -533,7 +531,7 @@ class RRuleIterator implements \Iterator
$this->currentDate = $this->currentDate->setDate( $this->currentDate = $this->currentDate->setDate(
(int) $this->currentDate->format('Y'), (int) $this->currentDate->format('Y'),
(int) $this->currentDate->format('n'), (int) $this->currentDate->format('n'),
(int) $occurrence (int) $selectedOccurrence
)->modify($this->startTime()); )->modify($this->startTime());
} }
@ -547,9 +545,9 @@ class RRuleIterator implements \Iterator
$currentDayOfMonth = $this->currentDate->format('j'); $currentDayOfMonth = $this->currentDate->format('j');
// No sub-rules, so we just advance by year // No sub-rules, so we just advance by year
if (empty($this->byMonth)) { if (!$this->byMonth) {
// Unless it was a leap day! // Unless it was a leap day!
if (2 == $currentMonth && 29 == $currentDayOfMonth) { if (2 === (int) $currentMonth && 29 === (int) $currentDayOfMonth) {
$counter = 0; $counter = 0;
do { do {
++$counter; ++$counter;
@ -563,7 +561,7 @@ class RRuleIterator implements \Iterator
// functions instead. // functions instead.
$nextDate = clone $this->currentDate; $nextDate = clone $this->currentDate;
$nextDate = $nextDate->modify('+ '.($this->interval * $counter).' years'); $nextDate = $nextDate->modify('+ '.($this->interval * $counter).' years');
} while (2 != $nextDate->format('n')); } while (2 !== (int) $nextDate->format('n'));
$this->currentDate = $nextDate; $this->currentDate = $nextDate;
@ -634,7 +632,7 @@ class RRuleIterator implements \Iterator
$date = $date->sub(new \DateInterval('P'.abs($byYearDay + 1).'D')); $date = $date->sub(new \DateInterval('P'.abs($byYearDay + 1).'D'));
} }
if ($date > $this->currentDate && in_array($date->format('N'), $dayOffsets)) { if ($date > $this->currentDate && in_array((int) $date->format('N'), $dayOffsets, true)) {
$checkDates[] = $date; $checkDates[] = $date;
} }
} }
@ -665,7 +663,7 @@ class RRuleIterator implements \Iterator
// If we got a byDay or getMonthDay filter, we must first expand // If we got a byDay or getMonthDay filter, we must first expand
// further. // further.
if ($this->byDay || $this->byMonthDay) { if ($this->byDay || $this->byMonthDay) {
$occurrence = -1; $selectedOccurrence = -1;
while (true) { while (true) {
$occurrences = $this->getMonthlyOccurrences(); $occurrences = $this->getMonthlyOccurrences();
@ -677,7 +675,8 @@ class RRuleIterator implements \Iterator
if ($occurrence > $currentDayOfMonth || $advancedToNewMonth) { if ($occurrence > $currentDayOfMonth || $advancedToNewMonth) {
// only consider byMonth matches, // only consider byMonth matches,
// otherwise, we don't follow RRule correctly // otherwise, we don't follow RRule correctly
if (in_array($currentMonth, $this->byMonth)) { if (in_array((string) $currentMonth, $this->byMonth, true)) {
$selectedOccurrence = $occurrence;
break 2; break 2;
} }
} }
@ -693,12 +692,12 @@ class RRuleIterator implements \Iterator
$currentYear += $this->interval; $currentYear += $this->interval;
$currentMonth = 1; $currentMonth = 1;
} }
} while (!in_array($currentMonth, $this->byMonth)); } while (!in_array((string) $currentMonth, $this->byMonth, true));
$this->currentDate = $this->currentDate->setDate( $this->currentDate = $this->currentDate->setDate(
(int) $currentYear, (int) $currentYear,
(int) $currentMonth, $currentMonth,
(int) $currentDayOfMonth $currentDayOfMonth
); );
// To prevent running this forever (better: until we hit the max date of DateTimeImmutable) we simply // To prevent running this forever (better: until we hit the max date of DateTimeImmutable) we simply
@ -714,28 +713,25 @@ class RRuleIterator implements \Iterator
$this->currentDate = $this->currentDate->setDate( $this->currentDate = $this->currentDate->setDate(
(int) $currentYear, (int) $currentYear,
(int) $currentMonth, (int) $currentMonth,
(int) $occurrence (int) $selectedOccurrence
)->modify($this->startTime());
return;
} else {
// These are the 'byMonth' rules, if there are no byDay or
// byMonthDay sub-rules.
do {
++$currentMonth;
if ($currentMonth > 12) {
$currentYear += $this->interval;
$currentMonth = 1;
}
} while (!in_array($currentMonth, $this->byMonth));
$this->currentDate = $this->currentDate->setDate(
(int) $currentYear,
(int) $currentMonth,
(int) $currentDayOfMonth
)->modify($this->startTime()); )->modify($this->startTime());
return; return;
} }
// These are the 'byMonth' rules, if there are no byDay or
// byMonthDay sub-rules.
do {
++$currentMonth;
if ($currentMonth > 12) {
$currentYear += $this->interval;
$currentMonth = 1;
}
} while (!in_array((string) $currentMonth, $this->byMonth, true));
$this->currentDate = $this->currentDate->setDate(
(int) $currentYear,
$currentMonth,
(int) $currentDayOfMonth
)->modify($this->startTime());
} }
/* }}} */ /* }}} */
@ -755,13 +751,14 @@ class RRuleIterator implements \Iterator
} }
foreach ($rrule as $key => $value) { foreach ($rrule as $key => $value) {
$key = strtoupper($key); $key = strtoupper((string) $key);
switch ($key) { switch ($key) {
case 'FREQ': case 'FREQ':
$value = strtolower($value); $value = strtolower((string) $value);
if (!in_array( if (!in_array(
$value, $value,
['secondly', 'minutely', 'hourly', 'daily', 'weekly', 'monthly', 'yearly'] ['secondly', 'minutely', 'hourly', 'daily', 'weekly', 'monthly', 'yearly'],
true
)) { )) {
throw new InvalidDataException('Unknown value for FREQ='.strtoupper($value)); throw new InvalidDataException('Unknown value for FREQ='.strtoupper($value));
} }
@ -809,7 +806,7 @@ class RRuleIterator implements \Iterator
case 'BYDAY': case 'BYDAY':
$value = (array) $value; $value = (array) $value;
foreach ($value as $part) { foreach ($value as $part) {
if (!preg_match('#^ (-|\+)? ([1-5])? (MO|TU|WE|TH|FR|SA|SU) $# xi', $part)) { if (!preg_match('#^ (-|\+)? ([1-5])? (MO|TU|WE|TH|FR|SA|SU) $#xi', (string) $part)) {
throw new InvalidDataException('Invalid part in BYDAY clause: '.$part); throw new InvalidDataException('Invalid part in BYDAY clause: '.$part);
} }
} }
@ -823,7 +820,7 @@ class RRuleIterator implements \Iterator
case 'BYYEARDAY': case 'BYYEARDAY':
$this->byYearDay = (array) $value; $this->byYearDay = (array) $value;
foreach ($this->byYearDay as $byYearDay) { foreach ($this->byYearDay as $byYearDay) {
if (!is_numeric($byYearDay) || (int) $byYearDay < -366 || 0 == (int) $byYearDay || (int) $byYearDay > 366) { if (!is_numeric($byYearDay) || (int) $byYearDay < -366 || 0 === (int) $byYearDay || (int) $byYearDay > 366) {
throw new InvalidDataException('BYYEARDAY in RRULE must have value(s) from 1 to 366, or -366 to -1!'); throw new InvalidDataException('BYYEARDAY in RRULE must have value(s) from 1 to 366, or -366 to -1!');
} }
} }
@ -832,7 +829,7 @@ class RRuleIterator implements \Iterator
case 'BYWEEKNO': case 'BYWEEKNO':
$this->byWeekNo = (array) $value; $this->byWeekNo = (array) $value;
foreach ($this->byWeekNo as $byWeekNo) { foreach ($this->byWeekNo as $byWeekNo) {
if (!is_numeric($byWeekNo) || (int) $byWeekNo < -53 || 0 == (int) $byWeekNo || (int) $byWeekNo > 53) { if (!is_numeric($byWeekNo) || (int) $byWeekNo < -53 || 0 === (int) $byWeekNo || (int) $byWeekNo > 53) {
throw new InvalidDataException('BYWEEKNO in RRULE must have value(s) from 1 to 53, or -53 to -1!'); throw new InvalidDataException('BYWEEKNO in RRULE must have value(s) from 1 to 53, or -53 to -1!');
} }
} }
@ -852,7 +849,7 @@ class RRuleIterator implements \Iterator
break; break;
case 'WKST': case 'WKST':
$this->weekStart = strtoupper($value); $this->weekStart = strtoupper((string) $value);
break; break;
default: default:
@ -892,7 +889,7 @@ class RRuleIterator implements \Iterator
// that point and add it to the results. // that point and add it to the results.
if ($this->byDay) { if ($this->byDay) {
foreach ($this->byDay as $day) { foreach ($this->byDay as $day) {
$dayName = $this->dayNames[$this->dayMap[substr($day, -2)]]; $dayName = $this->dayNames[$this->dayMap[substr((string) $day, -2)]];
// Dayname will be something like 'wednesday'. Now we need to find // Dayname will be something like 'wednesday'. Now we need to find
// all wednesdays in this month. // all wednesdays in this month.
@ -913,8 +910,8 @@ class RRuleIterator implements \Iterator
// So now we have 'all wednesdays' for month. It is however // So now we have 'all wednesdays' for month. It is however
// possible that the user only really wanted the 1st, 2nd or last // possible that the user only really wanted the 1st, 2nd or last
// wednesday. // wednesday.
if (strlen($day) > 2) { if (strlen((string) $day) > 2) {
$offset = (int) substr($day, 0, -2); $offset = (int) substr((string) $day, 0, -2);
if ($offset > 0) { if ($offset > 0) {
// It is possible that the day does not exist, such as a // It is possible that the day does not exist, such as a
@ -949,7 +946,7 @@ class RRuleIterator implements \Iterator
$byMonthDayResults[] = $monthDay; $byMonthDayResults[] = $monthDay;
} else { } else {
// Negative values // Negative values
$byMonthDayResults[] = $startDate->format('t') + 1 + $monthDay; $byMonthDayResults[] = (int) $startDate->format('t') + 1 + $monthDay;
} }
} }
} }
@ -1018,7 +1015,7 @@ class RRuleIterator implements \Iterator
// The day may be preceded with a positive (+n) or // The day may be preceded with a positive (+n) or
// negative (-n) integer. However, this does not make // negative (-n) integer. However, this does not make
// sense in 'weekly' so we ignore it here. // sense in 'weekly' so we ignore it here.
$recurrenceDays[] = $this->dayMap[substr($byDay, -2)]; $recurrenceDays[] = $this->dayMap[substr((string) $byDay, -2)];
} }
return $recurrenceDays; return $recurrenceDays;

View file

@ -96,8 +96,8 @@ class ICalendar implements SplitterInterface
} }
return $object; return $object;
} else {
return null;
} }
return null;
} }
} }

View file

@ -22,13 +22,6 @@ use Sabre\VObject\Parser\MimeDir;
*/ */
class VCard implements SplitterInterface class VCard implements SplitterInterface
{ {
/**
* File handle.
*
* @var resource
*/
protected $input;
/** /**
* Persistent parser. * Persistent parser.
*/ */
@ -42,10 +35,12 @@ class VCard implements SplitterInterface
* @param resource $input * @param resource $input
* @param int $options parser options, see the OPTIONS constants * @param int $options parser options, see the OPTIONS constants
*/ */
public function __construct($input, int $options = 0) public function __construct(/**
* File handle.
*/
protected $input, int $options = 0)
{ {
$this->input = $input; $this->parser = new MimeDir($this->input, $options);
$this->parser = new MimeDir($input, $options);
} }
/** /**
@ -64,7 +59,7 @@ class VCard implements SplitterInterface
if (!$object instanceof Component\VCard) { if (!$object instanceof Component\VCard) {
throw new VObject\ParseException('The supplied input contained non-VCARD data.'); throw new VObject\ParseException('The supplied input contained non-VCARD data.');
} }
} catch (VObject\EofException $e) { } catch (VObject\EofException) {
return null; return null;
} }

View file

@ -41,9 +41,7 @@ class TimeZoneUtil
private static function getInstance(): self private static function getInstance(): self
{ {
if (null === self::$instance) { self::$instance ??= new self();
self::$instance = new self();
}
return self::$instance; return self::$instance;
} }

View file

@ -24,6 +24,24 @@ class FindFromOffset implements TimezoneFinder
// @codeCoverageIgnoreEnd // @codeCoverageIgnoreEnd
} }
if (0 === strncasecmp($tzid, 'UTC', 3)) {
$offset = substr($tzid, 3);
if (6 === strlen($offset) && ':' === $offset[3]) {
$offset = substr($offset, 0, 3).substr($offset, 4);
}
if (5 !== strlen($offset)
|| ('+' !== $offset[0] && '-' !== $offset[0])
|| 4 !== strspn(substr($offset, 1), '0123456789')
|| (int) substr($offset, 1, 2) > 23
|| (int) substr($offset, 3, 2) > 59
) {
return null;
}
return new \DateTimeZone($offset);
}
return null; return null;
} }
} }

View file

@ -40,13 +40,13 @@ class FindFromTimezoneIdentifier implements TimezoneFinder
try { try {
if ( if (
in_array($tzid, $tzIdentifiers) in_array($tzid, $tzIdentifiers, true)
|| preg_match('/^GMT(\+|-)([0-9]{4})$/', $tzid, $matches) || preg_match('/^GMT(\+|-)([0-9]{4})$/', $tzid, $matches)
|| in_array($tzid, $this->getIdentifiersBC()) || in_array($tzid, $this->getIdentifiersBC(), true)
) { ) {
return new \DateTimeZone($tzid); return new \DateTimeZone($tzid);
} }
} catch (\Exception $e) { } catch (\Exception) {
} }
return null; return null;

View file

@ -18,11 +18,18 @@ class FindFromTimezoneMap implements TimezoneFinder
'/^\((UTC|GMT)(\+|\-)[\d]{2}\.[\d]{2}\) (.*)/', '/^\((UTC|GMT)(\+|\-)[\d]{2}\.[\d]{2}\) (.*)/',
]; ];
/**
* @throws void
*/
public function find(string $tzid, ?bool $failIfUncertain = false): ?\DateTimeZone public function find(string $tzid, ?bool $failIfUncertain = false): ?\DateTimeZone
{ {
// Next, we check if the tzid is somewhere in our tzid map. // Next, we check if the tzid is somewhere in our tzid map.
if ($this->hasTzInMap($tzid)) { if ($this->hasTzInMap($tzid)) {
return new \DateTimeZone($this->getTzFromMap($tzid)); try {
return new \DateTimeZone($this->getTzFromMap($tzid));
} catch (\Exception) {
return null;
}
} }
// Some Microsoft products prefix the offset first, so let's strip that off // Some Microsoft products prefix the offset first, so let's strip that off
@ -34,7 +41,11 @@ class FindFromTimezoneMap implements TimezoneFinder
} }
$tzidAlternate = $matches[3]; $tzidAlternate = $matches[3];
if ($this->hasTzInMap($tzidAlternate)) { if ($this->hasTzInMap($tzidAlternate)) {
return new \DateTimeZone($this->getTzFromMap($tzidAlternate)); try {
return new \DateTimeZone($this->getTzFromMap($tzidAlternate));
} catch (\Exception) {
return null;
}
} }
} }

View file

@ -23,7 +23,7 @@ class GuessFromLicEntry implements TimezoneGuesser
// Libical generators may specify strings like // Libical generators may specify strings like
// "SystemV/EST5EDT". For those we must remove the // "SystemV/EST5EDT". For those we must remove the
// SystemV part. // SystemV part.
if ('SystemV/' === substr($lic, 0, 8)) { if (str_starts_with($lic, 'SystemV/')) {
$lic = substr($lic, 8); $lic = substr($lic, 8);
} }

View file

@ -105,7 +105,7 @@ class GuessFromMsTzId implements TimezoneGuesser
$cdoId = (int) $vtimezone->{'X-MICROSOFT-CDO-TZID'}->getValue(); $cdoId = (int) $vtimezone->{'X-MICROSOFT-CDO-TZID'}->getValue();
// 2 can mean both Europe/Lisbon and Europe/Sarajevo. // 2 can mean both Europe/Lisbon and Europe/Sarajevo.
if (2 === $cdoId && false !== strpos((string) $vtimezone->TZID, 'Sarajevo')) { if (2 === $cdoId && str_contains((string) $vtimezone->TZID, 'Sarajevo')) {
return new \DateTimeZone('Europe/Sarajevo'); return new \DateTimeZone('Europe/Sarajevo');
} }

View file

@ -38,10 +38,10 @@ class VCardConverter
return clone $input; return clone $input;
} }
if (!in_array($inputVersion, [Document::VCARD21, Document::VCARD30, Document::VCARD40])) { if (!in_array($inputVersion, [Document::VCARD21, Document::VCARD30, Document::VCARD40], true)) {
throw new \InvalidArgumentException('Only vCard 2.1, 3.0 and 4.0 are supported for the input data'); throw new \InvalidArgumentException('Only vCard 2.1, 3.0 and 4.0 are supported for the input data');
} }
if (!in_array($targetVersion, [Document::VCARD30, Document::VCARD40])) { if (!in_array($targetVersion, [Document::VCARD30, Document::VCARD40], true)) {
throw new \InvalidArgumentException('You can only use vCard 3.0 or 4.0 for the target version'); throw new \InvalidArgumentException('You can only use vCard 3.0 or 4.0 for the target version');
} }
@ -69,7 +69,7 @@ class VCardConverter
protected function convertProperty(Component\VCard $input, Component\VCard $output, Property $property, int $targetVersion): void protected function convertProperty(Component\VCard $input, Component\VCard $output, Property $property, int $targetVersion): void
{ {
// Skipping these, those are automatically added. // Skipping these, those are automatically added.
if (in_array($property->name, ['VERSION', 'PRODID'])) { if (in_array($property->name, ['VERSION', 'PRODID'], true)) {
return; return;
} }
@ -93,7 +93,7 @@ class VCardConverter
); );
if (Document::VCARD30 === $targetVersion) { if (Document::VCARD30 === $targetVersion) {
if ($property instanceof Uri && in_array($property->name, ['PHOTO', 'LOGO', 'SOUND'])) { if ($property instanceof Uri && in_array($property->name, ['PHOTO', 'LOGO', 'SOUND'], true)) {
/** @var Uri $newProperty */ /** @var Uri $newProperty */
$newProperty = $this->convertUriToBinary($output, $newProperty); $newProperty = $this->convertUriToBinary($output, $newProperty);
} elseif ($property instanceof Property\VCard\DateAndOrTime) { } elseif ($property instanceof Property\VCard\DateAndOrTime) {
@ -111,7 +111,7 @@ class VCardConverter
$newProperty['X-APPLE-OMIT-YEAR'] = '1604'; $newProperty['X-APPLE-OMIT-YEAR'] = '1604';
} }
if ('ANNIVERSARY' == $newProperty->name) { if ('ANNIVERSARY' === $newProperty->name) {
// Microsoft non-standard anniversary // Microsoft non-standard anniversary
$newProperty->name = 'X-ANNIVERSARY'; $newProperty->name = 'X-ANNIVERSARY';
@ -127,7 +127,7 @@ class VCardConverter
$output->add('ITEM'.$x.'.X-ABLABEL', '_$!<Anniversary>!$_'); $output->add('ITEM'.$x.'.X-ABLABEL', '_$!<Anniversary>!$_');
} }
} elseif ('KIND' === $property->name) { } elseif ('KIND' === $property->name) {
switch (strtolower($property->getValue())) { switch (strtolower((string) $property->getValue())) {
case 'org': case 'org':
// vCard 3.0 does not have an equivalent to KIND:ORG, // vCard 3.0 does not have an equivalent to KIND:ORG,
// but apple has an extension that means the same // but apple has an extension that means the same
@ -149,7 +149,7 @@ class VCardConverter
} }
} elseif (Document::VCARD40 === $targetVersion) { } elseif (Document::VCARD40 === $targetVersion) {
// These properties were removed in vCard 4.0 // These properties were removed in vCard 4.0
if (in_array($property->name, ['NAME', 'MAILER', 'LABEL', 'CLASS'])) { if (in_array($property->name, ['NAME', 'MAILER', 'LABEL', 'CLASS'], true)) {
return; return;
} }
@ -171,12 +171,12 @@ class VCardConverter
} }
switch ($property->name) { switch ($property->name) {
case 'X-ABSHOWAS': case 'X-ABSHOWAS':
if ('COMPANY' === strtoupper($property->getValue())) { if ('COMPANY' === strtoupper((string) $property->getValue())) {
$newProperty = $output->createProperty('KIND', 'ORG'); $newProperty = $output->createProperty('KIND', 'ORG');
} }
break; break;
case 'X-ADDRESSBOOKSERVER-KIND': case 'X-ADDRESSBOOKSERVER-KIND':
if ('GROUP' === strtoupper($property->getValue())) { if ('GROUP' === strtoupper((string) $property->getValue())) {
$newProperty = $output->createProperty('KIND', 'GROUP'); $newProperty = $output->createProperty('KIND', 'GROUP');
} }
break; break;
@ -274,10 +274,11 @@ class VCardConverter
$newTypes = []; $newTypes = [];
foreach ($parameters['TYPE']->getParts() as $typePart) { foreach ($parameters['TYPE']->getParts() as $typePart) {
if (in_array( if (in_array(
strtoupper($typePart), strtoupper((string) $typePart),
['JPEG', 'PNG', 'GIF'] ['JPEG', 'PNG', 'GIF'],
true
)) { )) {
$mimeType = 'image/'.strtolower($typePart); $mimeType = 'image/'.strtolower((string) $typePart);
} else { } else {
$newTypes[] = $typePart; $newTypes[] = $typePart;
} }
@ -292,7 +293,7 @@ class VCardConverter
} }
} }
$newProperty->setValue('data:'.$mimeType.';base64,'.base64_encode($value)); $newProperty->setValue('data:'.$mimeType.';base64,'.base64_encode((string) $value));
return $newProperty; return $newProperty;
} }
@ -313,7 +314,7 @@ class VCardConverter
$value = $newProperty->getValue(); $value = $newProperty->getValue();
// Only converting data: uris // Only converting data: uris
if ('data:' !== substr($value, 0, 5)) { if (!str_starts_with((string) $value, 'data:')) {
return $newProperty; return $newProperty;
} }
@ -325,12 +326,12 @@ class VCardConverter
'BINARY' 'BINARY'
); );
$mimeType = substr($value, 5, strpos($value, ',') - 5); $mimeType = substr((string) $value, 5, strpos((string) $value, ',') - 5);
if (strpos($mimeType, ';')) { if (strpos($mimeType, ';')) {
$mimeType = substr($mimeType, 0, strpos($mimeType, ';')); $mimeType = substr($mimeType, 0, strpos($mimeType, ';'));
$newProperty->setValue(base64_decode(substr($value, strpos($value, ',') + 1))); $newProperty->setValue(base64_decode(substr((string) $value, strpos((string) $value, ',') + 1), true));
} else { } else {
$newProperty->setValue(substr($value, strpos($value, ',') + 1)); $newProperty->setValue(substr((string) $value, strpos((string) $value, ',') + 1));
} }
unset($value); unset($value);
@ -367,7 +368,7 @@ class VCardConverter
// that's now PREF=1. // that's now PREF=1.
case 'TYPE': case 'TYPE':
foreach ($param->getParts() as $paramPart) { foreach ($param->getParts() as $paramPart) {
if ('PREF' === strtoupper($paramPart)) { if ('PREF' === strtoupper((string) $paramPart)) {
$newProperty->add('PREF', '1'); $newProperty->add('PREF', '1');
} else { } else {
$newProperty->add($param->name, $paramPart); $newProperty->add($param->name, $paramPart);
@ -402,7 +403,7 @@ class VCardConverter
case 'ENCODING': case 'ENCODING':
// This value only existed in vCard 2.1, and should be // This value only existed in vCard 2.1, and should be
// removed for anything else. // removed for anything else.
if ('QUOTED-PRINTABLE' !== strtoupper($param->getValue())) { if ('QUOTED-PRINTABLE' !== strtoupper((string) $param->getValue())) {
$newProperty->add($param->name, $param->getParts()); $newProperty->add($param->name, $param->getParts());
} }
break; break;
@ -413,7 +414,7 @@ class VCardConverter
* Any other PREF numbers we'll drop. * Any other PREF numbers we'll drop.
*/ */
case 'PREF': case 'PREF':
if ('1' == $param->getValue()) { if ('1' === (string) $param->getValue()) {
$newProperty->add('TYPE', 'PREF'); $newProperty->add('TYPE', 'PREF');
} }
break; break;

View file

@ -14,5 +14,5 @@ class Version
/** /**
* Full version number. * Full version number.
*/ */
public const VERSION = '4.5.2'; public const VERSION = '5.0.0';
} }

View file

@ -17,6 +17,7 @@ return [
'Greenwich Mean Time: Dublin, Edinburgh, Lisbon, London' => 'Europe/Lisbon', 'Greenwich Mean Time: Dublin, Edinburgh, Lisbon, London' => 'Europe/Lisbon',
'Greenwich Mean Time; Dublin, Edinburgh, London' => 'Europe/London', 'Greenwich Mean Time; Dublin, Edinburgh, London' => 'Europe/London',
'Amsterdam, Berlin, Bern, Rome, Stockholm, Vienna' => 'Europe/Berlin', 'Amsterdam, Berlin, Bern, Rome, Stockholm, Vienna' => 'Europe/Berlin',
'Amsterdam, Berlin, Bern, Rom, Stockholm, Wien' => 'Europe/Berlin',
'Belgrade, Pozsony, Budapest, Ljubljana, Prague' => 'Europe/Prague', 'Belgrade, Pozsony, Budapest, Ljubljana, Prague' => 'Europe/Prague',
'Brussels, Copenhagen, Madrid, Paris' => 'Europe/Paris', 'Brussels, Copenhagen, Madrid, Paris' => 'Europe/Paris',
'Paris, Madrid, Brussels, Copenhagen' => 'Europe/Paris', 'Paris, Madrid, Brussels, Copenhagen' => 'Europe/Paris',
@ -39,12 +40,12 @@ return [
'Kabul' => 'Asia/Kabul', 'Kabul' => 'Asia/Kabul',
'Ekaterinburg' => 'Asia/Yekaterinburg', 'Ekaterinburg' => 'Asia/Yekaterinburg',
'Islamabad, Karachi, Tashkent' => 'Asia/Karachi', 'Islamabad, Karachi, Tashkent' => 'Asia/Karachi',
'Kolkata, Chennai, Mumbai, New Delhi, India Standard Time' => 'Asia/Calcutta', 'Kolkata, Chennai, Mumbai, New Delhi, India Standard Time' => 'Asia/Kolkata',
'Kathmandu, Nepal' => 'Asia/Kathmandu', 'Kathmandu, Nepal' => 'Asia/Kathmandu',
'Almaty, Novosibirsk, North Central Asia' => 'Asia/Almaty', 'Almaty, Novosibirsk, North Central Asia' => 'Asia/Almaty',
'Astana, Dhaka' => 'Asia/Dhaka', 'Astana, Dhaka' => 'Asia/Dhaka',
'Sri Jayawardenepura, Sri Lanka' => 'Asia/Colombo', 'Sri Jayawardenepura, Sri Lanka' => 'Asia/Colombo',
'Rangoon' => 'Asia/Rangoon', 'Rangoon' => 'Asia/Yangon',
'Bangkok, Hanoi, Jakarta' => 'Asia/Bangkok', 'Bangkok, Hanoi, Jakarta' => 'Asia/Bangkok',
'Krasnoyarsk' => 'Asia/Krasnoyarsk', 'Krasnoyarsk' => 'Asia/Krasnoyarsk',
'Beijing, Chongqing, Hong Kong SAR, Urumqi' => 'Asia/Shanghai', 'Beijing, Chongqing, Hong Kong SAR, Urumqi' => 'Asia/Shanghai',
@ -71,7 +72,7 @@ return [
'Mid-Atlantic' => 'America/Noronha', 'Mid-Atlantic' => 'America/Noronha',
'Brasilia' => 'America/Sao_Paulo', // Best guess 'Brasilia' => 'America/Sao_Paulo', // Best guess
'Buenos Aires' => 'America/Argentina/Buenos_Aires', 'Buenos Aires' => 'America/Argentina/Buenos_Aires',
'Greenland' => 'America/Godthab', 'Greenland' => 'America/Nuuk',
'Newfoundland' => 'America/St_Johns', 'Newfoundland' => 'America/St_Johns',
'Atlantic Time (Canada)' => 'America/Halifax', 'Atlantic Time (Canada)' => 'America/Halifax',
'Caracas, La Paz' => 'America/Caracas', 'Caracas, La Paz' => 'America/Caracas',

View file

@ -34,7 +34,7 @@ return [
'Newfoundland' => 'America/St_Johns', 'Newfoundland' => 'America/St_Johns',
'Argentina' => 'America/Argentina/Buenos_Aires', 'Argentina' => 'America/Argentina/Buenos_Aires',
'E. South America' => 'America/Belem', 'E. South America' => 'America/Belem',
'Greenland' => 'America/Godthab', 'Greenland' => 'America/Nuuk',
'Montevideo' => 'America/Montevideo', 'Montevideo' => 'America/Montevideo',
'SA Eastern' => 'America/Belem', 'SA Eastern' => 'America/Belem',
// 'Mid-Atlantic' => 'Etc/GMT-2', // conflict with windows timezones. // 'Mid-Atlantic' => 'Etc/GMT-2', // conflict with windows timezones.
@ -71,12 +71,12 @@ return [
'Ekaterinburg' => 'Asia/Yekaterinburg', 'Ekaterinburg' => 'Asia/Yekaterinburg',
'Pakistan' => 'Asia/Karachi', 'Pakistan' => 'Asia/Karachi',
'West Asia' => 'Asia/Tashkent', 'West Asia' => 'Asia/Tashkent',
'India' => 'Asia/Calcutta', 'India' => 'Asia/Kolkata',
'Sri Lanka' => 'Asia/Colombo', 'Sri Lanka' => 'Asia/Colombo',
'Nepal' => 'Asia/Kathmandu', 'Nepal' => 'Asia/Kathmandu',
'Central Asia' => 'Asia/Dhaka', 'Central Asia' => 'Asia/Dhaka',
'N. Central Asia' => 'Asia/Almaty', 'N. Central Asia' => 'Asia/Almaty',
'Myanmar' => 'Asia/Rangoon', 'Myanmar' => 'Asia/Yangon',
'North Asia' => 'Asia/Krasnoyarsk', 'North Asia' => 'Asia/Krasnoyarsk',
'SE Asia' => 'Asia/Bangkok', 'SE Asia' => 'Asia/Bangkok',
'China' => 'Asia/Shanghai', 'China' => 'Asia/Shanghai',

View file

@ -20,7 +20,7 @@ return [
'Arab Standard Time' => 'Asia/Riyadh', 'Arab Standard Time' => 'Asia/Riyadh',
'Arabian Standard Time' => 'Asia/Dubai', 'Arabian Standard Time' => 'Asia/Dubai',
'Arabic Standard Time' => 'Asia/Baghdad', 'Arabic Standard Time' => 'Asia/Baghdad',
'Argentina Standard Time' => 'America/Buenos_Aires', 'Argentina Standard Time' => 'America/Argentina/Buenos_Aires',
'Astrakhan Standard Time' => 'Europe/Astrakhan', 'Astrakhan Standard Time' => 'Europe/Astrakhan',
'Atlantic Standard Time' => 'America/Halifax', 'Atlantic Standard Time' => 'America/Halifax',
'Aus Central W. Standard Time' => 'Australia/Eucla', 'Aus Central W. Standard Time' => 'Australia/Eucla',
@ -55,16 +55,16 @@ return [
'Eastern Standard Time (Mexico)' => 'America/Cancun', 'Eastern Standard Time (Mexico)' => 'America/Cancun',
'Egypt Standard Time' => 'Africa/Cairo', 'Egypt Standard Time' => 'Africa/Cairo',
'Ekaterinburg Standard Time' => 'Asia/Yekaterinburg', 'Ekaterinburg Standard Time' => 'Asia/Yekaterinburg',
'FLE Standard Time' => 'Europe/Kiev', 'FLE Standard Time' => 'Europe/Kyiv',
'Fiji Standard Time' => 'Pacific/Fiji', 'Fiji Standard Time' => 'Pacific/Fiji',
'GMT Standard Time' => 'Europe/London', 'GMT Standard Time' => 'Europe/London',
'GTB Standard Time' => 'Europe/Bucharest', 'GTB Standard Time' => 'Europe/Bucharest',
'Georgian Standard Time' => 'Asia/Tbilisi', 'Georgian Standard Time' => 'Asia/Tbilisi',
'Greenland Standard Time' => 'America/Godthab', 'Greenland Standard Time' => 'America/Nuuk',
'Greenwich Standard Time' => 'Atlantic/Reykjavik', 'Greenwich Standard Time' => 'Atlantic/Reykjavik',
'Haiti Standard Time' => 'America/Port-au-Prince', 'Haiti Standard Time' => 'America/Port-au-Prince',
'Hawaiian Standard Time' => 'Pacific/Honolulu', 'Hawaiian Standard Time' => 'Pacific/Honolulu',
'India Standard Time' => 'Asia/Calcutta', 'India Standard Time' => 'Asia/Kolkata',
'Iran Standard Time' => 'Asia/Tehran', 'Iran Standard Time' => 'Asia/Tehran',
'Israel Standard Time' => 'Asia/Jerusalem', 'Israel Standard Time' => 'Asia/Jerusalem',
'Jordan Standard Time' => 'Asia/Amman', 'Jordan Standard Time' => 'Asia/Amman',
@ -82,10 +82,10 @@ return [
'Morocco Standard Time' => 'Africa/Casablanca', 'Morocco Standard Time' => 'Africa/Casablanca',
'Mountain Standard Time' => 'America/Denver', 'Mountain Standard Time' => 'America/Denver',
'Mountain Standard Time (Mexico)' => 'America/Chihuahua', 'Mountain Standard Time (Mexico)' => 'America/Chihuahua',
'Myanmar Standard Time' => 'Asia/Rangoon', 'Myanmar Standard Time' => 'Asia/Yangon',
'N. Central Asia Standard Time' => 'Asia/Novosibirsk', 'N. Central Asia Standard Time' => 'Asia/Novosibirsk',
'Namibia Standard Time' => 'Africa/Windhoek', 'Namibia Standard Time' => 'Africa/Windhoek',
'Nepal Standard Time' => 'Asia/Katmandu', 'Nepal Standard Time' => 'Asia/Kathmandu',
'New Zealand Standard Time' => 'Pacific/Auckland', 'New Zealand Standard Time' => 'Pacific/Auckland',
'Newfoundland Standard Time' => 'America/St_Johns', 'Newfoundland Standard Time' => 'America/St_Johns',
'Norfolk Standard Time' => 'Pacific/Norfolk', 'Norfolk Standard Time' => 'Pacific/Norfolk',
@ -127,7 +127,7 @@ return [
'Transbaikal Standard Time' => 'Asia/Chita', 'Transbaikal Standard Time' => 'Asia/Chita',
'Turkey Standard Time' => 'Europe/Istanbul', 'Turkey Standard Time' => 'Europe/Istanbul',
'Turks And Caicos Standard Time' => 'America/Grand_Turk', 'Turks And Caicos Standard Time' => 'America/Grand_Turk',
'US Eastern Standard Time' => 'America/Indianapolis', 'US Eastern Standard Time' => 'America/Indiana/Indianapolis',
'US Mountain Standard Time' => 'America/Phoenix', 'US Mountain Standard Time' => 'America/Phoenix',
'UTC' => 'Etc/GMT', 'UTC' => 'Etc/GMT',
'UTC+12' => 'Etc/GMT-12', 'UTC+12' => 'Etc/GMT-12',

View file

@ -212,11 +212,13 @@ function valueObject(Reader $reader, string $className, string $namespace): obje
$reader->read(); $reader->read();
do { do {
if (Reader::ELEMENT === $reader->nodeType && $reader->namespaceURI == $namespace) { if (Reader::ELEMENT === $reader->nodeType && $reader->namespaceURI === $namespace) {
if (property_exists($valueObject, $reader->localName)) { if (property_exists($valueObject, $reader->localName)) {
if (is_array($defaultProperties[$reader->localName])) { if (is_array($defaultProperties[$reader->localName])) {
// @phpstan-ignore property.dynamicName
$valueObject->{$reader->localName}[] = $reader->parseCurrentElement()['value']; $valueObject->{$reader->localName}[] = $reader->parseCurrentElement()['value'];
} else { } else {
// @phpstan-ignore property.dynamicName
$valueObject->{$reader->localName} = $reader->parseCurrentElement()['value']; $valueObject->{$reader->localName} = $reader->parseCurrentElement()['value'];
} }
} else { } else {

View file

@ -19,19 +19,13 @@ use Sabre\Xml;
*/ */
class Base implements Xml\Element class Base implements Xml\Element
{ {
/**
* @var mixed PHP value to serialize
*/
protected $value;
/** /**
* Constructor. * Constructor.
* *
* @param mixed $value PHP value to serialize * @param mixed $value PHP value to serialize
*/ */
public function __construct($value = null) public function __construct(protected $value = null)
{ {
$this->value = $value;
} }
/** /**

View file

@ -21,17 +21,15 @@ use Sabre\Xml;
*/ */
class Cdata implements Xml\XmlSerializable class Cdata implements Xml\XmlSerializable
{ {
/**
* CDATA element value.
*/
protected string $value;
/** /**
* Constructor. * Constructor.
*/ */
public function __construct(string $value) public function __construct(
{ /**
$this->value = $value; * CDATA element value.
*/
protected string $value,
) {
} }
/** /**
@ -52,6 +50,6 @@ class Cdata implements Xml\XmlSerializable
*/ */
public function xmlSerialize(Xml\Writer $writer): void public function xmlSerialize(Xml\Writer $writer): void
{ {
$writer->writeCData($this->value); $writer->writeCdata($this->value);
} }
} }

View file

@ -37,21 +37,17 @@ use Sabre\Xml\Serializer;
*/ */
class Elements implements Xml\Element class Elements implements Xml\Element
{ {
/**
* Value to serialize.
*
* @var array<int, mixed>
*/
protected array $value;
/** /**
* Constructor. * Constructor.
* *
* @param array<int, mixed> $value * @param array<int, mixed> $value
*/ */
public function __construct(array $value = []) public function __construct(
{ /**
$this->value = $value; * Value to serialize.
*/
protected array $value = [],
) {
} }
/** /**

View file

@ -37,21 +37,17 @@ use Sabre\Xml\Deserializer;
*/ */
class KeyValue implements Xml\Element class KeyValue implements Xml\Element
{ {
/**
* Value to serialize.
*
* @var array<string, mixed>
*/
protected array $value;
/** /**
* Constructor. * Constructor.
* *
* @param array<string, mixed> $value * @param array<string, mixed> $value
*/ */
public function __construct(array $value = []) public function __construct(
{ /**
$this->value = $value; * Value to serialize.
*/
protected array $value = [],
) {
} }
/** /**

View file

@ -26,17 +26,15 @@ use function Sabre\Uri\resolve;
*/ */
class Uri implements Xml\Element class Uri implements Xml\Element
{ {
/**
* Uri element value.
*/
protected string $value;
/** /**
* Constructor. * Constructor.
*/ */
public function __construct(string $value) public function __construct(
{ /**
$this->value = $value; * Uri element value.
*/
protected string $value,
) {
} }
/** /**

View file

@ -24,17 +24,15 @@ use Sabre\Xml\Writer;
*/ */
class XmlFragment implements Element class XmlFragment implements Element
{ {
/**
* The inner XML value.
*/
protected string $xml;
/** /**
* Constructor. * Constructor.
*/ */
public function __construct(string $xml) public function __construct(
{ /**
$this->xml = $xml; * The inner XML value.
*/
protected string $xml,
) {
} }
/** /**
@ -72,7 +70,7 @@ class XmlFragment implements Element
<xml-fragment xmlns="http://sabre.io/ns">{$this->getXml()}</xml-fragment> <xml-fragment xmlns="http://sabre.io/ns">{$this->getXml()}</xml-fragment>
XML; XML;
$reader->xml($xml); $reader->XML($xml);
while ($reader->read()) { while ($reader->read()) {
if ($reader->depth < 1) { if ($reader->depth < 1) {

View file

@ -17,13 +17,6 @@ use LibXMLError;
*/ */
class LibXMLException extends ParseException class LibXMLException extends ParseException
{ {
/**
* The error list.
*
* @var \LibXMLError[]
*/
protected array $errors;
/** /**
* Creates the exception. * Creates the exception.
* *
@ -31,10 +24,12 @@ class LibXMLException extends ParseException
* *
* @param \LibXMLError[] $errors * @param \LibXMLError[] $errors
*/ */
public function __construct(array $errors, int $code = 0, ?\Throwable $previousException = null) public function __construct(/**
* The error list.
*/
protected array $errors, int $code = 0, ?\Throwable $previousException = null)
{ {
$this->errors = $errors; parent::__construct($this->errors[0]->message.' on line '.$this->errors[0]->line.', column '.$this->errors[0]->column, $code, $previousException);
parent::__construct($errors[0]->message.' on line '.$errors[0]->line.', column '.$errors[0]->column, $code, $previousException);
} }
/** /**

View file

@ -33,7 +33,7 @@ class Reader extends \XMLReader
*/ */
public function getClark(): ?string public function getClark(): ?string
{ {
if (!$this->localName) { if ('' === $this->localName) {
return null; return null;
} }
@ -67,7 +67,7 @@ class Reader extends \XMLReader
if (!$this->read()) { if (!$this->read()) {
$errors = libxml_get_errors(); $errors = libxml_get_errors();
libxml_clear_errors(); libxml_clear_errors();
if ($errors) { if (count($errors) > 0) {
throw new LibXMLException($errors); throw new LibXMLException($errors);
} }
} }
@ -77,7 +77,7 @@ class Reader extends \XMLReader
// last line of defense in case errors did occur above // last line of defense in case errors did occur above
$errors = libxml_get_errors(); $errors = libxml_get_errors();
libxml_clear_errors(); libxml_clear_errors();
if ($errors) { if (count($errors) > 0) {
throw new LibXMLException($errors); throw new LibXMLException($errors);
} }
} finally { } finally {
@ -153,7 +153,7 @@ class Reader extends \XMLReader
if (!$this->read()) { if (!$this->read()) {
$errors = libxml_get_errors(); $errors = libxml_get_errors();
libxml_clear_errors(); libxml_clear_errors();
if ($errors) { if (count($errors) > 0) {
throw new LibXMLException($errors); throw new LibXMLException($errors);
} }
throw new ParseException('This should never happen (famous last words)'); throw new ParseException('This should never happen (famous last words)');
@ -165,7 +165,7 @@ class Reader extends \XMLReader
if (!$this->isValid()) { if (!$this->isValid()) {
$errors = libxml_get_errors(); $errors = libxml_get_errors();
if ($errors) { if (count($errors) > 0) {
libxml_clear_errors(); libxml_clear_errors();
throw new LibXMLException($errors); throw new LibXMLException($errors);
} }
@ -199,7 +199,7 @@ class Reader extends \XMLReader
} }
} }
return $elements ?: $text; return count($elements) > 0 ? $elements : $text;
} }
/** /**
@ -210,8 +210,8 @@ class Reader extends \XMLReader
$result = ''; $result = '';
$previousDepth = $this->depth; $previousDepth = $this->depth;
while ($this->read() && $this->depth != $previousDepth) { while ($this->read() && $this->depth !== $previousDepth) {
if (in_array($this->nodeType, [\XMLReader::TEXT, \XMLReader::CDATA, \XMLReader::WHITESPACE])) { if (in_array($this->nodeType, [\XMLReader::TEXT, \XMLReader::CDATA, \XMLReader::WHITESPACE], true)) {
$result .= $this->value; $result .= $this->value;
} }
} }
@ -266,7 +266,7 @@ class Reader extends \XMLReader
$attributes = []; $attributes = [];
while ($this->moveToNextAttribute()) { while ($this->moveToNextAttribute()) {
if ($this->namespaceURI) { if ('' !== $this->namespaceURI) {
// Ignoring 'xmlns', it doesn't make any sense. // Ignoring 'xmlns', it doesn't make any sense.
if ('http://www.w3.org/2000/xmlns/' === $this->namespaceURI) { if ('http://www.w3.org/2000/xmlns/' === $this->namespaceURI) {
continue; continue;
@ -290,10 +290,10 @@ class Reader extends \XMLReader
public function getDeserializerForElementName(string $name): callable public function getDeserializerForElementName(string $name): callable
{ {
if (!array_key_exists($name, $this->elementMap)) { if (!array_key_exists($name, $this->elementMap)) {
if ('{}' == substr($name, 0, 2) && array_key_exists(substr($name, 2), $this->elementMap)) { if (str_starts_with($name, '{}') && array_key_exists(substr($name, 2), $this->elementMap)) {
$name = substr($name, 2); $name = substr($name, 2);
} else { } else {
return ['Sabre\\Xml\\Element\\Base', 'xmlDeserialize']; return Element\Base::xmlDeserialize(...);
} }
} }
@ -302,8 +302,8 @@ class Reader extends \XMLReader
return $deserializer; return $deserializer;
} }
if (is_subclass_of($deserializer, 'Sabre\\Xml\\XmlDeserializable')) { if (is_subclass_of($deserializer, XmlDeserializable::class)) {
return [$deserializer, 'xmlDeserialize']; return fn (Reader $reader) => $deserializer::xmlDeserialize($reader);
} }
throw new \LogicException('Could not use this type as a deserializer: '.get_debug_type($deserializer).' for element: '.$name); throw new \LogicException('Could not use this type as a deserializer: '.get_debug_type($deserializer).' for element: '.$name);

View file

@ -123,7 +123,7 @@ function valueObject(Writer $writer, object $valueObject, string $namespace): vo
* *
* You can even mix the two array syntaxes. * You can even mix the two array syntaxes.
* *
* @param string|int|float|bool|array<int|string, mixed>|object $value * @param string|int|float|bool|array<int|string, mixed>|object|null $value
*/ */
function standardSerializer(Writer $writer, $value): void function standardSerializer(Writer $writer, $value): void
{ {
@ -133,9 +133,9 @@ function standardSerializer(Writer $writer, $value): void
} elseif ($value instanceof XmlSerializable) { } elseif ($value instanceof XmlSerializable) {
// XmlSerializable classes or Element classes. // XmlSerializable classes or Element classes.
$value->xmlSerialize($writer); $value->xmlSerialize($writer);
} elseif (is_object($value) && isset($writer->classMap[get_class($value)])) { } elseif (is_object($value) && isset($writer->classMap[$value::class])) {
// It's an object which class appears in the classmap. // It's an object which class appears in the classmap.
$writer->classMap[get_class($value)]($writer, $value); $writer->classMap[$value::class]($writer, $value);
} elseif (is_callable($value)) { } elseif (is_callable($value)) {
// A callback // A callback
$value($writer); $value($writer);
@ -144,8 +144,8 @@ function standardSerializer(Writer $writer, $value): void
// describes a 'name' and optionally 'attributes' and 'value'. // describes a 'name' and optionally 'attributes' and 'value'.
$name = $value['name']; $name = $value['name'];
$attributes = isset($value['attributes']) ? $value['attributes'] : []; $attributes = $value['attributes'] ?? [];
$value = isset($value['value']) ? $value['value'] : null; $value = $value['value'] ?? null;
$writer->startElement($name); $writer->startElement($name);
$writer->writeAttributes($attributes); $writer->writeAttributes($attributes);
@ -157,7 +157,7 @@ function standardSerializer(Writer $writer, $value): void
// This item has a numeric index. We just loop through the // This item has a numeric index. We just loop through the
// array and throw it back in the writer. // array and throw it back in the writer.
standardSerializer($writer, $item); standardSerializer($writer, $item);
} elseif (is_string($name) && is_array($item) && isset($item['attributes'])) { } elseif (is_array($item) && isset($item['attributes'])) {
// The key is used for a name, but $item has 'attributes' and // The key is used for a name, but $item has 'attributes' and
// possibly 'value' // possibly 'value'
$writer->startElement($name); $writer->startElement($name);
@ -176,7 +176,7 @@ function standardSerializer(Writer $writer, $value): void
} }
} }
} elseif (is_object($value)) { } elseif (is_object($value)) {
throw new \InvalidArgumentException('The writer cannot serialize objects of class: '.get_class($value)); throw new \InvalidArgumentException('The writer cannot serialize objects of class: '.$value::class);
} elseif (!is_null($value)) { } elseif (!is_null($value)) {
throw new \InvalidArgumentException('The writer cannot serialize values of type: '.get_debug_type($value)); throw new \InvalidArgumentException('The writer cannot serialize values of type: '.get_debug_type($value));
} }

View file

@ -123,7 +123,7 @@ class Service
} }
// If input is empty, then it's safe to throw an exception // If input is empty, then it's safe to throw an exception
if (empty($input)) { if ('' === $input) {
throw new ParseException('The input element to parse is empty. Do not attempt to parse'); throw new ParseException('The input element to parse is empty. Do not attempt to parse');
} }
@ -174,7 +174,7 @@ class Service
} }
// If input is empty, then it's safe to throw an exception // If input is empty, then it's safe to throw an exception
if (empty($input)) { if ('' === $input) {
throw new ParseException('The input element to parse is empty. Do not attempt to parse'); throw new ParseException('The input element to parse is empty. Do not attempt to parse');
} }
@ -254,14 +254,12 @@ class Service
*/ */
public function mapValueObject(string $elementName, string $className): void public function mapValueObject(string $elementName, string $className): void
{ {
list($namespace) = self::parseClarkNotation($elementName); [$namespace] = self::parseClarkNotation($elementName);
require_once __DIR__ . '/Deserializer/functions.php'; require_once __DIR__ . '/Deserializer/functions.php';
$this->elementMap[$elementName] = function (Reader $reader) use ($className, $namespace) { $this->elementMap[$elementName] = (fn (Reader $reader) => \Sabre\Xml\Deserializer\valueObject($reader, $className, $namespace));
return \Sabre\Xml\Deserializer\valueObject($reader, $className, $namespace);
};
require_once __DIR__ . '/Serializer/functions.php'; require_once __DIR__ . '/Serializer/functions.php';
$this->classMap[$className] = function (Writer $writer, $valueObject) use ($namespace) { $this->classMap[$className] = function (Writer $writer, $valueObject) use ($namespace): void {
\Sabre\Xml\Serializer\valueObject($writer, $valueObject, $namespace); \Sabre\Xml\Serializer\valueObject($writer, $valueObject, $namespace);
}; };
$this->valueObjectMap[$className] = $elementName; $this->valueObjectMap[$className] = $elementName;
@ -280,12 +278,12 @@ class Service
*/ */
public function writeValueObject(object $object, ?string $contextUri = null): string public function writeValueObject(object $object, ?string $contextUri = null): string
{ {
if (!isset($this->valueObjectMap[get_class($object)])) { if (!isset($this->valueObjectMap[$object::class])) {
throw new \InvalidArgumentException('"'.get_class($object).'" is not a registered value object class. Register your class with mapValueObject.'); throw new \InvalidArgumentException('"'.$object::class.'" is not a registered value object class. Register your class with mapValueObject.');
} }
return $this->write( return $this->write(
$this->valueObjectMap[get_class($object)], $this->valueObjectMap[$object::class],
$object, $object,
$contextUri $contextUri
); );
@ -306,7 +304,7 @@ class Service
static $cache = []; static $cache = [];
if (!isset($cache[$str])) { if (!isset($cache[$str])) {
if (!preg_match('/^{([^}]*)}(.*)$/', $str, $matches)) { if (1 !== preg_match('/^{([^}]*)}(.*)$/', $str, $matches)) {
throw new \InvalidArgumentException('\''.$str.'\' is not a valid clark-notation formatted string'); throw new \InvalidArgumentException('\''.$str.'\' is not a valid clark-notation formatted string');
} }

View file

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

View file

@ -123,11 +123,11 @@ class Writer extends \XMLWriter
public function startElement($name): bool public function startElement($name): bool
{ {
if ('{' === $name[0]) { if ('{' === $name[0]) {
list($namespace, $localName) = [$namespace, $localName] =
Service::parseClarkNotation($name); Service::parseClarkNotation($name);
if (array_key_exists($namespace, $this->namespaceMap)) { if (array_key_exists($namespace, $this->namespaceMap)) {
$result = $this->startElementNS( $result = $this->startElementNs(
'' === $this->namespaceMap[$namespace] ? null : $this->namespaceMap[$namespace], '' === $this->namespaceMap[$namespace] ? null : $this->namespaceMap[$namespace],
$localName, $localName,
null null
@ -139,10 +139,8 @@ class Writer extends \XMLWriter
$result = $this->startElement($localName); $result = $this->startElement($localName);
$this->writeAttribute('xmlns', ''); $this->writeAttribute('xmlns', '');
} else { } else {
if (!isset($this->adhocNamespaces[$namespace])) { $this->adhocNamespaces[$namespace] ??= 'x'.(count($this->adhocNamespaces) + 1);
$this->adhocNamespaces[$namespace] = 'x'.(count($this->adhocNamespaces) + 1); $result = $this->startElementNs($this->adhocNamespaces[$namespace], $localName, $namespace);
}
$result = $this->startElementNS($this->adhocNamespaces[$namespace], $localName, $namespace);
} }
} }
} else { } else {
@ -151,7 +149,7 @@ class Writer extends \XMLWriter
if (!$this->namespacesWritten) { if (!$this->namespacesWritten) {
foreach ($this->namespaceMap as $namespace => $prefix) { foreach ($this->namespaceMap as $namespace => $prefix) {
$this->writeAttribute($prefix ? 'xmlns:'.$prefix : 'xmlns', $namespace); $this->writeAttribute(is_string($prefix) && '' !== $prefix ? 'xmlns:'.$prefix : 'xmlns', $namespace);
} }
$this->namespacesWritten = true; $this->namespacesWritten = true;
} }
@ -248,11 +246,9 @@ class Writer extends \XMLWriter
} }
// We don't know the namespace, we must add it in-line // We don't know the namespace, we must add it in-line
if (!isset($this->adhocNamespaces[$namespace])) { $this->adhocNamespaces[$namespace] ??= 'x'.(count($this->adhocNamespaces) + 1);
$this->adhocNamespaces[$namespace] = 'x'.(count($this->adhocNamespaces) + 1);
}
return $this->writeAttributeNS( return $this->writeAttributeNs(
$this->adhocNamespaces[$namespace], $this->adhocNamespaces[$namespace],
$localName, $localName,
$namespace, $namespace,

View file

@ -495,6 +495,7 @@
"CHROME_NOTIFICATION_DESC_DENIED": "(تم إعتراضها من قبل المتصفح)", "CHROME_NOTIFICATION_DESC_DENIED": "(تم إعتراضها من قبل المتصفح)",
"COLLAPSE_BLOCKQUOTES": "Collapse quoted text", "COLLAPSE_BLOCKQUOTES": "Collapse quoted text",
"MAX_QUOTE_LEVEL": "Maximum visible quoted text levels", "MAX_QUOTE_LEVEL": "Maximum visible quoted text levels",
"MIN_FONT_SIZE": "Minimum Font Size",
"LIST_INLINE_ATTACHMENTS": "Show inline attachments in list", "LIST_INLINE_ATTACHMENTS": "Show inline attachments in list",
"LIST_SIMPLE_ATTACHMENTS": "Simple attachments list", "LIST_SIMPLE_ATTACHMENTS": "Simple attachments list",
"LIST_MESSAGES_GROUPED": "List messages grouped (when sorting by date or from)", "LIST_MESSAGES_GROUPED": "List messages grouped (when sorting by date or from)",

View file

@ -495,6 +495,7 @@
"CHROME_NOTIFICATION_DESC_DENIED": "(Заблакавана браўзэрам)", "CHROME_NOTIFICATION_DESC_DENIED": "(Заблакавана браўзэрам)",
"COLLAPSE_BLOCKQUOTES": "Згортваць блок с працытаваным тэкстам", "COLLAPSE_BLOCKQUOTES": "Згортваць блок с працытаваным тэкстам",
"MAX_QUOTE_LEVEL": "Максімальны ўзровень адлюстравання працытаванага тэксту", "MAX_QUOTE_LEVEL": "Максімальны ўзровень адлюстравання працытаванага тэксту",
"MIN_FONT_SIZE": "Minimum Font Size",
"LIST_INLINE_ATTACHMENTS": "Паказваць укладанні ў спісе", "LIST_INLINE_ATTACHMENTS": "Паказваць укладанні ў спісе",
"LIST_SIMPLE_ATTACHMENTS": "Просты спіс укладанняў", "LIST_SIMPLE_ATTACHMENTS": "Просты спіс укладанняў",
"LIST_MESSAGES_GROUPED": "Группаванне спісу паведамленняў (пры сартаванні по даце альбо адпраўніку)", "LIST_MESSAGES_GROUPED": "Группаванне спісу паведамленняў (пры сартаванні по даце альбо адпраўніку)",

View file

@ -495,6 +495,7 @@
"CHROME_NOTIFICATION_DESC_DENIED": "(Блокирано от браузъра)", "CHROME_NOTIFICATION_DESC_DENIED": "(Блокирано от браузъра)",
"COLLAPSE_BLOCKQUOTES": "Collapse quoted text", "COLLAPSE_BLOCKQUOTES": "Collapse quoted text",
"MAX_QUOTE_LEVEL": "Maximum visible quoted text levels", "MAX_QUOTE_LEVEL": "Maximum visible quoted text levels",
"MIN_FONT_SIZE": "Minimum Font Size",
"LIST_INLINE_ATTACHMENTS": "Show inline attachments in list", "LIST_INLINE_ATTACHMENTS": "Show inline attachments in list",
"LIST_SIMPLE_ATTACHMENTS": "Simple attachments list", "LIST_SIMPLE_ATTACHMENTS": "Simple attachments list",
"LIST_MESSAGES_GROUPED": "List messages grouped (when sorting by date or from)", "LIST_MESSAGES_GROUPED": "List messages grouped (when sorting by date or from)",

View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 RainLoopTeam
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,17 @@
# Catalan translation
Catalan (`ca`, Català) translation for SnappyMail. A complete linguistic pass
was applied on 2026-10-16; see [review notes](REVIEW.md). Further corrections
from Catalan-speaking users are welcome.
Existing translations were adapted from RainLoop's official Catalan resources:
https://github.com/RainLoop/rainloop-webmail/tree/9bf8fa5b63d7e6871ee533126a7e1b1c85ee45b5/rainloop/v/0.0.0/app/localization
SnappyMail-specific strings were translated for this contribution. Preserve the
original MIT copyright and permission notice in `LICENSE-RainLoop` when reusing
the adapted RainLoop texts. The translation includes user, admin and static
messages, plus the relative-time fallback resource.
Native language selector on a test login page (no authenticated session or mail):
![Català in the native selector](selector.png)

View file

@ -0,0 +1,43 @@
# Catalan linguistic review — 2026-10-16
Reviewed the full user, administration, static and relative-time resources
against their English originals: 567 user entries, 140 administration entries
and 7 static entries. This pass changes 210 entries (161 user,
46 administration, 3 static), including typography.
## Editorial criteria
- Use the Softcatalà distinction between short imperative commands in buttons
(for example, *Desa*, *Xifra*) and the *vós* form when addressing users in
explanations, questions or instructions (*Introduïu*, *Seleccioneu*, *Voleu*).
- Prefer consistent terms such as *desar*, *xifrar/desxifrar*, *conversa*,
*text sense format* and *emmagatzematge*. Preserve proper names such as Sieve.
- Correct semantic errors, not only spelling: `STOP_LABEL` must stop further
processing, `LABEL_UNREAD` marks messages as unread, and the domain allow-list
describes permitted **users**, not domains.
- Distinguish read/display receipts from delivery receipts. Preserve all message
variables, including `%READ-RECEIPT%`.
- Remove inherited RainLoop product references from SnappyMail interface text.
- Preserve JSON keys, technical flags, placeholders and required HTML structure.
Use consistent apostrophes and quotation marks in displayed Catalan text.
## Representative corrections
| Before | After | Reason |
| --- | --- | --- |
| No aturis el processament de les regles | Atura el processament de les regles següents | The previous wording reversed the action. |
| No llegeixis els missatges seleccionats | Marca els missatges seleccionats com a no llegits | Describes changing the unread flag. |
| Net | Text sense format | Correct meaning of the plain-text editor option. |
| scripts de sedàs | scripts Sieve | Sieve is a protocol name. |
| Emmagatzeatge | Emmagatzematge | Spelling correction. |
| Llista de dominis… | Llista d’usuaris del domini… | The allow-list contains users. |
| El RainLoop està actualitzat. | SnappyMail està actualitzat. | Correct product name. |
## References
- [Softcatalà linguistic style](https://www.softcatala.org/guia-estil-de-softcatala/aspectes-linguistics/)
- [TERMCAT criteria for software localization](https://arxiu.termcat.cat/criteris/localitzacio-productes-informatics.pdf)
This is a linguistic review of this contribution, not an endorsement by either
organization. Further review and corrections from Catalan-speaking maintainers
remain welcome.

View file

@ -0,0 +1,182 @@
{
"GLOBAL": {
"ADD": "Afegeix",
"CLOSE": "Tanca",
"SAVE": "Desa",
"TEST": "Prova",
"UPDATE": "Actualitza",
"USERNAME": "Nom d’usuari",
"PASSWORD": "Contrasenya",
"CANCEL": "Cancel·la",
"SEARCH": "Cerca",
"YES": "Sí",
"NO": "No"
},
"LOGIN": {
"LABEL_LOGIN": "Nom d’usuari",
"LABEL_TOTP": "Codi TOTP",
"BUTTON_LOGIN": "Accedeix al tauler d’administració"
},
"TOP_PANEL": {
"LABEL_ADMIN_PANEL": "Tauler d’administració"
},
"SETTINGS_LABELS": {
"GENERAL": "General",
"LOGIN": "Inici de sessió",
"BRANDING": "Imatge de marca",
"CONTACTS": "Contactes",
"DOMAINS": "Dominis",
"SECURITY": "Seguretat",
"PACKAGES": "Extensions",
"CONFIG": "Configuració",
"ABOUT": "Quant a"
},
"TAB_GENERAL": {
"LEGEND_INTERFACE": "Interfície",
"LABEL_LANGUAGE": "Llengua",
"LABEL_LANGUAGE_ADMIN": "Llengua (administració)",
"LABEL_THEME": "Tema",
"LABEL_ALLOW_LANGUAGES_ON_SETTINGS": "Permet la selecció de llengua a la pantalla de configuració",
"LABEL_ALLOW_THEMES_ON_SETTINGS": "Permet la selecció del tema a la pantalla de configuració",
"LABEL_ALLOW_BACKGROUND_ON_SETTINGS": "Permet la selecció del fons a la pantalla de configuració",
"LABEL_SHOW_THUMBNAILS": "Mostra les miniatures (adjunts)",
"LEGEND_MAIN": "Principal",
"LABEL_ATTACHMENT_SIZE_LIMIT": "Límit de mida dels adjunts",
"LABEL_ALLOW_ADDITIONAL_ACCOUNTS": "Permet comptes addicionals",
"LABEL_ALLOW_IDENTITIES": "Permet identitats múltiples",
"ALERT_DATA_ACCESS": "La carpeta de dades és accessible des de l’exterior. Configureu el servidor web per impedir-hi l’accés extern. Més informació:",
"ALERT_WARNING": "Avís!",
"HTML_ALERT_WEAK_PASSWORD": "Esteu utilitzant la contrasenya d’administració predeterminada.\n<br>\nPer motius de seguretat,\n<strong><a href=\"#/security\">canvieu-la</a></strong>\nara per una altra.\n"
},
"TAB_LOGIN": {
"LEGEND_LOGIN_SCREEN": "Pantalla d’inici de sessió",
"LABEL_DEFAULT_DOMAIN": "Domini predeterminat",
"LABEL_DETERMINE_USER_DOMAIN": "Intenta determinar el domini de l’usuari",
"LABEL_ALLOW_LANGUAGES_ON_LOGIN": "Permet triar la llengua a la pantalla d’inici de sessió",
"LABEL_DETERMINE_USER_LANGUAGE": "Intenta determinar la llengua de l’usuari"
},
"TAB_BRANDING": {
"LEGEND_BRANDING": "Imatge de marca",
"LABEL_PAGE_TITLE": "Títol de la pàgina",
"LABEL_LOADING_DESCRIPTION": "Text durant la càrrega",
"LABEL_FAVICON_URL": "URL de la icona de favorits"
},
"TAB_CONTACTS": {
"LEGEND_CONTACTS": "Contactes",
"LEGEND_STORAGE": "Emmagatzematge (PDO)",
"LABEL_ENABLE_CONTACTS": "Activa els contactes",
"LABEL_ALLOW_SYNC": "Permet la sincronització dels contactes (amb el servidor extern CardDAV)",
"LABEL_STORAGE_TYPE": "Tipus",
"LABEL_STORAGE_DSN": "DSN (nom de la font de dades)",
"LABEL_STORAGE_USER": "Usuari",
"LABEL_STORAGE_PASSWORD": "Contrasenya",
"SQLITE_GLOBAL": "Utilitza una base de dades global en lloc d’una per compte",
"ALERT_NOTICE": "Atenció!",
"SUGGESTIONS_LIMIT": "Límit de suggeriments",
"HTML_ALERT_DO_NOT_USE_THIS_DATABASE": "No useu aquest tipus de base de dades amb un gran nombre d’usuaris actius.",
"HTML_ALERT_DOES_NOT_SUPPORTED": "El sistema no admet els contactes.\n<br>\nHeu d’instal·lar o activar l’extensió <strong>PDO (SQLite / MySQL / PostgreSQL)</strong> al servidor.\n"
},
"TAB_DOMAINS": {
"LEGEND_DOMAINS": "Dominis",
"BUTTON_ADD_DOMAIN": "Afegeix un domini",
"BUTTON_ADD_ALIAS": "Afegeix un àlies",
"DELETE_ARE_YOU_SURE": "Voleu continuar?",
"HTML_DOMAINS_HELPER": "Llista de dominis als quals el correu web pot accedir.\n<br>\nFeu clic al nom d’un domini per configurar-lo.\n"
},
"TAB_SECURITY": {
"LEGEND_SECURITY": "Seguretat",
"LABEL_USE_IMAGE_PROXY": "Utilitza un servidor intermediari local per a les imatges externes",
"LABEL_AUTO_VERIFY": "Verifica les signatures automàticament",
"LABEL_SHOW_PHP_INFO": "Mostra la informació de PHP",
"LEGEND_ADMIN_PANEL_ACCESS_CREDENTIALS": "Credencials d’accés al tauler d’administració",
"LABEL_CURRENT_PASSWORD": "Contrasenya actual",
"LABEL_NEW_PASSWORD": "Contrasenya nova",
"LABEL_REPEAT_PASSWORD": "Repetiu la contrasenya",
"LABEL_REQUIRE_VERIFICATION": "Exigeix la verificació del certificat SSL utilitzat (IMAP / SMTP)",
"CRYPTO": "Criptografia",
"LABEL_ALLOW_SELF_SIGNED": "Permet els certificats autosignats"
},
"TAB_PACKAGES": {
"LABEL_ENABLE_PLUGINS": "Activa les extensions",
"LEGEND_AVAILABLE_FOR_UPDATE": "Actualitzacions disponibles",
"LEGEND_AVAILABLE_FOR_INSTALLATION": "Extensions disponibles per instal·lar",
"LEGEND_INSTALLED_PACKAGES": "Paquets instal·lats",
"ALERT_CANNOT_ACCESS_REPOSITORY": "Ara no es pot accedir al repositori."
},
"TAB_ABOUT": {
"LEGEND_ABOUT": "Quant a",
"LABEL_TAG_HINT": "Client de correu electrònic senzill, modern i ràpid basat en la web",
"LABEL_ALL_RIGHTS_RESERVED": "Tots els drets reservats.",
"HINT_READ_CHANGE_LOG": "Llegiu el registre de canvis abans d’actualitzar.",
"HINT_IS_UP_TO_DATE": "SnappyMail està actualitzat.",
"HTML_NEW_VERSION": "Hi ha disponible una versió nova: <b>%VERSION%</b>.",
"LABEL_UPDATING": "S’està actualitzant…",
"LABEL_CHECKING": "S’està comprovant si hi ha actualitzacions…",
"BUTTON_RELEASES": "Versions publicades"
},
"POPUPS_DOMAIN_ALIAS": {
"TITLE_ADD_DOMAIN_ALIAS": "Afegeix un àlies",
"LABEL_ALIAS": "Àlies",
"LABEL_DOMAIN": "Domini"
},
"POPUPS_DOMAIN": {
"TITLE_ADD_DOMAIN": "Afegeix un domini",
"TITLE_ADD_DOMAIN_WITH_NAME": "Afegeix el domini «%NAME%»",
"TITLE_EDIT_DOMAIN": "Edita el domini «%NAME%»",
"LABEL_NAME": "Nom",
"NAME_HELPER": "S’admet el comodí *",
"LABEL_SERVER": "Servidor",
"LABEL_PORT": "Port",
"LABEL_SECURE": "Seguretat",
"TIMEOUT": "Temps d’espera",
"DISABLE_CAPABILITIES": "Desactiva capacitats",
"LIMITS": "Límits",
"FOLDERS": "Carpetes",
"MESSAGES": "Missatges",
"SECURE_OPTION_NONE": "Cap",
"LABEL_ALLOW_SIEVE_SCRIPTS": "Permet scripts Sieve",
"LABEL_USE_SHORT_LOGIN": "Utilitza el nom d’usuari sense el domini",
"LABEL_LOWERCASE_LOGIN": "Nom d’usuari en minúscules, sense distingir majúscules i minúscules",
"LABEL_USE_AUTH": "Utilitza l’autenticació",
"LABEL_SET_SENDER": "Utilitza el nom d’usuari com a remitent",
"LABEL_AUTH_PLAIN_LINE": "Força l’autenticació amb una sola ordre AUTH PLAIN",
"LABEL_USE_PHP_MAIL": "Utilitza la funció mail() de PHP",
"BUTTON_WHITE_LIST": "Usuaris permesos",
"NEW_DOMAIN_DESC": "Aquesta configuració de domini us permetrà treballar amb les adreces de correu de <strong>%NAME%</strong>.\n",
"WHITE_LIST_ALERT": "Llista d’usuaris del domini que poden accedir al correu web.\nSepareu-los amb espais.\n",
"VALID_SPAM_HEADERS": "Capçaleres vàlides de correu brossa",
"VALID_VIRUS_HEADERS": "Capçaleres vàlides de virus"
},
"POPUPS_PLUGIN": {
"TITLE_PLUGIN": "Extensió",
"DESC_NOTHING_TO_CONFIGURE": "No hi ha res per configurar."
},
"POPUPS_ASK": {
"DESC_WANT_CLOSE_THIS_WINDOW": "Segur que voleu tancar aquesta finestra?"
},
"POPUPS_LANGUAGES": {
"TITLE_LANGUAGES": "Trieu la llengua"
},
"HINTS": {
"BETA": "beta",
"WARNING": "Avís!"
},
"ERRORS": {
"DOMAIN_ALREADY_EXISTS": "El domini ja existeix.",
"UNKNOWN_ERROR": "Error desconegut"
},
"NOTIFICATIONS": {
"RequestError": "La sol·licitud ha fallat",
"RequestAborted": "S’ha cancel·lat la sol·licitud",
"RequestTimeout": "S’ha esgotat el temps d’espera de la sol·licitud",
"InvalidToken": "Testimoni no vàlid",
"AuthError": "Ha fallat l’autenticació",
"DomainAlreadyExists": "El domini ja existeix.",
"CantSavePluginSettings": "No es pot desar la configuració.",
"CantInstallPackage": "No s’ha pogut instal·lar l’extensió",
"CantDeletePackage": "No s’ha pogut eliminar l’extensió",
"InvalidPluginPackage": "Paquet d’extensió no vàlid",
"UnsupportedPluginPackage": "Paquet d’extensió no compatible",
"UnknownError": "Error desconegut"
}
}

View file

@ -0,0 +1,75 @@
{
"long": {
"year": {
"past": {
"one": "fa {0} any",
"other": "fa {0} anys"
},
"future": {
"one": "d’aquí a {0} any",
"other": "d’aquí a {0} anys"
}
},
"month": {
"past": {
"one": "fa {0} mes",
"other": "fa {0} mesos"
},
"future": {
"one": "d’aquí a {0} mes",
"other": "d’aquí a {0} mesos"
}
},
"week": {
"past": {
"one": "fa {0} setmana",
"other": "fa {0} setmanes"
},
"future": {
"one": "d’aquí a {0} setmana",
"other": "d’aquí a {0} setmanes"
}
},
"day": {
"past": {
"one": "fa {0} dia",
"other": "fa {0} dies"
},
"future": {
"one": "d’aquí a {0} dia",
"other": "d’aquí a {0} dies"
}
},
"hour": {
"past": {
"one": "fa {0} hora",
"other": "fa {0} hores"
},
"future": {
"one": "d’aquí a {0} hora",
"other": "d’aquí a {0} hores"
}
},
"minute": {
"past": {
"one": "fa {0} minut",
"other": "fa {0} minuts"
},
"future": {
"one": "d’aquí a {0} minut",
"other": "d’aquí a {0} minuts"
}
},
"second": {
"past": {
"one": "fa {0} segon",
"other": "fa {0} segons"
},
"future": {
"one": "d’aquí a {0} segon",
"other": "d’aquí a {0} segons"
}
}
},
plural: n => n === 1 ? "one" : "other"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

View file

@ -0,0 +1,9 @@
{
"BACK_LINK": "Torna a carregar",
"NO_SCRIPT_TITLE": "Aquesta aplicació necessita JavaScript.",
"NO_SCRIPT_DESC": "El navegador no té activat JavaScript.\nActiveu-lo a la configuració del navegador i torneu-ho a provar.",
"NO_COOKIE_TITLE": "Aquesta aplicació necessita galetes.",
"NO_COOKIE_DESC": "El navegador no té activades les galetes.\nActiveu-les a la configuració del navegador i torneu-ho a provar.",
"BAD_BROWSER_TITLE": "El navegador està obsolet.",
"BAD_BROWSER_DESC": "Per utilitzar totes les funcions de l’aplicació,\nbaixeu i instal·leu un navegador actualitzat."
}

View file

@ -0,0 +1,647 @@
{
"LANG_DIR": "ltr",
"GLOBAL": {
"TO_ARCHIVE": "Arxiva",
"ARE_YOU_SURE": "Voleu continuar?",
"BACK": "Enrere",
"BCC": "C/o",
"CANCEL": "Cancel·la",
"CC": "A/c",
"CLEAR": "Neteja",
"CLOSE": "Tanca",
"CONTACTS": "Contactes",
"DELETE": "Suprimeix",
"UNDELETE": "Restaura",
"DONE": "Fet",
"EMAIL": "Adreça de correu",
"FROM": "De",
"LOADING": "S’està carregant…",
"LOGOUT": "Tanca la sessió",
"MORE": "Més",
"MOVE_TO": "Mou a",
"COPY_TO": "Copia a",
"NAME": "Nom",
"NONE": "Cap",
"TO_SPAM": "Mou els missatges al correu brossa",
"NOT_SPAM": "No és correu brossa",
"USERNAME": "Nom d’usuari",
"PASSWORD": "Contrasenya",
"REMEMBER": "Recorda",
"REPLY_TO": "Respon a",
"SAVE": "Desa",
"SAVE_CHANGES": "Voleu desar els canvis?",
"SEARCH": "Cerca",
"SPAM": "Correu brossa",
"SUBJECT": "Assumpte",
"TEST": "Prova",
"TO": "Per a",
"SERVER_MESSAGE": "Missatge del servidor",
"YES": "Sí",
"NO": "No",
"INACTIVE": "Inactiu",
"DEFAULT": "predeterminat",
"HELP": "Ajuda",
"TEXT": "Text",
"ATTACHMENTS": "Adjunts"
},
"LOGIN": {
"LABEL_LOGIN": "Nom d’usuari",
"LABEL_SIGN_ME": "Recorda’m",
"BUTTON_SIGN_IN": "Inicia la sessió"
},
"TOP_TOOLBAR": {
"BUTTON_ADD_ACCOUNT": "Afegeix un compte"
},
"SEARCH": {
"TITLE_ADV": "Cerca avançada",
"TEXT": "Text",
"HAS_ATTACHMENT": "Té fitxers adjunts",
"FLAGGED": "Destacats",
"UNSEEN": "No llegits",
"DATE": "Data",
"DATE_ALL": "Qualsevol data",
"BEFORE_3_DAYS": "De fa més de 3 dies",
"BEFORE_7_DAYS": "De fa més d’una setmana",
"BEFORE_MONTH": "De fa més d’un mes",
"BEFORE_3_MONTHS": "De fa més de 3 mesos",
"BEFORE_6_MONTHS": "De fa més de 6 mesos",
"BEFORE_YEAR": "De fa més d’un any",
"SINCE_3_DAYS": "Antiguitat de fins a 3 dies",
"SINCE_7_DAYS": "Antiguitat de fins a 1 setmana",
"SINCE_MONTH": "Antiguitat de fins a 1 mes",
"SINCE_3_MONTHS": "Antiguitat de fins a 3 mesos",
"SINCE_6_MONTHS": "Antiguitat de fins a 6 mesos",
"SINCE_YEAR": "Antiguitat de fins a 1 any",
"SUBFOLDERS": "Subcarpetes",
"SUBFOLDERS_NONE": "Cap",
"SUBFOLDERS_SUBTREE": "Totes",
"SUBFOLDERS_SUBTREE_ONE": "Un nivell",
"REPLIED": "Respostos",
"KEYWORD": "Etiqueta"
},
"FOLDER_LIST": {
"BUTTON_COMPOSE": "Escriu",
"BUTTON_NEW_MESSAGE": "Missatge nou",
"INBOX_NAME": "Bústia d’entrada",
"SENT_NAME": "Enviats",
"DRAFTS_NAME": "Esborranys",
"TRASH_NAME": "Paperera",
"ARCHIVE_NAME": "Arxiu"
},
"IMAP": {
"ACL_RIGHTS": "Permisos",
"ACL_A": "Administra",
"ACL_E": "Elimina definitivament",
"ACL_I": "Insereix missatges",
"ACL_K": "Crea subcarpetes",
"ACL_L": "Mostra els missatges",
"ACL_P": "Envia correu",
"ACL_R": "Selecciona una carpeta",
"ACL_S": "Etiqueta \\SEEN",
"ACL_T": "Etiqueta \\DELETED",
"ACL_W": "Altres etiquetes",
"ACL_X": "Elimina o canvia el nom de la carpeta"
},
"MESSAGE_LIST": {
"BUTTON_RELOAD": "Torna a carregar la llista de missatges",
"BUTTON_EMPTY_FOLDER": "Buida la carpeta",
"BUTTON_MULTY_FORWARD": "Reenvia com a fitxers adjunts",
"BUTTON_DELETE_WITHOUT_MOVE": "Suprimeix definitivament",
"MENU_SET_SEEN": "Marca com a llegit",
"MENU_SET_ALL_SEEN": "Marca’ls tots com a llegits",
"MENU_UNSET_SEEN": "Marca com a no llegit",
"MENU_SET_FLAG": "Destaca",
"MENU_UNSET_FLAG": "Deixa de destacar",
"MENU_SELECT_ALL": "Selecciona-ho tot",
"MENU_SELECT_INVERT": "Inverteix la selecció",
"MENU_SELECT_UNSEEN": "No llegits",
"MENU_SELECT_SEEN": "Llegits",
"MENU_SELECT_FLAGGED": "Destacats",
"MENU_SELECT_UNFLAGGED": "Sense destacar",
"DOWNLOAD_ALL_ATTACHMENTS": "Baixa tots els fitxers adjunts",
"EMPTY_LIST": "No hi ha missatges.",
"EMPTY_SEARCH_LIST": "Cap missatge coincideix amb la cerca.",
"SEARCH_RESULT_FOR": "Resultats de la cerca de «%SEARCH%»",
"BACK_TO_MESSAGE_LIST": "Torna a la llista de missatges",
"PUT_MESSAGE_HERE": "Deixeu anar el missatge aquí per veure’l a la llista.",
"TODAY_AT": "avui a les %TIME%",
"YESTERDAY_AT": "ahir a les %TIME%",
"NEW_MESSAGE_NOTIFICATION": "Teniu %COUNT% missatges nous.",
"QUOTA_SIZE": "S’utilitzen <strong>%SIZE% (%PROC%%)</strong> de <strong>%LIMIT%</strong>",
"SORT": "Ordena",
"NO_SORT": "El servidor de correu no permet ordenar els missatges",
"SORT_ARRIVAL_ASC": "Data de recepció ascendent",
"SORT_ARRIVAL_DESC": "Data de recepció descendent",
"SORT_DATE_ASC": "Data ascendent",
"SORT_DATE_DESC": "Data descendent",
"SORT_SIZE_ASC": "Mida ascendent",
"SORT_SIZE_DESC": "Mida descendent",
"SORT_SUBJECT_ASC": "Assumpte A–Z",
"SORT_SUBJECT_DESC": "Assumpte Z–A",
"SORT_FROM_ASC": "Remitent A–Z",
"SORT_FROM_DESC": "Remitent Z–A"
},
"MESSAGE": {
"BUTTON_EDIT": "Edita",
"BUTTON_UNSUBSCRIBE": "Cancel·la la subscripció a la llista",
"BUTTON_REPLY": "Respon",
"BUTTON_REPLY_ALL": "Respon a tothom",
"BUTTON_FORWARD": "Reenvia",
"BUTTON_FORWARD_AS_ATTACHMENT": "Reenvia com a fitxer adjunt",
"BUTTON_EDIT_AS_NEW": "Edita com a missatge nou",
"BUTTON_SHOW_IMAGES": "Mostra les imatges externes",
"BUTTON_NOTIFY_READ_RECEIPT": "El remitent ha demanat rebre una notificació quan llegiu aquest missatge.",
"BUTTON_IN_NEW_WINDOW": "Mostra en una finestra separada",
"BUTTON_THREAD_LIST": "Llista de missatges de la conversa",
"BUTTON_THREAD_PREV": "Anterior",
"BUTTON_THREAD_NEXT": "Següent",
"BUTTON_THREAD_MORE": "Més missatges",
"MENU_HEADERS": "Mostra les capçaleres del missatge",
"MENU_VIEW_ORIGINAL": "Mostra el codi font",
"HTML_VIEW": "Mostra el missatge en HTML",
"PLAIN_VIEW": "Mostra el missatge en text sense format",
"SWAP_COLORS": "Intercanvia el color de fons predeterminat",
"MENU_DOWNLOAD_ORIGINAL": "Baixa com a fitxer .eml",
"MENU_FILTER_SIMILAR": "Filtra missatges semblants",
"MENU_PRINT": "Imprimeix",
"EMPTY_SUBJECT_TEXT": "(Sense assumpte)",
"LABEL_DATE": "Data",
"LABEL_FROM_SHORT": "de",
"LABEL_TO_SHORT": "per a",
"PRINT_LABEL_ATTACHMENTS": "Adjunts",
"MESSAGE_VIEW_DESC": "Seleccioneu qualsevol missatge de la llista per veure’l aquí.",
"MESSAGE_VIEW_MOVE_DESC": "Feu clic al nom de la carpeta del tauler de l’esquerra per seleccionar la destinació.",
"LINK_DOWNLOAD_AS_ZIP": "Baixa en format ZIP",
"SPAM_SCORE": "Puntuació de correu brossa",
"HAS_VIRUS_WARNING": "AVÍS: s’ha detectat un virus",
"TAGS": "Etiquetes",
"NEW_TAG": "Etiqueta nova",
"ENABLE_TRACKING_LINKS": "Activa els enllaços de seguiment",
"TRACKING_ENABLED": "Ara es farà el seguiment dels enllaços quan hi feu clic."
},
"MESSAGE_TAGS": {
"$important": "Important",
"$todo": "Per fer",
"$label1": "Important",
"$label2": "Feina",
"$label3": "Personal",
"$label4": "Per fer",
"$label5": "Més endavant"
},
"READ_RECEIPT": {
"SUBJECT": "Confirmació de lectura (missatge mostrat) - %SUBJECT%",
"BODY": "Aquesta és una confirmació de lectura del missatge que vau enviar a %READ-RECEIPT%.\n\nNota: aquesta confirmació només indica que el missatge s’ha mostrat a l’ordinador del destinatari.\nNo garanteix que el destinatari hagi llegit o entès el contingut del missatge.\n"
},
"SUGGESTIONS": {
"SEARCHING_DESC": "S’està cercant…"
},
"CONTACTS": {
"TAB_CONTACT": "Contacte",
"TAB_LOCATIONS": "Ubicacions",
"TAB_BUSINESS": "Feina",
"LABEL_ORGANIZATION": "Organització",
"LABEL_DEPARTMENT": "Departament",
"LABEL_TITLE": "Càrrec",
"TAB_CRYPTO": "Criptografia",
"ASK": "Pregunta",
"NEVER": "Mai",
"ALWAYS": "Sempre",
"ALWAYS_IF_POSSIBLE": "Sempre que sigui possible",
"BUTTON_ADD_CONTACT": "Afegeix un contacte",
"BUTTON_CREATE_CONTACT": "Crea",
"BUTTON_IMPORT": "Importa (csv, vcf, vCard)",
"BUTTON_EXPORT_VCARD": "Exporta (vcf, vCard)",
"BUTTON_EXPORT_CSV": "Exporta (csv)",
"ERROR_IMPORT_FILE": "Error d’importació (format de fitxer no vàlid)",
"EMPTY_LIST": "Aquí no hi ha cap contacte",
"EMPTY_SEARCH": "No s’ha trobat cap contacte",
"CLEAR_SEARCH": "Neteja la cerca",
"CONTACT_VIEW_DESC": "Seleccioneu un contacte de la llista per veure’l aquí.",
"LABEL_BIRTHDAY": "Data de naixement",
"DISPLAY_NAME": "Nom per mostrar",
"LAST_NAME": "Cognoms",
"FIRST_NAME": "Nom",
"MIDDLE_NAME": "Segon nom",
"NAME_PREFIX": "Prefix del nom",
"NAME_SUFFIX": "Sufix del nom",
"NICK_NAME": "Àlies",
"LABEL_READ_ONLY": "Només de lectura",
"ADD_MENU_LABEL": "Afegeix",
"ADD_MENU_NICKNAME": "Àlies",
"ADD_MENU_NOTE": "Nota",
"ADD_MENU_PHONE": "Telèfon",
"ADD_MENU_URL": "URL",
"ADD_MENU_ADDRESS": "Adreça",
"ADD_MENU_BIRTHDAY": "Data de naixement",
"ADD_MENU_TAGS": "Etiquetes",
"BUTTON_SHARE_ALL": "Tothom",
"BUTTON_SYNC": "Sincronització (CardDAV)",
"SEND_TO_ALL_CONTACT_EMAILS": "Envia a totes les adreces del contacte"
},
"COMPOSE": {
"LINK_SHOW_INPUTS": "Mostra tots els camps",
"BUTTON_SEND": "Envia",
"BUTTON_MINIMIZE": "Minimitza",
"SAVED_TIME": "Desat a les %TIME%",
"SAVED_ERROR_ON_SEND": "El missatge s’ha enviat, però no s’ha desat a la carpeta de missatges enviats.",
"DISCARD_UNSAVED_DATA": "Voleu descartar les dades no desades?",
"ATTACH_FILES": "Adjunta fitxers",
"ATTACH_DROP_FILES_DESC": "Deixeu anar fitxers aquí",
"REPLY_MESSAGE_TITLE": "%DATETIME%, %EMAIL% va escriure",
"FORWARD_MESSAGE_TOP_TITLE": "-------- Missatge reenviat -------",
"FORWARD_MESSAGE_TOP_SENT": "Enviat",
"EMPTY_TO_ERROR_DESC": "Indiqueu almenys un destinatari.",
"NO_ATTACHMENTS_HERE_DESC": "No hi ha cap fitxer adjunt.",
"ATTACHMENTS_ERROR_DESC": "No s’han pogut pujar tots els fitxers adjunts.",
"ATTACHMENTS_UPLOAD_ERROR_DESC": "Encara s’estan pujant fitxers adjunts.",
"ALLOW_SPELLCHECK": "Activa el corrector ortogràfic del navegador",
"ALLOW_DRAFT_AUTOSAVE": "Desa l’esborrany automàticament",
"BUTTON_REQUEST_READ_RECEIPT": "Demana una confirmació de lectura",
"BUTTON_MARK_AS_IMPORTANT": "Marca com a important",
"BUTTON_REQUEST_DSN": "Demana una confirmació de lliurament",
"BUTTON_REQUIRE_TLS": "Exigeix un lliurament segur",
"ERROR_EMPTY_BODY": "El cos del missatge és buit"
},
"POPUPS_ASK": {
"EXIT_ARE_YOU_SURE": "Segur que voleu sortir?",
"DESC_WANT_CLOSE_THIS_WINDOW": "Segur que voleu tancar aquesta finestra?",
"DESC_WANT_DELETE_MESSAGES": "Segur que voleu suprimir els missatges?"
},
"POPUPS_LANGUAGES": {
"TITLE_LANGUAGES": "Trieu la llengua"
},
"POPUPS_ADD_ACCOUNT": {
"TITLE_ADD_ACCOUNT": "Afegeix un compte",
"BUTTON_ADD_ACCOUNT": "Afegeix-lo",
"TITLE_UPDATE_ACCOUNT": "Actualitza el compte"
},
"POPUPS_IDENTITY": {
"TITLE_ADD_IDENTITY": "Afegeix una identitat",
"TITLE_UPDATE_IDENTITY": "Actualitza la identitat",
"LABEL": "Etiqueta",
"BUTTON_ADD_IDENTITY": "Afegeix-la",
"SIGNATURE": "Signatura",
"LABEL_SIGNATURE_INSERT_BEFORE": "Insereix aquesta signatura abans del text citat en les respostes."
},
"POPUPS_CREATE_FOLDER": {
"TITLE_CREATE_FOLDER": "Crea una carpeta",
"LABEL_PARENT": "Carpeta superior",
"BUTTON_CREATE": "Crea-la",
"TITLE_CREATING_PROCESS": "S’està creant la carpeta…"
},
"POPUPS_CLEAR_FOLDER": {
"TITLE_CLEAR_FOLDER": "Voleu suprimir tots els missatges de la carpeta?",
"DANGER_DESC_WARNING": "Avís",
"DANGER_DESC_HTML_1": "Aquesta acció suprimirà definitivament tots els missatges de la carpeta <strong>%FOLDER%</strong>.",
"DANGER_DESC_HTML_2": "Un cop iniciat, el procés no es pot aturar ni cancel·lar.",
"TITLE_CLEARING_PROCESS": "S’està buidant la carpeta…"
},
"CRYPTO": {
"SIGN": "Signa",
"ENCRYPT": "Xifra",
"DECRYPT": "Desxifra",
"VERIFY": "Verifica",
"GOOD_SIGNATURE": "Signatura vàlida de %USER%",
"ERROR": "Error de %TYPE%: %ERROR%",
"CREATE_SELF_SIGNED": "Crea un certificat autosignat",
"VALID_UNTIL": "Vàlid fins al",
"PRIVATE_KEY": "Clau privada",
"CHANGE_PASS": "Canvia la contrasenya",
"CURRENT_PASS": "Contrasenya actual",
"NEW_PASS": "Contrasenya nova",
"ASK_CRYPTKEY_PASS": "La contrasenya d’accés ha canviat. Introduïu la contrasenya anterior per desxifrar les dades."
},
"OPENPGP": {
"POPUP_IMPORT_TITLE": "Importa una clau OpenPGP",
"POPUP_IMPORT_BUTTON": "Importa-la",
"POPUP_VIEW_TITLE": "Mostra una clau OpenPGP",
"POPUP_VIEW_BUTTON": "Selecciona",
"POPUP_GENERATE_TITLE": "Genera un parell de claus OpenPGP",
"POPUP_GENERATE_BUTTON": "Genera",
"LABEL_KEY_TYPE": "Tipus",
"SIGNED_MESSAGE": "Missatge signat amb OpenPGP",
"ENCRYPTED_MESSAGE": "Missatge xifrat amb OpenPGP",
"STORE_IN_GNUPG": "Desa al servidor amb GnuPG",
"STORE_PUBLIC_KEY_IN_GNUPG": "Desa la clau pública al servidor amb GnuPG",
"STORE_PRIVATE_KEY_IN_GNUPG": "Desa la clau privada al servidor amb GnuPG",
"BACKUP_ON_SERVER": "Desa al servidor amb xifratge",
"BACKUP_PUBLIC_KEY_ON_SERVER": "Desa una còpia de la clau pública al servidor",
"BACKUP_PRIVATE_KEY_ON_SERVER": "Desa una còpia de la clau privada al servidor"
},
"SMIME": {
"POPUP_IMPORT_TITLE": "Importa un certificat S/MIME",
"CERTIFICATE": "Certificat",
"CERTIFICATES": "Certificats S/MIME",
"SIGNED_MESSAGE": "Missatge signat amb S/MIME",
"ENCRYPTED_MESSAGE": "Missatge xifrat amb S/MIME",
"PRIVATE_KEY_OF": "Clau privada S/MIME de %EMAIL%"
},
"POPUPS_FILTER": {
"TITLE_CREATE_FILTER": "Crea un filtre",
"TITLE_EDIT_FILTER": "Edita el filtre",
"LEGEND_CONDITIONS": "Condicions",
"LEGEND_ACTIONS": "Accions",
"BUTTON_ADD_CONDITION": "Afegeix-hi una condició",
"SELECT_ACTION_MOVE_TO": "Mou a",
"SELECT_ACTION_FORWARD_TO": "Reenvia a",
"SELECT_ACTION_REJECT": "Rebutja",
"SELECT_ACTION_VACATION_MESSAGE": "Missatge de vacances",
"SELECT_ACTION_DISCARD": "Descarta-ho",
"SELECT_FIELD_RECIPIENTS": "Destinataris (Per a o A/c)",
"SELECT_FIELD_HEADER": "Capçalera",
"SELECT_FIELD_BODY": "Cos",
"SELECT_FIELD_SIZE": "Mida",
"SELECT_TYPE_CONTAINS": "Conté",
"SELECT_TYPE_NOT_CONTAINS": "No conté",
"SELECT_TYPE_MATCHES": "Coincideix amb (s’admeten * i ?)",
"SELECT_TYPE_NOT_MATCHES": "No coincideix amb (s’admeten * i ?)",
"SELECT_TYPE_REGEXP": "Expressió regular",
"SELECT_TYPE_NOT_REGEXP": "No coincideix amb l’expressió regular",
"SELECT_TYPE_EQUAL_TO": "Igual a",
"SELECT_TYPE_NOT_EQUAL_TO": "No igual a",
"SELECT_TYPE_OVER": "Més gran que",
"SELECT_TYPE_UNDER": "Més petit que",
"SELECT_TYPE_TEXT": "Text",
"SELECT_TYPE_RAW": "Sense processar",
"SELECT_MATCH_ANY": "Coincideix amb QUALSEVOL de les regles següents",
"SELECT_MATCH_ALL": "Coincideix amb TOTES les regles següents",
"MARK_AS_READ_LABEL": "Marca com a llegit",
"REPLY_INTERVAL_LABEL": "Interval de resposta (dies)",
"KEEP_LABEL": "Mantén-ho",
"STOP_LABEL": "Atura el processament de les regles següents",
"VACATION_SUBJECT_LABEL": "Assumpte (opcional)",
"VACATION_MESSAGE_LABEL": "Missatge",
"VACATION_RECIPIENTS_LABEL": "Destinataris (separats per comes)",
"REJECT_MESSAGE_LABEL": "Missatge de rebuig",
"ALL_INCOMING_MESSAGES_DESC": "Tots els missatges d’entrada"
},
"POPUPS_SIEVE_SCRIPT": {
"TITLE_CREATE": "Crea un script",
"TITLE_EDIT": "Edita l’script",
"BUTTON_ADD_FILTER": "Afegeix-hi un filtre",
"BUTTON_RAW_SCRIPT": "Utilitza un script d’usuari personalitzat",
"CAPABILITY_LABEL": "Capacitats",
"CHANGES_NEED_TO_BE_SAVED_DESC": "Cal desar aquests canvis al servidor."
},
"POPUPS_SYSTEM_FOLDERS": {
"TITLE_SYSTEM_FOLDERS": "Seleccioneu carpetes del sistema",
"SELECT_CHOOSE_ONE": "Trieu-ne una",
"SELECT_UNUSE_NAME": "No l’utilitzis",
"NOTIFICATION_SENT": "No heu seleccionat la carpeta del sistema «Enviats», on es desen els missatges enviats.\nSi no els voleu desar, seleccioneu l’opció «No l’utilitzis».\n",
"NOTIFICATION_DRAFTS": "No heu seleccionat la carpeta del sistema «Esborranys», on es desen els missatges mentre els redacteu.",
"NOTIFICATION_SPAM": "No heu seleccionat la carpeta del sistema «Correu brossa», on es mouen els missatges marcats com a correu brossa.\nSi els voleu suprimir definitivament, seleccioneu l’opció «No l’utilitzis».\n",
"NOTIFICATION_TRASH": "No heu seleccionat la carpeta del sistema «Paperera», on es mouen els missatges suprimits.\nSi els voleu suprimir definitivament, seleccioneu l’opció «No l’utilitzis».\n",
"NOTIFICATION_ARCHIVE": "No heu seleccionat la carpeta del sistema «Arxiu», on es desen els missatges arxivats."
},
"TITLES": {
"MAILBOX": "Bústia de correu",
"SETTINGS": "Configuració"
},
"UPLOAD": {
"ERROR_FILE_IS_TOO_BIG": "El fitxer és massa gran",
"ERROR_FILE_PARTIALLY_UPLOADED": "El fitxer només s’ha carregat parcialment a causa d’un error desconegut",
"ERROR_NO_FILE_UPLOADED": "No s’ha carregat cap fitxer",
"ERROR_MISSING_TEMP_FOLDER": "Manca el fitxer temporal",
"ERROR_ON_SAVING_FILE": "S’ha produït un error en desar el fitxer.",
"ERROR_FILE_TYPE": "Tipus de fitxer no vàlid",
"ERROR_UNKNOWN": "S’ha produït un error desconegut en pujar el fitxer."
},
"EDITOR": {
"PLAIN": "HTML <-> TEXT",
"SOURCE": "HTML <-> CODI FONT",
"FONTFAMILY": "Tipus de lletra",
"FONTSIZE": "Mida de la lletra",
"DIR": "Direcció del text",
"DIR_LTR": "D’esquerra a dreta",
"DIR_RTL": "De dreta a esquerra",
"DIR_AUTO": "Automàtica",
"TEXTCOLOR": "Color del text",
"BACKGROUNDCOLOR": "Color de fons",
"BOLD": "Negreta",
"ITALIC": "Cursiva",
"UNDERLINE": "Subratllat",
"STRIKE": "Ratllat",
"SUB": "Subíndex",
"SUP": "Superíndex",
"OL": "Llista numerada",
"UL": "Llista amb pics",
"QUOTE": "Citació",
"INDENTDECREASE": "Disminueix el sagnat",
"INDENTINCREASE": "Augmenta el sagnat",
"LINK": "Enllaç",
"IMAGEURL": "URL de la imatge",
"IMAGEUPLOAD": "Selecciona una imatge",
"UNDO": "Desfés",
"REDO": "Refés",
"REMOVESTYLE": "Elimina el format"
},
"SETTINGS_LABELS": {
"GENERAL": "General",
"CONTACTS": "Contactes",
"FOLDERS": "Carpetes",
"ACCOUNTS": "Comptes",
"FILTERS": "Filtres",
"SECURITY": "Seguretat",
"THEMES": "Temes"
},
"SETTINGS_FILTERS": {
"SIMPLE": "Simple",
"ADVANCED": "Avançat",
"BUTTON_ADD_SCRIPT": "Afegeix un script",
"SUBNAME_MOVE_TO": "Mou a «%FOLDER%»",
"SUBNAME_FORWARD_TO": "Reenvia a «%EMAIL%»",
"SUBNAME_REJECT": "Rebutja",
"SUBNAME_VACATION_MESSAGE": "Missatge de vacances",
"SUBNAME_DISCARD": "Descarta-ho"
},
"SETTINGS_SECURITY": {
"LEGEND_SECURITY": "Seguretat",
"LABEL_AUTOLOGOUT": "Tancament automàtic de sessió",
"FORGET_KEY_PASS": "Oblida la contrasenya de la clau privada",
"NEVER": "Mai"
},
"SETTINGS_GENERAL": {
"LANGUAGE": "Llengua",
"HOUR_FORMAT": "Format de l’hora",
"IDENTITY": "Identitat",
"LAYOUT": "Disposició",
"LAYOUT_NO_SPLIT": "Sense divisió",
"LAYOUT_VERTICAL_SPLIT": "Divisió vertical",
"LAYOUT_HORIZONTAL_SPLIT": "Divisió horitzontal",
"SHOW_NEXT_MESSAGE": "Mostra el missatge següent en moure o eliminar el missatge actual",
"EDITOR": "Editor de text predeterminat",
"EDITOR_HTML": "HTML",
"EDITOR_PLAIN": "Text sense format",
"VIEW_OPTIONS": "Opcions de visualització",
"USE_PREVIEW_PANE": "Utilitza la subfinestra de previsualització",
"HIDE_DELETED_MESSAGES": "Amaga els missatges marcats per eliminar",
"USE_CHECKBOXES_IN_LIST": "Mostra les caselles a la llista",
"USE_THREADS": "Agrupa els missatges en converses",
"THREAD_ALGORITHM": "Algorisme d’agrupació de converses",
"REPLY_SAME_FOLDER": "Desa les respostes a la carpeta del missatge al qual es respon",
"PREFER_HTML": "Prefereix HTML al text sense format",
"PREFER_HTML_INFO": "De vegades els missatges inclouen tots dos formats. Aquesta opció permet triar si es mostra la part HTML o la part de text sense format.",
"REMOVE_COLORS": "Elimina els colors de fons i del text del cos del missatge",
"ALLOW_STYLES": "Permet CSS amb <style>",
"MESSAGE_PER_PAGE": "Missatges per pàgina",
"CHECK_MAIL_INTERVAL": "Interval de comprovació del correu",
"MARK_MESSAGE_READ_AFTER": "Marca el missatge com a llegit després de",
"MSG_DEFAULT_ACTION": "Acció predeterminada",
"SECONDS": "segons",
"MINUTES": "minuts",
"NOTIFICATIONS": "Notificacions",
"SOUND_NOTIFICATION": "Notificació acústica",
"CHROME_NOTIFICATION_DESC": "Mostra notificacions emergents per als missatges nous",
"CHROME_NOTIFICATION_DESC_DENIED": "(El navegador les ha bloquejades)",
"COLLAPSE_BLOCKQUOTES": "Replega el text citat",
"MAX_QUOTE_LEVEL": "Nombre màxim de nivells visibles de text citat",
"LIST_INLINE_ATTACHMENTS": "Mostra a la llista els fitxers adjunts inserits al missatge",
"LIST_SIMPLE_ATTACHMENTS": "Llista simple de fitxers adjunts",
"LIST_MESSAGES_GROUPED": "Agrupa els missatges de la llista en ordenar per data o remitent",
"EXTERNAL_IMAGES": "Imatges externes",
"IMAGES_OPTION_ASK": "Pregunta sempre",
"IMAGES_OPTION_WHITELIST": "Permet les adreces de confiança; pregunta en els altres casos",
"IMAGES_OPTION_ALWAYS": "Mostra sempre",
"IMAGES_WHITELIST": "Adreces de confiança per mostrar imatges",
"MESSAGE_POPUP_WINDOW": "Obre les finestres emergents en una finestra nova en lloc d’una pestanya",
"MAILTO": "Registra com a gestor d’enllaços «mailto:»",
"HTML_TO_MD": "Converteix l’HTML a Markdown en lloc de text sense format"
},
"SETTINGS_CONTACTS": {
"LABEL_CONTACTS_AUTOSAVE": "Afegeix els destinataris a la llibreta d’adreces automàticament",
"LEGEND_CONTACTS_SYNC": "Sincronització remota (CardDAV)",
"LABEL_CONTACTS_SYNC_ENABLE": "Activa la sincronització remota",
"LABEL_CONTACTS_SYNC_SERVER": "Servidor",
"LABEL_CONTACTS_SYNC_AB_URL": "URL de la llibreta d’adreces",
"LABEL_CONTACTS_SYNC_USER": "Usuari",
"SYNC_READ": "Només de lectura"
},
"SETTINGS_THEMES": {
"LEGEND_THEMES": "Temes",
"LABEL_BACKGROUND": "Fons",
"LABEL_FONTS": "Tipus de lletra",
"BUTTON_UPLOAD_BACKGROUND_IMAGE": "Puja una imatge de fons (JPG, PNG)",
"ERROR_FILE_IS_TOO_BIG": "El fitxer és massa gran",
"ERROR_FILE_TYPE_ERROR": "Tipus de fitxer no vàlid (només JPG i PNG)",
"ERROR_UNKNOWN": "S’ha produït un error desconegut en pujar el fitxer."
},
"SETTINGS_FOLDERS": {
"LEGEND_FOLDERS": "Llista de carpetes",
"BUTTON_CREATE": "Crea una carpeta",
"BUTTON_SYSTEM": "Carpetes del sistema",
"BUTTON_SUBSCRIBE": "Subscriu-te a la carpeta",
"BUTTON_UNSUBSCRIBE": "Cancel·la la subscripció a la carpeta",
"LOADING_PROCESS": "S’està actualitzant la llista de carpetes…",
"CREATING_PROCESS": "S’està creant la carpeta…",
"DELETING_PROCESS": "S’està suprimint la carpeta…",
"RENAMING_PROCESS": "S’està canviant el nom de la carpeta…",
"TO_MANY_FOLDERS_DESC_1": "Teniu massa carpetes.",
"TO_MANY_FOLDERS_DESC_2": "Només n’hem mostrat una part per evitar problemes de rendiment.",
"HELP_DELETE_FOLDER": "Suprimeix la carpeta",
"HELP_SHOW_HIDE_FOLDER": "Mostra o amaga la carpeta",
"HELP_CHECK_FOR_NEW_MESSAGES": "Activa o desactiva la comprovació de missatges nous",
"HIDE_UNSUBSCRIBED": "Amaga les carpetes sense subscripció",
"UNHIDE_KOLAB_FOLDERS": "Mostra les carpetes Kolab ocultes",
"TYPE_CALENDAR": "Calendari",
"TYPE_CONFIGURATION": "Configuració",
"TYPE_CONTACTS": "Contactes",
"TYPE_FILES": "Fitxers",
"TYPE_JOURNAL": "Diari",
"TYPE_NOTES": "Notes",
"TYPE_TASKS": "Tasques"
},
"SETTINGS_ACCOUNTS": {
"LEGEND_ACCOUNTS": "Comptes",
"LEGEND_IDENTITIES": "Identitats",
"LEGEND_ACCOUNTS_AND_IDENTITIES": "Comptes i identitats",
"BUTTON_ADD_ACCOUNT": "Afegeix un compte",
"BUTTON_ADD_IDENTITY": "Afegeix una identitat",
"LOADING_PROCESS": "S’està actualitzant…",
"SHOW_UNREAD": "Mostra el nombre de missatges no llegits"
},
"SETTINGS_OPENPGP": {
"BUTTON_IMPORT_KEY": "Importa una clau",
"BUTTON_GENERATE_KEY_PAIR": "Genera un parell de claus",
"IMPORT_FROM_SERVER": "Importa del servidor",
"TITLE_PRIVATE": "Privat",
"TITLE_PUBLIC": "Públic",
"GET_MAILVELOPE": "Obteniu l’extensió Mailvelope per al navegador."
},
"SHORTCUTS_HELP": {
"LEGEND_SHORTCUTS_HELP": "Ajuda per a les dreceres de teclat",
"TAB_MAILBOX": "Bústia de correu",
"TAB_MESSAGE_LIST": "Llista de missatges",
"TAB_MESSAGE_VIEW": "Vista de missatges",
"TAB_COMPOSE": "Escriu",
"LABEL_OPEN_USER_DROPDOWN": "Obre el desplegable d’usuaris",
"LABEL_REPLY": "Respon",
"LABEL_REPLY_ALL": "Respon a tothom",
"LABEL_FORWARD": "Reenvia",
"LABEL_FORWARD_MULTIPLY": "Reenvia com a fitxers adjunts",
"LABEL_CHECK_ALL": "Selecciona tots els missatges",
"LABEL_DELETE": "Suprimeix",
"LABEL_OPEN_THREAD": "Obre la conversa seleccionada",
"LABEL_MOVE": "Mou",
"LABEL_READ": "Marca els missatges seleccionats com a llegits",
"LABEL_UNREAD": "Marca els missatges seleccionats com a no llegits",
"LABEL_IMPORTANT": "Destaca els missatges seleccionats",
"LABEL_CANCEL_SEARCH": "Cancel·la la cerca",
"LABEL_FULLSCREEN_ENTER": "Pantalla completa (disposició de previsualització)",
"LABEL_VIEW_MESSAGE_ENTER": "Mostra el missatge (sense disposició de previsualització)",
"LABEL_SWITCH_TO_MESSAGE": "Canvia el focus al missatge seleccionat",
"LABEL_SWITCH_TO_FOLDER_LIST": "Canvia el focus a la llista de carpetes",
"LABEL_FULLSCREEN_TOGGLE": "Commuta el mode de pantalla completa",
"LABEL_BLOCKQUOTES_TOGGLE": "Mostra o amaga el text citat dels missatges",
"LABEL_THREAD_NEXT": "Missatge següent de la conversa",
"LABEL_THREAD_PREV": "Missatge anterior de la conversa",
"LABEL_EXIT_FULLSCREEN": "Surt del mode de pantalla completa",
"LABEL_CLOSE_MESSAGE": "Tanca el missatge (sense disposició de previsualització)",
"LABEL_SWITCH_TO_LIST": "Torna el focus a la llista de missatges",
"LABEL_OPEN_COMPOSE_POPUP": "Obre la finestra de redacció",
"LABEL_MINIMIZE_COMPOSE_POPUP": "Minimitza la finestra de redacció",
"LABEL_OPEN_IDENTITIES_DROPDOWN": "Obre el desplegable d’identitats",
"LABEL_SAVE_MESSAGE": "Desa el missatge",
"LABEL_SEND_MESSAGE": "Envia el missatge",
"LABEL_CLOSE_COMPOSE": "Tanca la finestra de redacció"
},
"NOTIFICATIONS": {
"RequestError": "La sol·licitud ha fallat",
"RequestAborted": "S’ha cancel·lat la sol·licitud",
"RequestTimeout": "S’ha esgotat el temps d’espera de la sol·licitud",
"InvalidToken": "Testimoni no vàlid",
"AuthError": "Ha fallat l’autenticació",
"ConnectionError": "No es pot connectar amb el servidor.",
"DomainAlreadyExists": "El domini ja existeix.",
"DomainNotAllowed": "El domini no es permet.",
"AccountNotAllowed": "El compte no es permet.",
"CryptKeyError": "La contrasenya d’accés ha canviat",
"ContactsSyncError": "Error de sincronització dels contactes",
"CantGetMessageList": "No es pot obtenir la llista de missatges.",
"CantGetMessage": "No es pot obtenir el missatge.",
"CantDeleteMessage": "No es pot suprimir el missatge.",
"CantCopyMessage": "No s’ha pogut copiar el missatge",
"CantMoveMessage": "No es pot moure el missatge.",
"CantSaveMessage": "No es pot desar el missatge.",
"CantSendMessage": "No es pot enviar el missatge.",
"InvalidRecipients": "Destinataris no vàlids",
"CantSaveFilters": "No es poden desar els filtres.",
"CantGetFilters": "No es poden obtenir els filtres.",
"CantActivateFiltersScript": "No s’ha pogut activar l’script de filtres",
"CantDeleteFiltersScript": "No s’ha pogut eliminar l’script de filtres",
"FiltersAreNotCorrect": "Els filtres no són correctes.",
"CantCreateFolder": "No es pot crear la carpeta.",
"CantRenameFolder": "No es pot canviar el nom de la carpeta.",
"CantDeleteFolder": "No es pot suprimir la carpeta.",
"CantDeleteNonEmptyFolder": "No es pot suprimir una carpeta que no és buida.",
"CantSubscribeFolder": "No es pot activar la subscripció a la carpeta.",
"CantUnsubscribeFolder": "No es pot cancel·lar la subscripció a la carpeta.",
"DemoSendMessageError": "Per raons de seguretat, aquest compte no està autoritzat a enviar missatges a adreces de correu electrònic externes.",
"DemoAccountError": "Per raons de seguretat, aquest compte no té permís per a aquesta acció.",
"AccountAlreadyExists": "El compte ja existeix.",
"AccountDoesNotExist": "El compte no existeix.",
"AccountSwitchFailed": "No s’ha pogut canviar al compte «%EMAIL%».",
"MailServerError": "Hi ha hagut un error en accedir al servidor de correu.",
"InvalidInputArgument": "Argument d’entrada no vàlid",
"UnknownError": "Error desconegut"
}
}

View file

@ -100,7 +100,7 @@
"ACL_S": "\\SEEN značka", "ACL_S": "\\SEEN značka",
"ACL_T": "\\DELETED značka", "ACL_T": "\\DELETED značka",
"ACL_W": "Ostatní značky", "ACL_W": "Ostatní značky",
"ACL_X": "Smazat/Přejmenovat složku" "ACL_X": "Smazat\/Přejmenovat složku"
}, },
"MESSAGE_LIST": { "MESSAGE_LIST": {
"BUTTON_RELOAD": "Obnovit seznam zpráv", "BUTTON_RELOAD": "Obnovit seznam zpráv",
@ -130,6 +130,8 @@
"QUOTA_SIZE": "Používáte <strong>%SIZE% (%PROC%%)<\/strong> z vašich <strong>%LIMIT%<\/strong>", "QUOTA_SIZE": "Používáte <strong>%SIZE% (%PROC%%)<\/strong> z vašich <strong>%LIMIT%<\/strong>",
"SORT": "Seřadit", "SORT": "Seřadit",
"NO_SORT": "Server nepodporuje řazení", "NO_SORT": "Server nepodporuje řazení",
"SORT_ARRIVAL_ASC": "Arrived ascending",
"SORT_ARRIVAL_DESC": "Arrived descending",
"SORT_DATE_ASC": "Datum vzestupně", "SORT_DATE_ASC": "Datum vzestupně",
"SORT_DATE_DESC": "Datum sestupně", "SORT_DATE_DESC": "Datum sestupně",
"SORT_SIZE_ASC": "Velikost vzestupně", "SORT_SIZE_ASC": "Velikost vzestupně",
@ -466,7 +468,7 @@
"LAYOUT_NO_SPLIT": "Nerozdělovat", "LAYOUT_NO_SPLIT": "Nerozdělovat",
"LAYOUT_VERTICAL_SPLIT": "Vertikální rozdělení", "LAYOUT_VERTICAL_SPLIT": "Vertikální rozdělení",
"LAYOUT_HORIZONTAL_SPLIT": "Horizontální rozdělení", "LAYOUT_HORIZONTAL_SPLIT": "Horizontální rozdělení",
"SHOW_NEXT_MESSAGE": "Zobrazit další zprávu při přesunutí/odstranění aktuální zprávy", "SHOW_NEXT_MESSAGE": "Zobrazit další zprávu při přesunutí\/odstranění aktuální zprávy",
"EDITOR": "Přednastavený editor zpráv", "EDITOR": "Přednastavený editor zpráv",
"EDITOR_HTML": "Html", "EDITOR_HTML": "Html",
"EDITOR_PLAIN": "Text", "EDITOR_PLAIN": "Text",
@ -493,6 +495,7 @@
"CHROME_NOTIFICATION_DESC_DENIED": "(Zablokované prohlížečem)", "CHROME_NOTIFICATION_DESC_DENIED": "(Zablokované prohlížečem)",
"COLLAPSE_BLOCKQUOTES": "Sbalit citovaný text", "COLLAPSE_BLOCKQUOTES": "Sbalit citovaný text",
"MAX_QUOTE_LEVEL": "Maximální počet úrovní zobrazených citací", "MAX_QUOTE_LEVEL": "Maximální počet úrovní zobrazených citací",
"MIN_FONT_SIZE": "Minimum Font Size",
"LIST_INLINE_ATTACHMENTS": "Zobrazit vložené přílohy v seznamu", "LIST_INLINE_ATTACHMENTS": "Zobrazit vložené přílohy v seznamu",
"LIST_SIMPLE_ATTACHMENTS": "Jednoduchý seznam příloh", "LIST_SIMPLE_ATTACHMENTS": "Jednoduchý seznam příloh",
"LIST_MESSAGES_GROUPED": "Seskupit zprávy v seznamu (při řazení podle data nebo odesílatele)", "LIST_MESSAGES_GROUPED": "Seskupit zprávy v seznamu (při řazení podle data nebo odesílatele)",

View file

@ -495,6 +495,7 @@
"CHROME_NOTIFICATION_DESC_DENIED": "(Blokeret af browseren)", "CHROME_NOTIFICATION_DESC_DENIED": "(Blokeret af browseren)",
"COLLAPSE_BLOCKQUOTES": "Collapse quoted text", "COLLAPSE_BLOCKQUOTES": "Collapse quoted text",
"MAX_QUOTE_LEVEL": "Maximum visible quoted text levels", "MAX_QUOTE_LEVEL": "Maximum visible quoted text levels",
"MIN_FONT_SIZE": "Minimum Font Size",
"LIST_INLINE_ATTACHMENTS": "Show inline attachments in list", "LIST_INLINE_ATTACHMENTS": "Show inline attachments in list",
"LIST_SIMPLE_ATTACHMENTS": "Simple attachments list", "LIST_SIMPLE_ATTACHMENTS": "Simple attachments list",
"LIST_MESSAGES_GROUPED": "List messages grouped (when sorting by date or from)", "LIST_MESSAGES_GROUPED": "List messages grouped (when sorting by date or from)",

View file

@ -495,6 +495,7 @@
"CHROME_NOTIFICATION_DESC_DENIED": "(Vom Browser blockiert)", "CHROME_NOTIFICATION_DESC_DENIED": "(Vom Browser blockiert)",
"COLLAPSE_BLOCKQUOTES": "zitierten Text ausblenden", "COLLAPSE_BLOCKQUOTES": "zitierten Text ausblenden",
"MAX_QUOTE_LEVEL": "Maximal sichtbare Ebenen von zitiertem Text", "MAX_QUOTE_LEVEL": "Maximal sichtbare Ebenen von zitiertem Text",
"MIN_FONT_SIZE": "Minimum Font Size",
"LIST_INLINE_ATTACHMENTS": "Inline-Anhänge in der Liste anzeigen", "LIST_INLINE_ATTACHMENTS": "Inline-Anhänge in der Liste anzeigen",
"LIST_SIMPLE_ATTACHMENTS": "Vereinfachte Liste der Anhänge", "LIST_SIMPLE_ATTACHMENTS": "Vereinfachte Liste der Anhänge",
"LIST_MESSAGES_GROUPED": "Nachrichten bei Sortierung nach Datum oder Absender gruppieren", "LIST_MESSAGES_GROUPED": "Nachrichten bei Sortierung nach Datum oder Absender gruppieren",

View file

@ -495,6 +495,7 @@
"CHROME_NOTIFICATION_DESC_DENIED": "(Αποκλείστηκε από το φυλλομετρητή)", "CHROME_NOTIFICATION_DESC_DENIED": "(Αποκλείστηκε από το φυλλομετρητή)",
"COLLAPSE_BLOCKQUOTES": "Collapse quoted text", "COLLAPSE_BLOCKQUOTES": "Collapse quoted text",
"MAX_QUOTE_LEVEL": "Maximum visible quoted text levels", "MAX_QUOTE_LEVEL": "Maximum visible quoted text levels",
"MIN_FONT_SIZE": "Minimum Font Size",
"LIST_INLINE_ATTACHMENTS": "Show inline attachments in list", "LIST_INLINE_ATTACHMENTS": "Show inline attachments in list",
"LIST_SIMPLE_ATTACHMENTS": "Simple attachments list", "LIST_SIMPLE_ATTACHMENTS": "Simple attachments list",
"LIST_MESSAGES_GROUPED": "List messages grouped (when sorting by date or from)", "LIST_MESSAGES_GROUPED": "List messages grouped (when sorting by date or from)",

View file

@ -495,6 +495,7 @@
"CHROME_NOTIFICATION_DESC_DENIED": "(Blocked by the browser)", "CHROME_NOTIFICATION_DESC_DENIED": "(Blocked by the browser)",
"COLLAPSE_BLOCKQUOTES": "Collapse quoted text", "COLLAPSE_BLOCKQUOTES": "Collapse quoted text",
"MAX_QUOTE_LEVEL": "Maximum visible quoted text levels", "MAX_QUOTE_LEVEL": "Maximum visible quoted text levels",
"MIN_FONT_SIZE": "Minimum Font Size",
"LIST_INLINE_ATTACHMENTS": "Show inline attachments in list", "LIST_INLINE_ATTACHMENTS": "Show inline attachments in list",
"LIST_SIMPLE_ATTACHMENTS": "Simple attachments list", "LIST_SIMPLE_ATTACHMENTS": "Simple attachments list",
"LIST_MESSAGES_GROUPED": "List messages grouped (when sorting by date or from)", "LIST_MESSAGES_GROUPED": "List messages grouped (when sorting by date or from)",

View file

@ -495,6 +495,7 @@
"CHROME_NOTIFICATION_DESC_DENIED": "(Bloqueado por el navegador)", "CHROME_NOTIFICATION_DESC_DENIED": "(Bloqueado por el navegador)",
"COLLAPSE_BLOCKQUOTES": "Colapsar texto citado", "COLLAPSE_BLOCKQUOTES": "Colapsar texto citado",
"MAX_QUOTE_LEVEL": "Niveles máximos de texto citado visibles", "MAX_QUOTE_LEVEL": "Niveles máximos de texto citado visibles",
"MIN_FONT_SIZE": "Minimum Font Size",
"LIST_INLINE_ATTACHMENTS": "Mostrar adjuntos en línea en la lista", "LIST_INLINE_ATTACHMENTS": "Mostrar adjuntos en línea en la lista",
"LIST_SIMPLE_ATTACHMENTS": "Lista simple de adjuntos", "LIST_SIMPLE_ATTACHMENTS": "Lista simple de adjuntos",
"LIST_MESSAGES_GROUPED": "Listar mensajes agrupados (al ordenar por fecha o remitente)", "LIST_MESSAGES_GROUPED": "Listar mensajes agrupados (al ordenar por fecha o remitente)",

View file

@ -495,6 +495,7 @@
"CHROME_NOTIFICATION_DESC_DENIED": "(Veebilehitseja poolt blokeeritud)", "CHROME_NOTIFICATION_DESC_DENIED": "(Veebilehitseja poolt blokeeritud)",
"COLLAPSE_BLOCKQUOTES": "Collapse quoted text", "COLLAPSE_BLOCKQUOTES": "Collapse quoted text",
"MAX_QUOTE_LEVEL": "Maximum visible quoted text levels", "MAX_QUOTE_LEVEL": "Maximum visible quoted text levels",
"MIN_FONT_SIZE": "Minimum Font Size",
"LIST_INLINE_ATTACHMENTS": "Show inline attachments in list", "LIST_INLINE_ATTACHMENTS": "Show inline attachments in list",
"LIST_SIMPLE_ATTACHMENTS": "Simple attachments list", "LIST_SIMPLE_ATTACHMENTS": "Simple attachments list",
"LIST_MESSAGES_GROUPED": "List messages grouped (when sorting by date or from)", "LIST_MESSAGES_GROUPED": "List messages grouped (when sorting by date or from)",

View file

@ -495,6 +495,7 @@
"CHROME_NOTIFICATION_DESC_DENIED": "(Nabigatzaileak blokeatuta)", "CHROME_NOTIFICATION_DESC_DENIED": "(Nabigatzaileak blokeatuta)",
"COLLAPSE_BLOCKQUOTES": "Bildu aipuko testua", "COLLAPSE_BLOCKQUOTES": "Bildu aipuko testua",
"MAX_QUOTE_LEVEL": "Ageriko aipamen-maila maximoak", "MAX_QUOTE_LEVEL": "Ageriko aipamen-maila maximoak",
"MIN_FONT_SIZE": "Minimum Font Size",
"LIST_INLINE_ATTACHMENTS": "Erakutsi sareko eranskinak zerrendan", "LIST_INLINE_ATTACHMENTS": "Erakutsi sareko eranskinak zerrendan",
"LIST_SIMPLE_ATTACHMENTS": "Eranskinen zerrenda arrunta", "LIST_SIMPLE_ATTACHMENTS": "Eranskinen zerrenda arrunta",
"LIST_MESSAGES_GROUPED": "Zerrendatu mezuak taldekatuak (ordena jatorria edo data denean)", "LIST_MESSAGES_GROUPED": "Zerrendatu mezuak taldekatuak (ordena jatorria edo data denean)",

View file

@ -495,6 +495,7 @@
"CHROME_NOTIFICATION_DESC_DENIED": "(مسدود شده توسط مرورگر)", "CHROME_NOTIFICATION_DESC_DENIED": "(مسدود شده توسط مرورگر)",
"COLLAPSE_BLOCKQUOTES": "Collapse quoted text", "COLLAPSE_BLOCKQUOTES": "Collapse quoted text",
"MAX_QUOTE_LEVEL": "Maximum visible quoted text levels", "MAX_QUOTE_LEVEL": "Maximum visible quoted text levels",
"MIN_FONT_SIZE": "Minimum Font Size",
"LIST_INLINE_ATTACHMENTS": "Show inline attachments in list", "LIST_INLINE_ATTACHMENTS": "Show inline attachments in list",
"LIST_SIMPLE_ATTACHMENTS": "Simple attachments list", "LIST_SIMPLE_ATTACHMENTS": "Simple attachments list",
"LIST_MESSAGES_GROUPED": "List messages grouped (when sorting by date or from)", "LIST_MESSAGES_GROUPED": "List messages grouped (when sorting by date or from)",

View file

@ -495,6 +495,7 @@
"CHROME_NOTIFICATION_DESC_DENIED": "(Estetty selaimessa)", "CHROME_NOTIFICATION_DESC_DENIED": "(Estetty selaimessa)",
"COLLAPSE_BLOCKQUOTES": "Collapse quoted text", "COLLAPSE_BLOCKQUOTES": "Collapse quoted text",
"MAX_QUOTE_LEVEL": "Maximum visible quoted text levels", "MAX_QUOTE_LEVEL": "Maximum visible quoted text levels",
"MIN_FONT_SIZE": "Minimum Font Size",
"LIST_INLINE_ATTACHMENTS": "Show inline attachments in list", "LIST_INLINE_ATTACHMENTS": "Show inline attachments in list",
"LIST_SIMPLE_ATTACHMENTS": "Simple attachments list", "LIST_SIMPLE_ATTACHMENTS": "Simple attachments list",
"LIST_MESSAGES_GROUPED": "List messages grouped (when sorting by date or from)", "LIST_MESSAGES_GROUPED": "List messages grouped (when sorting by date or from)",

View file

@ -495,6 +495,7 @@
"CHROME_NOTIFICATION_DESC_DENIED": "(Bloqué par le navigateur)", "CHROME_NOTIFICATION_DESC_DENIED": "(Bloqué par le navigateur)",
"COLLAPSE_BLOCKQUOTES": "Réduire le texte cité", "COLLAPSE_BLOCKQUOTES": "Réduire le texte cité",
"MAX_QUOTE_LEVEL": "Niveau maximum de texte cité visible", "MAX_QUOTE_LEVEL": "Niveau maximum de texte cité visible",
"MIN_FONT_SIZE": "Minimum Font Size",
"LIST_INLINE_ATTACHMENTS": "Afficher les pièces jointes intégrées dans la liste", "LIST_INLINE_ATTACHMENTS": "Afficher les pièces jointes intégrées dans la liste",
"LIST_SIMPLE_ATTACHMENTS": "Liste simple des pièces jointes", "LIST_SIMPLE_ATTACHMENTS": "Liste simple des pièces jointes",
"LIST_MESSAGES_GROUPED": "Lister les messages groupés (lors du tri par Date ou Destinataire)", "LIST_MESSAGES_GROUPED": "Lister les messages groupés (lors du tri par Date ou Destinataire)",

View file

@ -495,6 +495,7 @@
"CHROME_NOTIFICATION_DESC_DENIED": "(a böngésző blokkolta)", "CHROME_NOTIFICATION_DESC_DENIED": "(a böngésző blokkolta)",
"COLLAPSE_BLOCKQUOTES": "Collapse quoted text", "COLLAPSE_BLOCKQUOTES": "Collapse quoted text",
"MAX_QUOTE_LEVEL": "Maximum visible quoted text levels", "MAX_QUOTE_LEVEL": "Maximum visible quoted text levels",
"MIN_FONT_SIZE": "Minimum Font Size",
"LIST_INLINE_ATTACHMENTS": "Show inline attachments in list", "LIST_INLINE_ATTACHMENTS": "Show inline attachments in list",
"LIST_SIMPLE_ATTACHMENTS": "Simple attachments list", "LIST_SIMPLE_ATTACHMENTS": "Simple attachments list",
"LIST_MESSAGES_GROUPED": "List messages grouped (when sorting by date or from)", "LIST_MESSAGES_GROUPED": "List messages grouped (when sorting by date or from)",

View file

@ -495,6 +495,7 @@
"CHROME_NOTIFICATION_DESC_DENIED": "(diblokir oleh browser)", "CHROME_NOTIFICATION_DESC_DENIED": "(diblokir oleh browser)",
"COLLAPSE_BLOCKQUOTES": "Collapse quoted text", "COLLAPSE_BLOCKQUOTES": "Collapse quoted text",
"MAX_QUOTE_LEVEL": "Maximum visible quoted text levels", "MAX_QUOTE_LEVEL": "Maximum visible quoted text levels",
"MIN_FONT_SIZE": "Minimum Font Size",
"LIST_INLINE_ATTACHMENTS": "Show inline attachments in list", "LIST_INLINE_ATTACHMENTS": "Show inline attachments in list",
"LIST_SIMPLE_ATTACHMENTS": "Simple attachments list", "LIST_SIMPLE_ATTACHMENTS": "Simple attachments list",
"LIST_MESSAGES_GROUPED": "List messages grouped (when sorting by date or from)", "LIST_MESSAGES_GROUPED": "List messages grouped (when sorting by date or from)",

View file

@ -495,6 +495,7 @@
"CHROME_NOTIFICATION_DESC_DENIED": "(Lokað af vafra)", "CHROME_NOTIFICATION_DESC_DENIED": "(Lokað af vafra)",
"COLLAPSE_BLOCKQUOTES": "Collapse quoted text", "COLLAPSE_BLOCKQUOTES": "Collapse quoted text",
"MAX_QUOTE_LEVEL": "Maximum visible quoted text levels", "MAX_QUOTE_LEVEL": "Maximum visible quoted text levels",
"MIN_FONT_SIZE": "Minimum Font Size",
"LIST_INLINE_ATTACHMENTS": "Show inline attachments in list", "LIST_INLINE_ATTACHMENTS": "Show inline attachments in list",
"LIST_SIMPLE_ATTACHMENTS": "Simple attachments list", "LIST_SIMPLE_ATTACHMENTS": "Simple attachments list",
"LIST_MESSAGES_GROUPED": "List messages grouped (when sorting by date or from)", "LIST_MESSAGES_GROUPED": "List messages grouped (when sorting by date or from)",

View file

@ -495,6 +495,7 @@
"CHROME_NOTIFICATION_DESC_DENIED": "(Bloccate dal browser)", "CHROME_NOTIFICATION_DESC_DENIED": "(Bloccate dal browser)",
"COLLAPSE_BLOCKQUOTES": "Nascondi testo quotato", "COLLAPSE_BLOCKQUOTES": "Nascondi testo quotato",
"MAX_QUOTE_LEVEL": "Livelli massimi visibilità testo quotato", "MAX_QUOTE_LEVEL": "Livelli massimi visibilità testo quotato",
"MIN_FONT_SIZE": "Minimum Font Size",
"LIST_INLINE_ATTACHMENTS": "Visualizza lista degli allegati in linea", "LIST_INLINE_ATTACHMENTS": "Visualizza lista degli allegati in linea",
"LIST_SIMPLE_ATTACHMENTS": "Lista semplice allegati", "LIST_SIMPLE_ATTACHMENTS": "Lista semplice allegati",
"LIST_MESSAGES_GROUPED": "Lista messaggi raggruppati (quando riordinati per data o mittente)", "LIST_MESSAGES_GROUPED": "Lista messaggi raggruppati (quando riordinati per data o mittente)",

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