Some big update

This commit is contained in:
2026-03-25 14:35:44 +01:00
parent 0abd37d7a5
commit 867da15823
111 changed files with 173994 additions and 2061 deletions

View File

@@ -53,7 +53,7 @@ class NotInternal implements Validator
return null;
}
if (!$this->hostCheck->isNotInternalHost($value)) {
if (!$this->hostCheck->isHostAndNotInternal($value)) {
return Failure::create();
}

View File

@@ -62,7 +62,7 @@ class NotInternal implements Validator
return null;
}
if (!$this->urlCheck->isNotInternalUrl($value)) {
if (!$this->urlCheck->isUrlAndNotIternal($value)) {
return Failure::create();
}

View File

@@ -178,7 +178,9 @@ class MassUpdate implements MassAction
private function clearRoleCache(string $id): void
{
$this->fileManager->removeFile('data/cache/application/acl/' . $id . '.php');
$part = basename($id);
$this->fileManager->removeFile("data/cache/application/acl/$part.php");
}
private function clearPortalRolesCache(): void

View File

@@ -76,7 +76,7 @@ class BeforeSaveValidateHosts implements SaveHook
return;
}
if (!$this->hostCheck->isNotInternalHost($host)) {
if (!$this->hostCheck->isHostAndNotInternal($host)) {
$message = $this->composeErrorMessage($host, $address);
throw new Forbidden($message);
@@ -97,7 +97,11 @@ class BeforeSaveValidateHosts implements SaveHook
$address = $host . ':' . $port;
if (!$this->hostCheck->isNotInternalHost($host)) {
if (in_array($address, $this->getAllowedAddressList())) {
return;
}
if (!$this->hostCheck->isHostAndNotInternal($host)) {
$message = $this->composeErrorMessage($host, $address);
throw new Forbidden($message);

View File

@@ -32,7 +32,7 @@ namespace Espo\Classes\TemplateHelpers;
use Espo\Core\Htmlizer\Helper;
use Espo\Core\Htmlizer\Helper\Data;
use Espo\Core\Htmlizer\Helper\Result;
use Michelf\MarkdownExtra as MarkdownTransformer;
use Espo\Core\Utils\Markdown\Markdown;
class MarkdownText implements Helper
{
@@ -44,7 +44,7 @@ class MarkdownText implements Helper
return Result::createEmpty();
}
$transformed = MarkdownTransformer::defaultTransform($value);
$transformed = Markdown::transform($value);
return Result::createSafeString($transformed);
}

View File

@@ -63,7 +63,7 @@ class Clearer
return;
}
$part = $user->getId() . '.php';
$part = basename($user->getId() . '.php');
$this->fileManager->remove('data/cache/application/acl/' . $part);
$this->fileManager->remove('data/cache/application/aclMap/' . $part);
@@ -77,7 +77,7 @@ class Clearer
->find();
foreach ($portals as $portal) {
$part = $portal->getId() . '/' . $user->getId() . '.php';
$part = basename($portal->getId()) . '/' . basename($user->getId() . '.php');
$this->fileManager->remove('data/cache/application/aclPortal/' . $part);
$this->fileManager->remove('data/cache/application/aclPortalMap/' . $part);

View File

@@ -115,7 +115,8 @@ class EspoUploadDir implements Storage, Local
protected function getFilePath(Attachment $attachment)
{
$sourceId = $attachment->getSourceId();
$file = basename($sourceId);
return 'data/upload/' . $sourceId;
return 'data/upload/' . $file;
}
}

View File

@@ -33,7 +33,7 @@ use Espo\Core\Formula\EvaluatedArgumentList;
use Espo\Core\Formula\Exceptions\BadArgumentType;
use Espo\Core\Formula\Exceptions\TooFewArguments;
use Espo\Core\Formula\Func;
use Michelf\Markdown;
use Espo\Core\Utils\Markdown\Markdown;
/**
* @noinspection PhpUnused
@@ -52,6 +52,6 @@ class TransformType implements Func
throw BadArgumentType::create(1, 'string');
}
return Markdown::defaultTransform($string);
return Markdown::transform($string);
}
}

View File

@@ -89,7 +89,7 @@ class Service
if (
$params->getHost() &&
!$this->addressUtil->isAllowedAddress($params) &&
!$this->hostCheck->isNotInternalHost($params->getHost())
!$this->hostCheck->isHostAndNotInternal($params->getHost())
) {
throw new Forbidden("Not allowed internal host.");
}
@@ -124,7 +124,7 @@ class Service
if (
$params->getHost() &&
!$this->addressUtil->isAllowedAddress($params) &&
!$this->hostCheck->isNotInternalHost($params->getHost())
!$this->hostCheck->isHostAndNotInternal($params->getHost())
) {
throw new Forbidden("Not allowed internal host.");
}

View File

@@ -103,7 +103,7 @@ class Service
if (
$params->getHost() &&
!$this->addressUtil->isAllowedAddress($params) &&
!$this->hostCheck->isNotInternalHost($params->getHost())
!$this->hostCheck->isHostAndNotInternal($params->getHost())
) {
throw new Forbidden("Not allowed internal host.");
}
@@ -144,7 +144,7 @@ class Service
if (
$params->getHost() &&
!$this->addressUtil->isAllowedAddress($params) &&
!$this->hostCheck->isNotInternalHost($params->getHost())
!$this->hostCheck->isHostAndNotInternal($params->getHost())
) {
throw new Forbidden("Not allowed host.");
}

View File

@@ -49,7 +49,9 @@ class Starter extends StarterBase
SystemConfig $systemConfig,
ApplicationState $applicationState
) {
$routeCacheFile = 'data/cache/application/slim-routes-portal-' . $applicationState->getPortalId() . '.php';
$part = basename($applicationState->getPortalId());
$routeCacheFile = 'data/cache/application/slim-routes-portal-' . $part . '.php';
parent::__construct(
$requestProcessor,

View File

@@ -0,0 +1,50 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2026 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\Utils\Markdown;
use Michelf\Markdown as MarkdownParser;
/**
* @internal
*/
class Markdown
{
/**
* @internal
*/
public static function transform(string $text): string
{
$parser = new MarkdownParser();
$parser->no_markup = true;
$parser->no_entities = true;
return $parser->transform($text);
}
}

View File

@@ -32,30 +32,35 @@ namespace Espo\Core\Utils\Security;
use const DNS_A;
use const FILTER_FLAG_NO_PRIV_RANGE;
use const FILTER_FLAG_NO_RES_RANGE;
use const FILTER_FLAG_HOSTNAME;
use const FILTER_VALIDATE_DOMAIN;
use const FILTER_VALIDATE_IP;
class HostCheck
{
public function isNotInternalHost(string $host): bool
/**
* Validates the string is a host and it's not internal.
* If not a host, returns false.
*
* @since 9.3.4
*/
public function isHostAndNotInternal(string $host): bool
{
$records = dns_get_record($host, DNS_A);
if (filter_var($host, FILTER_VALIDATE_IP)) {
return $this->ipAddressIsNotInternal($host);
}
if (!$records) {
return true;
if (!$this->isDomainHost($host)) {
return false;
}
foreach ($records as $record) {
/** @var ?string $idAddress */
$idAddress = $record['ip'] ?? null;
$ipAddresses = $this->getHostIpAddresses($host);
if (!$idAddress) {
return false;
}
if ($ipAddresses === []) {
return false;
}
foreach ($ipAddresses as $idAddress) {
if (!$this->ipAddressIsNotInternal($idAddress)) {
return false;
}
@@ -64,7 +69,66 @@ class HostCheck
return true;
}
private function ipAddressIsNotInternal(string $ipAddress): bool
/**
* @internal
* @since 9.3.4
*/
public function isDomainHost(string $host): bool
{
$normalized = $this->normalizeIpAddress($host);
if ($normalized !== false && filter_var($normalized, FILTER_VALIDATE_IP)) {
return false;
}
if (!filter_var($host, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME)) {
return false;
}
if (!$this->hasNoNumericItem($host)) {
return false;
}
if (filter_var($host, FILTER_VALIDATE_DOMAIN)) {
return true;
}
return false;
}
/**
* @return string[]
* @internal
* @since 9.3.4
*/
public function getHostIpAddresses(string $host): array
{
$records = dns_get_record($host, DNS_A);
if (!$records) {
return [];
}
$output = [];
foreach ($records as $record) {
/** @var ?string $idAddress */
$idAddress = $record['ip'] ?? null;
if (!$idAddress) {
continue;
}
$output[] = $idAddress;
}
return $output;
}
/**
* @internal
*/
public function ipAddressIsNotInternal(string $ipAddress): bool
{
return (bool) filter_var(
$ipAddress,
@@ -72,4 +136,90 @@ class HostCheck
FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE
);
}
/**
* @deprecated Since 9.3.4. Use `isHostAndNotInternal`.
* @todo Remove in 9.4.0.
*/
public function isNotInternalHost(string $host): bool
{
return $this->isHostAndNotInternal($host);
}
private function normalizeIpAddress(string $ip): string|false
{
if (!str_contains($ip, '.')) {
return self::normalizePart($ip);
}
$parts = explode('.', $ip);
if (count($parts) !== 4) {
return false;
}
$result = [];
foreach ($parts as $part) {
if (preg_match('/^0x[0-9a-f]+$/i', $part)) {
$num = hexdec($part);
} else if (preg_match('/^0[0-7]+$/', $part) && $part !== '0') {
$num = octdec($part);
} else if (ctype_digit($part)) {
$num = (int)$part;
} else {
return false;
}
if ($num < 0 || $num > 255) {
return false;
}
$result[] = $num;
}
return implode('.', $result);
}
private static function normalizePart(string $ip): string|false
{
if (preg_match('/^0x[0-9a-f]+$/i', $ip)) {
$num = hexdec($ip);
} elseif (preg_match('/^0[0-7]+$/', $ip) && $ip !== '0') {
$num = octdec($ip);
} elseif (ctype_digit($ip)) {
$num = (int) $ip;
} else {
return false;
}
if ($num < 0 || $num > 0xFFFFFFFF) {
return false;
}
$num = (int) $num;
return long2ip($num);
}
private function hasNoNumericItem(string $host): bool
{
$hasNoNumeric = false;
foreach (explode('.', $host) as $it) {
if (!is_numeric($it) && !self::isHex($it)) {
$hasNoNumeric = true;
break;
}
}
return $hasNoNumeric;
}
private function isHex(string $value): bool
{
return preg_match('/^0x[0-9a-fA-F]+$/', $value) === 1;
}
}

View File

@@ -29,9 +29,6 @@
namespace Espo\Core\Utils\Security;
use const FILTER_VALIDATE_URL;
use const PHP_URL_HOST;
class UrlCheck
{
public function __construct(
@@ -44,9 +41,11 @@ class UrlCheck
}
/**
* Checks whether a URL does not follow to an internal host.
* Checks whether it's a URL, and it does not follow to an internal host.
*
* @since 9.3.4
*/
public function isNotInternalUrl(string $url): bool
public function isUrlAndNotIternal(string $url): bool
{
if (!$this->isUrl($url)) {
return false;
@@ -58,6 +57,118 @@ class UrlCheck
return false;
}
return $this->hostCheck->isNotInternalHost($host);
return $this->hostCheck->isHostAndNotInternal($host);
}
/**
* @return ?string[] Null if not a domain name or not a URL.
* @internal
* @since 9.3.4
*/
public function getCurlResolve(string $url): ?array
{
if (!$this->isUrl($url)) {
return null;
}
$host = parse_url($url, PHP_URL_HOST);
$port = parse_url($url, PHP_URL_PORT);
$scheme = parse_url($url, PHP_URL_SCHEME);
if ($port === null && $scheme) {
$port = match (strtolower($scheme)) {
'http' => 80,
'https'=> 443,
'ftp' => 21,
'ssh' => 22,
'smtp' => 25,
default => null,
};
}
if ($port === null) {
return [];
}
if (!is_string($host)) {
return null;
}
if (filter_var($host, FILTER_VALIDATE_IP)) {
return null;
}
if (!$this->hostCheck->isDomainHost($host)) {
return null;
}
$ipAddresses = $this->hostCheck->getHostIpAddresses($host);
$output = [];
foreach ($ipAddresses as $ipAddress) {
$ipPart = $ipAddress;
if (filter_var($ipAddress, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
$ipPart = "[$ipPart]";
}
$output[] = "$host:$port:$ipPart";
}
return $output;
}
/**
* @deprecated Since 9.3.4. Use `isUrlAndNotIternal`.
* @todo Remove in 9.5.0.
*/
public function isNotInternalUrl(string $url): bool
{
return $this->isUrlAndNotIternal($url);
}
/**
* @param string[] $resolve
* @param string[] $allowed An allowed address list in the `{host}:{port}` format.
* @internal
*/
public function validateCurlResolveNotInternal(array $resolve, array $allowed = []): bool
{
if ($resolve === []) {
return false;
}
$ipAddresses = [];
foreach ($resolve as $item) {
$arr = explode(':', $item, 3);
if (count($arr) < 3) {
return false;
}
$ipAddress = $arr[2];
$port = $arr[1];
$domain = $arr[0];
if (in_array("$ipAddress:$port", $allowed) || in_array("$domain:$port", $allowed)) {
return true;
}
if (str_starts_with($ipAddress, '[') && str_ends_with($ipAddress, ']')) {
$ipAddress = substr($ipAddress, 1, -1);
}
$ipAddresses[] = $ipAddress;
}
foreach ($ipAddresses as $ipAddress) {
if (!$this->hostCheck->ipAddressIsNotInternal($ipAddress)) {
return false;
}
}
return true;
}
}

View File

@@ -124,7 +124,13 @@ class TemplateFileManager
?string $entityType = null
): string {
$type = basename($type);
$language = basename($language);
$name = basename($name);
if ($entityType) {
$entityType = basename($entityType);
return "custom/Espo/Custom/Resources/templates/{$type}/{$language}/{$entityType}/{$name}.tpl";
}
@@ -152,7 +158,13 @@ class TemplateFileManager
?string $entityType = null
): string {
$type = basename($type);
$language = basename($language);
$name = basename($name);
if ($entityType) {
$entityType = basename($entityType);
return "templates/{$type}/{$language}/{$entityType}/{$name}.tpl";
}

View File

@@ -95,11 +95,24 @@ class Sender
if (
!$this->addressUtil->isAllowedUrl($url) &&
!$this->urlCheck->isNotInternalUrl($url)
!$this->urlCheck->isUrlAndNotIternal($url)
) {
throw new Error("URL '$url' points to an internal host, not allowed.");
}
$resolve = $this->urlCheck->getCurlResolve($url);
if ($resolve === []) {
throw new Error("Could not resolve the host.");
}
/** @var string[] $allowedAddressList */
$allowedAddressList = $this->config->get('webhookAllowedAddressList') ?? [];
if ($resolve !== null && !$this->urlCheck->validateCurlResolveNotInternal($resolve, $allowedAddressList)) {
throw new Error("Forbidden host.");
}
$handler = curl_init($url);
if ($handler === false) {
@@ -118,6 +131,10 @@ class Sender
curl_setopt($handler, \CURLOPT_HTTPHEADER, $headerList);
curl_setopt($handler, \CURLOPT_POSTFIELDS, $payload);
if ($resolve) {
curl_setopt($handler, CURLOPT_RESOLVE, $resolve);
}
curl_exec($handler);
$code = curl_getinfo($handler, \CURLINFO_HTTP_CODE);

View File

@@ -95,7 +95,7 @@ class Attachment implements EntryPoint
$response
->setHeader('Content-Length', (string) $size)
->setHeader('Cache-Control', 'private, max-age=864000, immutable')
->setHeader('Content-Security-Policy', "default-src 'self'")
->setHeader('Content-Security-Policy', "default-src 'self'; script-src 'none'; object-src 'none';")
->setBody($stream);
}

View File

@@ -87,7 +87,7 @@ class Download implements EntryPoint
if (in_array($type, $inlineMimeTypeList)) {
$disposition = 'inline';
$response->setHeader('Content-Security-Policy', "default-src 'self'");
$response->setHeader('Content-Security-Policy', "default-src 'self'; script-src 'none'; object-src 'none';");
}
$response->setHeader('Content-Description', 'File Transfer');

View File

@@ -153,7 +153,7 @@ class Image implements EntryPoint
$response
->setHeader('Content-Disposition', 'inline;filename="' . $fileName . '"')
->setHeader('Content-Length', (string) $fileSize)
->setHeader('Content-Security-Policy', "default-src 'self'");
->setHeader('Content-Security-Policy', "default-src 'self'; script-src 'none'; object-src 'none';");
if (!$noCacheHeaders) {
$response->setHeader('Cache-Control', 'private, max-age=864000, immutable');
@@ -174,7 +174,9 @@ class Image implements EntryPoint
$sourceId = $attachment->getSourceId();
$cacheFilePath = "data/upload/thumbs/{$sourceId}_$size";
$file = basename("{$sourceId}_$size");
$cacheFilePath = "data/upload/thumbs/$file";
if ($useCache && $this->fileManager->isFile($cacheFilePath)) {
return $this->fileManager->getContents($cacheFilePath);

View File

@@ -89,7 +89,9 @@ class RemoveFile implements AfterRemove
$sizeList = array_keys($this->metadata->get(['app', 'image', 'sizes']) ?? []);
foreach ($sizeList as $size) {
$filePath = "data/upload/thumbs/{$entity->getSourceId()}_{$size}";
$file = basename("{$entity->getSourceId()}_$size");
$filePath = "data/upload/thumbs/$file";
if ($this->fileManager->isFile($filePath)) {
$this->fileManager->removeFile($filePath);

View File

@@ -1,380 +1,380 @@
{
"labels": {
"Enabled": "Povoleno",
"Disabled": "Zakázáno",
"System": "Systém",
"Users": "Uživatelé",
"Customization": "Přizpůsobení",
"Available Fields": "Dostupná pole",
"Layout": "Vzhled",
"Entity Manager": "Správa entit",
"Add Panel": "Přidat panel",
"Add Field": "Přidat pole",
"Settings": "Nastavení",
"Scheduled Jobs": "Naplánované akce",
"Upgrade": "Aktualizace",
"Clear Cache": "Vyčistit cache",
"Rebuild": "Přestavět",
"Teams": "Týmy",
"Roles": "Role",
"Portal": "Portál",
"Portals": "Portály",
"Portal Roles": "Role portálu",
"Outbound Emails": "Odchozí emaily",
"Group Email Accounts": "Skupinové e-mailové účty",
"Personal Email Accounts": "Osobní e-mailové účty",
"Inbound Emails": "Příchozí emaily",
"Email Templates": "Šablony emailů",
"Layout Manager": "Správa layoutu",
"User Interface": "Uživatelské rozhraní",
"Auth Tokens": "Autentizační tokeny",
"Authentication": "Autentizace",
"Currency": "Měna",
"Integrations": "Integrace",
"Extensions": "Rozšíření",
"Upload": "Nahrát",
"Installing...": "Instaluji...",
"Upgrading...": "Upgraduji...",
"Upgraded successfully": "Úspěšně upgradováno",
"Installed successfully": "Úspěšně nainstalováno",
"Ready for upgrade": "Připraveno k upgradu",
"Run Upgrade": "Spustit upgrade",
"Install": "Instalovat",
"Ready for installation": "Připraveno k instalaci",
"Uninstalling...": "Odebírám...",
"Uninstalled": "Odebráno",
"Create Entity": "Vytvořit entitu",
"Edit Entity": "Upravit entitu",
"Create Link": "Vytvořit vazbu",
"Edit Link": "Upravit link",
"Notifications": "Upozornění",
"Jobs": "Joby",
"Reset to Default": "Obnovit do základního nastavení",
"Email Filters": "E-mailové filtry",
"Portal Users": "Uživatelé portálu",
"Action History": "Historie akcí",
"Label Manager": "Správce labelů",
"Auth Log": "Log autentizace",
"Lead Capture": "Zachycení potenciálů",
"Attachments": "Přílohy",
"API Users": "API uživatelé",
"Template Manager": "Správce šablon",
"System Requirements": "Požadavky na systém",
"PHP Settings": "Nastavení PHP",
"Database Settings": "Nastavení databáze",
"Permissions": "Oprávnění",
"Success": "Úspěch",
"Fail": "Selhání",
"is recommended": "je doporučeno",
"extension is missing": "rozšíření chybí",
"PDF Templates": "PDF Šablony",
"Webhooks": "Webhooky",
"Dashboard Templates": "Šablony hlavních panelů",
"Email Addresses": "Emailové adresy",
"Phone Numbers": "Telefonní čísla",
"Layout Sets": "Sady vzhledů",
"Messaging": "Zprávy",
"Misc": "Vedlejší",
"Job Settings": "Nastavení jobů",
"Configuration Instructions": "Instrukce k nastavení",
"Formula Sandbox": "Pískoviště pro formula skripty",
"Working Time Calendars": "Kalendáře pracovní doby",
"Group Email Folders": "Složky skupinových e-mailů",
"Authentication Providers": "Poskytovatelé autentizace",
"Setup": "Nastavení",
"App Log": "Log aplikace",
"Address Countries": "Seznam zemí",
"App Secrets": "Tajemství aplikace",
"OAuth Providers": "OAuth poskytovatelé"
},
"layouts": {
"list": "Seznam",
"listSmall": "Seznam (malý)",
"detailSmall": "Detail (malý)",
"filters": "Vyhledávací filtry",
"massUpdate": "Hromadný update",
"relationships": "Vztah",
"sidePanelsDetail": "Boční panely (Detail)",
"sidePanelsEdit": "Boční panely (Upravit)",
"sidePanelsDetailSmall": "Boční panely (Detail malé)",
"sidePanelsEditSmall": "Boční panely (Upravit malé)",
"detailPortal": "Detail (Portál)",
"detailSmallPortal": "Detail (Small, Portál)",
"listSmallPortal": "Seznam malý (Portál)",
"listPortal": "Seznam (portál)",
"relationshipsPortal": "Panely vztahů (Portál)",
"defaultSidePanel": "Pole bočního panelu",
"bottomPanelsDetail": "Spodní panely",
"bottomPanelsEdit": "Spodní panely (Upravit)",
"bottomPanelsDetailSmall": "Spodní panely (Detail malé)",
"bottomPanelsEditSmall": "Spodní panely (Upravit malé)"
},
"fieldTypes": {
"address": "Adresa",
"array": "Pole",
"foreign": "Cizí pole",
"duration": "Trvání",
"password": "Heslo",
"personName": "Jméno osoby",
"autoincrement": "Číslo (automaticky zvyšované)",
"bool": "Ano/Ne",
"currency": "Měna",
"date": "Datum",
"enum": "Výběr",
"enumInt": "Výběr (číslo)",
"enumFloat": "Výběr (desetinné číslo)",
"float": "Číslo (desetinné)",
"link": "Vazba",
"linkMultiple": "Vazba (vícenásobná)",
"linkParent": "Vazba (rodič)",
"phone": "Telefon",
"url": "URL adresa",
"file": "Soubor",
"image": "Obrázek",
"multiEnum": "Výběr (vícenásobný)",
"attachmentMultiple": "Více příloh",
"rangeInt": "Rozsah (celé číslo)",
"rangeFloat": "Rozsah (desetinné číslo)",
"rangeCurrency": "Rozsah (měna)",
"wysiwyg": "WYSIWYG editor",
"map": "Mapa",
"currencyConverted": "Měna (převedená)",
"colorpicker": "Výběr barvy",
"int": "Číslo (celé)",
"number": "Číslo faktury",
"jsonArray": "JSON pole",
"jsonObject": "JSON objekt",
"datetime": "Datum a čas",
"datetimeOptional": "Datum/Datum a čas",
"checklist": "Ano/Ne (seznam)",
"linkOne": "Vazba (jednonásobná)",
"barcode": "Čárový kód",
"urlMultiple": "URL adresy (více)",
"base": "Výchozí",
"decimal": "Desetinné číslo"
},
"fields": {
"type": "Typ",
"name": "Jméno",
"label": "Popisek",
"required": "Povinné",
"default": "Výchozí",
"maxLength": "Maximální délka",
"options": "Možnosti",
"after": "Po (pole)",
"before": "Před (pole)",
"link": "Odkaz",
"field": "Pole",
"min": "Minimum",
"max": "Maximum",
"translation": "Překlad",
"previewSize": "Velikost náhledu",
"defaultType": "Výchozí typ",
"seeMoreDisabled": "Zakázat ořez textu",
"entityList": "Seznam entit",
"isSorted": "Je seřazeno (abecedně)",
"audited": "Auditováno",
"trim": "Oříznout",
"height": "Výška (px)",
"minHeight": "Minimální výška (px)",
"provider": "Poskytovatel",
"typeList": "Seznam typů",
"lengthOfCut": "Délka řezu",
"sourceList": "Seznam zdrojů",
"tooltipText": "Text nápovědy",
"prefix": "Předpona",
"nextNumber": "Další číslo",
"padLength": "Délka výplně",
"disableFormatting": "Zakázat formátování",
"dynamicLogicVisible": "Podmínky, za kterých je pole viditelné",
"dynamicLogicReadOnly": "Podmínky, za kterých je pole jenom pro čtení",
"dynamicLogicRequired": "Podmínky, za kterých je pole povinné",
"dynamicLogicOptions": "Podmíněné možnosti",
"probabilityMap": "Pravděpodobnosti fáze (%)",
"readOnly": "Pouze ke čtení",
"noEmptyString": "Neprázdný řetězec",
"maxFileSize": "Maximální velikost souboru (Mb)",
"isPersonalData": "Jsou osobní údaje",
"useIframe": "Použít iframe",
"useNumericFormat": "Použít číselný formát",
"strip": "Odstranit",
"cutHeight": "Oříznout výšku (px)",
"minuteStep": "Minutový krok",
"inlineEditDisabled": "Zakázat samostatnou úpravu",
"displayAsLabel": "Zobrazit jako štítek",
"allowCustomOptions": "Povolit vlastní možnosti",
"maxCount": "Maximální počet položek",
"displayRawText": "Zobrazit holý text (bez označení)",
"notActualOptions": "Neopravdové možnosti",
"accept": "Přijmout",
"displayAsList": "Zobrazit jako seznam",
"viewMap": "Zobrazit mapu",
"codeType": "Typ kódu",
"lastChar": "Poslední znak",
"listPreviewSize": "Velikost náhledu seznamu",
"onlyDefaultCurrency": "Pouze výchozí měna",
"dynamicLogicInvalid": "Podmínky, které pole dělají neplatným",
"conversionDisabled": "Konverze zakázána",
"decimalPlaces": "Počet desetinných míst",
"pattern": "Vzor",
"globalRestrictions": "Globální omezení",
"decimal": "Desetinné",
"optionsReference": "Odkaz na možnosti",
"copyToClipboard": "Tlačítko na zkopírování do schránky",
"rows": "Počet řádků textové oblasti",
"readOnlyAfterCreate": "Pouze ke čtení po vytvoření",
"createButton": "Tlačítko pro vytváření",
"autocompleteOnEmpty": "Doplňování při prázdném poli",
"relateOnImport": "Provázat při importu",
"aclScope": "Entita pro acl",
"onlyAdmin": "Pouze pro administrátory",
"activeOptions": "Aktivní možnosti",
"labelType": "Typ zobrazení",
"preview": "Náhled",
"attachmentField": "Pole pro přílohu",
"dynamicLogicReadOnlySaved": "Podmínky, za kterých je pole jenom pro čtení (po uložení)",
"notStorable": "Neuložitelné",
"itemsEditable": "Upravitelné položky"
},
"messages": {
"selectEntityType": "Vybrat entitu v levém menu.",
"selectUpgradePackage": "Vybrat upgrade balíček",
"selectLayout": "Vybrat požadovaný layout v levém menu a upravit ho.",
"selectExtensionPackage": "Vybrat soubor s rozšířením",
"extensionInstalled": "Rozšíření {name} {version} bylo nainstalováno.",
"installExtension": "Rozšíření {name} {version} je připraveno k instalaci.",
"upgradeBackup": "Doporučujeme zálohovat soubory a data EspoCRM před upgradem.",
"thousandSeparatorEqualsDecimalMark": "Oddělovač tisíců nemůže být stejný jako desetinný symbol.",
"userHasNoEmailAddress": "Uživatel nemá emailovou adresu.",
"uninstallConfirmation": "Opravdu odinstalovat vybrané rozšíření?",
"cronIsNotConfigured": "Naplánované úlohy nejsou spuštěny. Příchozí e-maily, oznámení a připomenutí proto nefungují. Postupujte podle [pokynů](https://www.espocrm.com/documentation/administration/server-configuration/#user-content-setup-a-crontab) k nastavení úlohy cron.",
"newExtensionVersionIsAvailable": "Je k dispozici nová verze {latestName} {latestVersion}.",
"upgradeVersion": "EspoCRM bude upgradováno na verzi <strong>{version}</strong>. Toto může chvíli trvat.",
"upgradeDone": "EspoCRM bylo upgradováno na verzi <strong>{version}</strong>.",
"downloadUpgradePackage": "Stáhnout upgradovací balíčky na [tomto]({url}) odkaze.",
"upgradeInfo": "Přečtěte si [dokumentaci]({url}) o tom, jak upgradovat instanci AutoCRM.",
"upgradeRecommendation": "Tento způsob upgradu se nedoporučuje. Je lepší upgradovat z CLI.",
"newVersionIsAvailable": "K dispozici je nová verze AutoCRM {latestVersion}. Při aktualizaci instance postupujte podle [pokynů](https://www.espocrm.com/documentation/administration/upgrading/).",
"formulaFunctions": "Funkce formula skriptů",
"rebuildRequired": "Musíte spustit znovu rebuild z CLI.",
"cronIsDisabled": "Cron je zakázán",
"cacheIsDisabled": "Cache je zakázána"
},
"descriptions": {
"settings": "Systémová nastavení aplikace.",
"scheduledJob": "Činnosti vykonávané CRONem.",
"upgrade": "Upgradovat EspoCRM.",
"clearCache": "Vyčistit veškerou cache.",
"rebuild": "Přestavět backend a vyčistit cache.",
"users": "Správa uživatelů.",
"teams": "Správa týmů.",
"roles": "Správa rolí.",
"portals": "Správa portálů.",
"portalRoles": "Role pro portál.",
"outboundEmails": "Nastavení SMTP pro odchozí emaily.",
"groupEmailAccounts": "Skupinové IMAP emailové účty. Import emailů",
"personalEmailAccounts": "E-mailové účty uživatelů.",
"emailTemplates": "Šablony pro odchozí emaily.",
"import": "Importovat data z CSV souboru.",
"layoutManager": "Přizpůsobit layouty (seznam, detail, upravit, hledat, hromadný update).",
"userInterface": "Nastavit uživatelské rozhraní.",
"authTokens": "Aktivní autentizační sessions. IP adresa a datum posledního přístupu.",
"authentication": "Nastavení autentizace.",
"currency": "Nastavení měn a kurzů.",
"extensions": "Instalovat a odebrat rozšíření.",
"integrations": "Integrace se službami třetích stran.",
"notifications": "Nastavení In-app a emailových upozornění.",
"inboundEmails": "Nastavení příchozích mailů",
"portalUsers": "Uživatelé portálu.",
"entityManager": "Vytvořit vlastní entity, úpravit existující. Správa polí a vztahů.",
"emailFilters": "E-mailové zprávy, které odpovídají zadanému filtru, nebudou importovány.",
"actionHistory": "Protokol akcí uživatelů.",
"labelManager": "Upravit popisky",
"authLog": "Historie přihlášení.",
"attachments": "Všechny přílohy souborů uložené v systému.",
"templateManager": "Přizpůsobte si šablony zpráv.",
"systemRequirements": "Systémové požadavky na AutoCRM.",
"apiUsers": "Oddělte uživatele pro účely integrace.",
"jobs": "Spustit akce na pozadí.",
"pdfTemplates": "Šablony pro tisk do PDF.",
"webhooks": "Správa webhooků.",
"dashboardTemplates": "Umožňuje přidávat dashboardy uživatelům.",
"phoneNumbers": "Všechna telefonní čísla uložená v systému.",
"emailAddresses": "Všechny e-mailové adresy uložené v systému.",
"layoutSets": "Kolekce layoutů, které lze přiřadit týmům a portálům.",
"jobsSettings": "Nastavení zpracování jobů. Joby vykonávají úkoly na pozadí.",
"sms": "Nastavení SMS.",
"formulaSandbox": "Pískoviště pro testování formula skriptů bez ukládání změn.",
"workingTimeCalendars": "Pracovní plány.",
"groupEmailFolders": "Složky sdílené pro týmy",
"authenticationProviders": "Další poskytovatelé autentizace pro portály.",
"appLog": "Log aplikace.",
"addressCountries": "Dostupné země pro políčka typu 'adresa'.",
"appSecrets": "Pro ukládání citlivých informací jako jsou API klíče, hesla, a jiná tajemství.",
"leadCapture": "Koncové body pro zachycení potenciálů a webové formuláře.",
"oAuthProviders": "OAuth poskytovatelé pro integrace."
},
"options": {
"previewSize": {
"x-small": "Extra-malý",
"small": "Malý",
"medium": "Střední",
"large": "Velký",
"": "Prázdné"
},
"labelType": {
"state": "Stav",
"regular": "Výchozí"
}
},
"logicalOperators": {
"and": "a zároveň",
"or": "nebo",
"not": "negace"
},
"systemRequirements": {
"requiredPhpVersion": "Požadovaná verze PHP",
"requiredMysqlVersion": "Požadovaná verze MySQL",
"host": "Jméno hostitele",
"dbname": "Název databáze",
"user": "Uživatel",
"writable": "Zapisovatelné",
"readable": "Čitelné",
"requiredMariadbVersion": "Požadovaná verze MariaDB",
"requiredPostgresqlVersion": "Požadovaná verze PostgreSQL"
},
"templates": {
"accessInfo": "Přístupové údaje",
"accessInfoPortal": "Přístupové údaje na portály",
"assignment": "Úkol",
"mention": "Zmínka",
"notePost": "Poznámka k příspěvku",
"notePostNoParent": "Poznámka k příspěvku (bez rodiče)",
"noteStatus": "Poznámka k aktualizaci stavu",
"passwordChangeLink": "Odkaz na změnu hesla",
"noteEmailReceived": "Poznámka o přijatém e-mailu",
"twoFactorCode": "Dvoufaktorový kód"
},
"strings": {
"rebuildRequired": "Rebuild je vyžadován."
},
"keywords": {
"settings": "nastavení",
"userInterface": "uživatelské rozhraní",
"scheduledJob": "naplánovaná akce",
"integrations": "integrace",
"authLog": "log autentizace",
"authTokens": "autentizační tokeny",
"entityManager": "správce entit",
"templateManager": "správce šablon",
"jobs": "úlohy",
"authentication": "autentizace",
"labelManager": "správce popisků",
"appSecrets": "tajemství aplikace",
"leadCapture": "zachycení potenciálů"
},
"tooltips": {
"tabUrl": "URL záložky",
"tabUrlAclScope": "ACL rozsah pro záložku URL"
}
}
{
"labels": {
"Enabled": "Povoleno",
"Disabled": "Zakázáno",
"System": "Systém",
"Users": "Uživatelé",
"Customization": "Přizpůsobení",
"Available Fields": "Dostupná pole",
"Layout": "Vzhled",
"Entity Manager": "Správa entit",
"Add Panel": "Přidat panel",
"Add Field": "Přidat pole",
"Settings": "Nastavení",
"Scheduled Jobs": "Naplánované akce",
"Upgrade": "Aktualizace",
"Clear Cache": "Vyčistit cache",
"Rebuild": "Přestavět",
"Teams": "Týmy",
"Roles": "Role",
"Portal": "Portál",
"Portals": "Portály",
"Portal Roles": "Role portálu",
"Outbound Emails": "Odchozí emaily",
"Group Email Accounts": "Skupinové e-mailové účty",
"Personal Email Accounts": "Osobní e-mailové účty",
"Inbound Emails": "Příchozí emaily",
"Email Templates": "Šablony emailů",
"Layout Manager": "Správa layoutu",
"User Interface": "Uživatelské rozhraní",
"Auth Tokens": "Autentizační tokeny",
"Authentication": "Autentizace",
"Currency": "Měna",
"Integrations": "Integrace",
"Extensions": "Rozšíření",
"Upload": "Nahrát",
"Installing...": "Instaluji...",
"Upgrading...": "Upgraduji...",
"Upgraded successfully": "Úspěšně upgradováno",
"Installed successfully": "Úspěšně nainstalováno",
"Ready for upgrade": "Připraveno k upgradu",
"Run Upgrade": "Spustit upgrade",
"Install": "Instalovat",
"Ready for installation": "Připraveno k instalaci",
"Uninstalling...": "Odebírám...",
"Uninstalled": "Odebráno",
"Create Entity": "Vytvořit entitu",
"Edit Entity": "Upravit entitu",
"Create Link": "Vytvořit vazbu",
"Edit Link": "Upravit link",
"Notifications": "Upozornění",
"Jobs": "Joby",
"Reset to Default": "Obnovit do základního nastavení",
"Email Filters": "E-mailové filtry",
"Portal Users": "Uživatelé portálu",
"Action History": "Historie akcí",
"Label Manager": "Správce labelů",
"Auth Log": "Log autentizace",
"Lead Capture": "Zachycení potenciálů",
"Attachments": "Přílohy",
"API Users": "API uživatelé",
"Template Manager": "Správce šablon",
"System Requirements": "Požadavky na systém",
"PHP Settings": "Nastavení PHP",
"Database Settings": "Nastavení databáze",
"Permissions": "Oprávnění",
"Success": "Úspěch",
"Fail": "Selhání",
"is recommended": "je doporučeno",
"extension is missing": "rozšíření chybí",
"PDF Templates": "PDF Šablony",
"Webhooks": "Webhooky",
"Dashboard Templates": "Šablony hlavních panelů",
"Email Addresses": "Emailové adresy",
"Phone Numbers": "Telefonní čísla",
"Layout Sets": "Sady vzhledů",
"Messaging": "Zprávy",
"Misc": "Vedlejší",
"Job Settings": "Nastavení jobů",
"Configuration Instructions": "Instrukce k nastavení",
"Formula Sandbox": "Pískoviště pro formula skripty",
"Working Time Calendars": "Kalendáře pracovní doby",
"Group Email Folders": "Složky skupinových e-mailů",
"Authentication Providers": "Poskytovatelé autentizace",
"Setup": "Nastavení",
"App Log": "Log aplikace",
"Address Countries": "Seznam zemí",
"App Secrets": "Tajemství aplikace",
"OAuth Providers": "OAuth poskytovatelé"
},
"layouts": {
"list": "Seznam",
"listSmall": "Seznam (malý)",
"detailSmall": "Detail (malý)",
"filters": "Vyhledávací filtry",
"massUpdate": "Hromadný update",
"relationships": "Vztah",
"sidePanelsDetail": "Boční panely (Detail)",
"sidePanelsEdit": "Boční panely (Upravit)",
"sidePanelsDetailSmall": "Boční panely (Detail malé)",
"sidePanelsEditSmall": "Boční panely (Upravit malé)",
"detailPortal": "Detail (Portál)",
"detailSmallPortal": "Detail (Small, Portál)",
"listSmallPortal": "Seznam malý (Portál)",
"listPortal": "Seznam (portál)",
"relationshipsPortal": "Panely vztahů (Portál)",
"defaultSidePanel": "Pole bočního panelu",
"bottomPanelsDetail": "Spodní panely",
"bottomPanelsEdit": "Spodní panely (Upravit)",
"bottomPanelsDetailSmall": "Spodní panely (Detail malé)",
"bottomPanelsEditSmall": "Spodní panely (Upravit malé)"
},
"fieldTypes": {
"address": "Adresa",
"array": "Pole",
"foreign": "Cizí pole",
"duration": "Trvání",
"password": "Heslo",
"personName": "Jméno osoby",
"autoincrement": "Číslo (automaticky zvyšované)",
"bool": "Ano/Ne",
"currency": "Měna",
"date": "Datum",
"enum": "Výběr",
"enumInt": "Výběr (číslo)",
"enumFloat": "Výběr (desetinné číslo)",
"float": "Číslo (desetinné)",
"link": "Vazba",
"linkMultiple": "Vazba (vícenásobná)",
"linkParent": "Vazba (rodič)",
"phone": "Telefon",
"url": "URL adresa",
"file": "Soubor",
"image": "Obrázek",
"multiEnum": "Výběr (vícenásobný)",
"attachmentMultiple": "Více příloh",
"rangeInt": "Rozsah (celé číslo)",
"rangeFloat": "Rozsah (desetinné číslo)",
"rangeCurrency": "Rozsah (měna)",
"wysiwyg": "WYSIWYG editor",
"map": "Mapa",
"currencyConverted": "Měna (převedená)",
"colorpicker": "Výběr barvy",
"int": "Číslo (celé)",
"number": "Číslo faktury",
"jsonArray": "JSON pole",
"jsonObject": "JSON objekt",
"datetime": "Datum a čas",
"datetimeOptional": "Datum/Datum a čas",
"checklist": "Ano/Ne (seznam)",
"linkOne": "Vazba (jednonásobná)",
"barcode": "Čárový kód",
"urlMultiple": "URL adresy (více)",
"base": "Výchozí",
"decimal": "Desetinné číslo"
},
"fields": {
"type": "Typ",
"name": "Jméno",
"label": "Popisek",
"required": "Povinné",
"default": "Výchozí",
"maxLength": "Maximální délka",
"options": "Možnosti",
"after": "Po (pole)",
"before": "Před (pole)",
"link": "Odkaz",
"field": "Pole",
"min": "Minimum",
"max": "Maximum",
"translation": "Překlad",
"previewSize": "Velikost náhledu",
"defaultType": "Výchozí typ",
"seeMoreDisabled": "Zakázat ořez textu",
"entityList": "Seznam entit",
"isSorted": "Je seřazeno (abecedně)",
"audited": "Auditováno",
"trim": "Oříznout",
"height": "Výška (px)",
"minHeight": "Minimální výška (px)",
"provider": "Poskytovatel",
"typeList": "Seznam typů",
"lengthOfCut": "Délka řezu",
"sourceList": "Seznam zdrojů",
"tooltipText": "Text nápovědy",
"prefix": "Předpona",
"nextNumber": "Další číslo",
"padLength": "Délka výplně",
"disableFormatting": "Zakázat formátování",
"dynamicLogicVisible": "Podmínky, za kterých je pole viditelné",
"dynamicLogicReadOnly": "Podmínky, za kterých je pole jenom pro čtení",
"dynamicLogicRequired": "Podmínky, za kterých je pole povinné",
"dynamicLogicOptions": "Podmíněné možnosti",
"probabilityMap": "Pravděpodobnosti fáze (%)",
"readOnly": "Pouze ke čtení",
"noEmptyString": "Neprázdný řetězec",
"maxFileSize": "Maximální velikost souboru (Mb)",
"isPersonalData": "Jsou osobní údaje",
"useIframe": "Použít iframe",
"useNumericFormat": "Použít číselný formát",
"strip": "Odstranit",
"cutHeight": "Oříznout výšku (px)",
"minuteStep": "Minutový krok",
"inlineEditDisabled": "Zakázat samostatnou úpravu",
"displayAsLabel": "Zobrazit jako štítek",
"allowCustomOptions": "Povolit vlastní možnosti",
"maxCount": "Maximální počet položek",
"displayRawText": "Zobrazit holý text (bez označení)",
"notActualOptions": "Neopravdové možnosti",
"accept": "Přijmout",
"displayAsList": "Zobrazit jako seznam",
"viewMap": "Zobrazit mapu",
"codeType": "Typ kódu",
"lastChar": "Poslední znak",
"listPreviewSize": "Velikost náhledu seznamu",
"onlyDefaultCurrency": "Pouze výchozí měna",
"dynamicLogicInvalid": "Podmínky, které pole dělají neplatným",
"conversionDisabled": "Konverze zakázána",
"decimalPlaces": "Počet desetinných míst",
"pattern": "Vzor",
"globalRestrictions": "Globální omezení",
"decimal": "Desetinné",
"optionsReference": "Odkaz na možnosti",
"copyToClipboard": "Tlačítko na zkopírování do schránky",
"rows": "Počet řádků textové oblasti",
"readOnlyAfterCreate": "Pouze ke čtení po vytvoření",
"createButton": "Tlačítko pro vytváření",
"autocompleteOnEmpty": "Doplňování při prázdném poli",
"relateOnImport": "Provázat při importu",
"aclScope": "Entita pro acl",
"onlyAdmin": "Pouze pro administrátory",
"activeOptions": "Aktivní možnosti",
"labelType": "Typ zobrazení",
"preview": "Náhled",
"attachmentField": "Pole pro přílohu",
"dynamicLogicReadOnlySaved": "Podmínky, za kterých je pole jenom pro čtení (po uložení)",
"notStorable": "Neuložitelné",
"itemsEditable": "Upravitelné položky"
},
"messages": {
"selectEntityType": "Vybrat entitu v levém menu.",
"selectUpgradePackage": "Vybrat upgrade balíček",
"selectLayout": "Vybrat požadovaný layout v levém menu a upravit ho.",
"selectExtensionPackage": "Vybrat soubor s rozšířením",
"extensionInstalled": "Rozšíření {name} {version} bylo nainstalováno.",
"installExtension": "Rozšíření {name} {version} je připraveno k instalaci.",
"upgradeBackup": "Doporučujeme zálohovat soubory a data EspoCRM před upgradem.",
"thousandSeparatorEqualsDecimalMark": "Oddělovač tisíců nemůže být stejný jako desetinný symbol.",
"userHasNoEmailAddress": "Uživatel nemá emailovou adresu.",
"uninstallConfirmation": "Opravdu odinstalovat vybrané rozšíření?",
"cronIsNotConfigured": "Naplánované úlohy nejsou spuštěny. Příchozí e-maily, oznámení a připomenutí proto nefungují. Postupujte podle [pokynů](https://www.espocrm.com/documentation/administration/server-configuration/#user-content-setup-a-crontab) k nastavení úlohy cron.",
"newExtensionVersionIsAvailable": "Je k dispozici nová verze {latestName} {latestVersion}.",
"upgradeVersion": "EspoCRM bude upgradováno na verzi <strong>{version}</strong>. Toto může chvíli trvat.",
"upgradeDone": "EspoCRM bylo upgradováno na verzi <strong>{version}</strong>.",
"downloadUpgradePackage": "Stáhnout upgradovací balíčky na [tomto]({url}) odkaze.",
"upgradeInfo": "Přečtěte si [dokumentaci]({url}) o tom, jak upgradovat instanci EspoCRM.",
"upgradeRecommendation": "Tento způsob upgradu se nedoporučuje. Je lepší upgradovat z CLI.",
"newVersionIsAvailable": "K dispozici je nová verze EspoCRM {latestVersion}. Při aktualizaci instance postupujte podle [pokynů](https://www.espocrm.com/documentation/administration/upgrading/).",
"formulaFunctions": "Funkce formula skriptů",
"rebuildRequired": "Musíte spustit znovu rebuild z CLI.",
"cronIsDisabled": "Cron je zakázán",
"cacheIsDisabled": "Cache je zakázána"
},
"descriptions": {
"settings": "Systémová nastavení aplikace.",
"scheduledJob": "Činnosti vykonávané CRONem.",
"upgrade": "Upgradovat EspoCRM.",
"clearCache": "Vyčistit veškerou cache.",
"rebuild": "Přestavět backend a vyčistit cache.",
"users": "Správa uživatelů.",
"teams": "Správa týmů.",
"roles": "Správa rolí.",
"portals": "Správa portálů.",
"portalRoles": "Role pro portál.",
"outboundEmails": "Nastavení SMTP pro odchozí emaily.",
"groupEmailAccounts": "Skupinové IMAP emailové účty. Import emailů",
"personalEmailAccounts": "E-mailové účty uživatelů.",
"emailTemplates": "Šablony pro odchozí emaily.",
"import": "Importovat data z CSV souboru.",
"layoutManager": "Přizpůsobit layouty (seznam, detail, upravit, hledat, hromadný update).",
"userInterface": "Nastavit uživatelské rozhraní.",
"authTokens": "Aktivní autentizační sessions. IP adresa a datum posledního přístupu.",
"authentication": "Nastavení autentizace.",
"currency": "Nastavení měn a kurzů.",
"extensions": "Instalovat a odebrat rozšíření.",
"integrations": "Integrace se službami třetích stran.",
"notifications": "Nastavení In-app a emailových upozornění.",
"inboundEmails": "Nastavení příchozích mailů",
"portalUsers": "Uživatelé portálu.",
"entityManager": "Vytvořit vlastní entity, úpravit existující. Správa polí a vztahů.",
"emailFilters": "E-mailové zprávy, které odpovídají zadanému filtru, nebudou importovány.",
"actionHistory": "Protokol akcí uživatelů.",
"labelManager": "Upravit popisky",
"authLog": "Historie přihlášení.",
"attachments": "Všechny přílohy souborů uložené v systému.",
"templateManager": "Přizpůsobte si šablony zpráv.",
"systemRequirements": "Systémové požadavky na EspoCRM.",
"apiUsers": "Oddělte uživatele pro účely integrace.",
"jobs": "Spustit akce na pozadí.",
"pdfTemplates": "Šablony pro tisk do PDF.",
"webhooks": "Správa webhooků.",
"dashboardTemplates": "Umožňuje přidávat dashboardy uživatelům.",
"phoneNumbers": "Všechna telefonní čísla uložená v systému.",
"emailAddresses": "Všechny e-mailové adresy uložené v systému.",
"layoutSets": "Kolekce layoutů, které lze přiřadit týmům a portálům.",
"jobsSettings": "Nastavení zpracování jobů. Joby vykonávají úkoly na pozadí.",
"sms": "Nastavení SMS.",
"formulaSandbox": "Pískoviště pro testování formula skriptů bez ukládání změn.",
"workingTimeCalendars": "Pracovní plány.",
"groupEmailFolders": "Složky sdílené pro týmy",
"authenticationProviders": "Další poskytovatelé autentizace pro portály.",
"appLog": "Log aplikace.",
"addressCountries": "Dostupné země pro políčka typu 'adresa'.",
"appSecrets": "Pro ukládání citlivých informací jako jsou API klíče, hesla, a jiná tajemství.",
"leadCapture": "Koncové body pro zachycení potenciálů a webové formuláře.",
"oAuthProviders": "OAuth poskytovatelé pro integrace."
},
"options": {
"previewSize": {
"x-small": "Extra-malý",
"small": "Malý",
"medium": "Střední",
"large": "Velký",
"": "Prázdné"
},
"labelType": {
"state": "Stav",
"regular": "Výchozí"
}
},
"logicalOperators": {
"and": "a zároveň",
"or": "nebo",
"not": "negace"
},
"systemRequirements": {
"requiredPhpVersion": "Požadovaná verze PHP",
"requiredMysqlVersion": "Požadovaná verze MySQL",
"host": "Jméno hostitele",
"dbname": "Název databáze",
"user": "Uživatel",
"writable": "Zapisovatelné",
"readable": "Čitelné",
"requiredMariadbVersion": "Požadovaná verze MariaDB",
"requiredPostgresqlVersion": "Požadovaná verze PostgreSQL"
},
"templates": {
"accessInfo": "Přístupové údaje",
"accessInfoPortal": "Přístupové údaje na portály",
"assignment": "Úkol",
"mention": "Zmínka",
"notePost": "Poznámka k příspěvku",
"notePostNoParent": "Poznámka k příspěvku (bez rodiče)",
"noteStatus": "Poznámka k aktualizaci stavu",
"passwordChangeLink": "Odkaz na změnu hesla",
"noteEmailReceived": "Poznámka o přijatém e-mailu",
"twoFactorCode": "Dvoufaktorový kód"
},
"strings": {
"rebuildRequired": "Rebuild je vyžadován."
},
"keywords": {
"settings": "nastavení",
"userInterface": "uživatelské rozhraní",
"scheduledJob": "naplánovaná akce",
"integrations": "integrace",
"authLog": "log autentizace",
"authTokens": "autentizační tokeny",
"entityManager": "správce entit",
"templateManager": "správce šablon",
"jobs": "úlohy",
"authentication": "autentizace",
"labelManager": "správce popisků",
"appSecrets": "tajemství aplikace",
"leadCapture": "zachycení potenciálů"
},
"tooltips": {
"tabUrl": "URL záložky",
"tabUrlAclScope": "ACL rozsah pro záložku URL"
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,313 +1,313 @@
{
"fields": {
"useCache": "Použít cache",
"dateFormat": "Formát data",
"timeFormat": "Formát času",
"timeZone": "Časové pásmo",
"weekStart": "První den v týdnu",
"thousandSeparator": "Oddělovač tisíců",
"decimalMark": "Desetinný oddělovač",
"defaultCurrency": "Výchozí měna",
"baseCurrency": "Bázová měna",
"currencyRates": "Kurzy měn",
"currencyList": "Seznam měn",
"language": "Jazyk",
"companyLogo": "Logo společnosti",
"ldapPort": "LDAP Port",
"ldapAuth": "LDAP Auth",
"ldapSecurity": "Zabezpečení",
"ldapPassword": "Heslo",
"outboundEmailFromName": "Od (jméno)",
"outboundEmailIsShared": "Sdílení",
"recordsPerPage": "Záznamy na stránku",
"recordsPerPageSmall": "Záznamy na stránku (malý)",
"tabList": "Seznam záložek",
"quickCreateList": "Rychlé odkazy",
"exportDelimiter": "Export oddělovač",
"globalSearchEntityList": "Seznam entit globálního vyhledávání",
"authenticationMethod": "Autentizační metoda",
"ldapHost": "LDAP Host",
"ldapAccountCanonicalForm": "LDAP Account Canonical Form",
"ldapAccountDomainName": "Název domény účtu",
"ldapTryUsernameSplit": "Zkuste rozdělit uživatelské jméno",
"ldapCreateEspoUser": "Vytvořit uživatele v EspoCRM",
"ldapUserLoginFilter": "Filtr uživatelského přihlášení",
"ldapAccountDomainNameShort": "Account Domain Name krátké",
"ldapOptReferrals": "Volit doporučení",
"exportDisabled": "Zakázat export (povolen pouze správce)",
"b2cMode": "Režm B2C",
"avatarsDisabled": "Zakázat avatary",
"displayListViewRecordCount": "Zobrazit celkový počet (v zobrazení seznamu)",
"theme": "Téma",
"userThemesDisabled": "Zakázat uživatelské motivy",
"emailMessageMaxSize": "Maximální velikost emailu (Mb)",
"personalEmailMaxPortionSize": "Maximální velikost emailové části pro načítání osobních účtů",
"inboundEmailMaxPortionSize": "Maximální velikost emailové části pro načítání skupinových účtů",
"authTokenLifetime": "Životnost ověřovacího tokenu (hodiny)",
"authTokenMaxIdleTime": "Maximální doba nečinnosti ověřovacího tokenu (hodiny)",
"dashboardLayout": "Rozvržení Dashboardu (výchozí)",
"siteUrl": "URL stránky",
"addressPreview": "Náhled adresy",
"addressFormat": "Formát adresy",
"notificationSoundsDisabled": "Zakázat zvuky oznámení",
"applicationName": "Název aplikace",
"ldapUsername": "Uživatelské jméno",
"ldapBindRequiresDn": "Přiřazení vyžaduje Dn",
"ldapBaseDn": "Bázové Dn",
"ldapUserNameAttribute": "Atribut uživatelského jména",
"ldapUserObjectClass": "Třída objektu uživatele",
"ldapUserTitleAttribute": "Atribut názvu uživatele",
"ldapUserFirstNameAttribute": "Atribut křestního jména uživatele",
"ldapUserLastNameAttribute": "Atribut příjmení uživatele",
"ldapUserEmailAddressAttribute": "Atribut emailové adresy uživatele",
"ldapUserTeams": "Týmy uživatele",
"ldapUserDefaultTeam": "Výchozí tým uživatele",
"ldapUserPhoneNumberAttribute": "Atribut telefonního čísla uživatele",
"assignmentNotificationsEntityList": "Entity k upozornění podle přiřazení",
"assignmentEmailNotifications": "Poslat emailová upozornění podle přiřazení",
"assignmentEmailNotificationsEntityList": "Entity k upozornění emailem podle přiřazení",
"streamEmailNotifications": "Oznámení o aktualizacích ve streamu pro interní uživatele",
"portalStreamEmailNotifications": "Oznámení o aktualizacích ve streamu pro uživatele portálu",
"streamEmailNotificationsEntityList": "Rozsahy emailových oznámení o streamu",
"calendarEntityList": "Seznam entit kalendáře",
"mentionEmailNotifications": "Zasílejte emailová oznámení o nových příspěvcích",
"massEmailDisableMandatoryOptOutLink": "Zakázat povinný odkaz pro odhlášení",
"activitiesEntityList": "Seznam entit aktivit",
"historyEntityList": "Seznam entit historie",
"currencyFormat": "Formát měny",
"currencyDecimalPlaces": "Počet desetinných míst měny",
"followCreatedEntities": "Sledovat vytvořené entity",
"aclAllowDeleteCreated": "Povolit odebrání vytvořených záznamů",
"adminNotifications": "Systémová oznámení v administračním panelu",
"adminNotificationsNewVersion": "Zobrazit oznámení, až bude k dispozici nová verze CRM",
"massEmailMaxPerHourCount": "Maximální počet e-mailů odeslaných za hodinu",
"maxEmailAccountCount": "Maximální počet osobních emailových účtů na uživatele",
"streamEmailNotificationsTypeList": "Na co upozorňovat",
"authTokenPreventConcurrent": "Pouze jeden ověřovací token na uživatele",
"scopeColorsDisabled": "Zakázat barvy rozsahu",
"tabColorsDisabled": "Zakázat barvy záložek",
"tabIconsDisabled": "Zakázat ikony na kartě",
"textFilterUseContainsForVarchar": "Při filtrování polí varchar používat operátor „obsahuje“",
"emailAddressIsOptedOutByDefault": "Označit nové emailové adresy jako odhlášené",
"outboundEmailBccAddress": "Adresa BCC pro externí klienty",
"adminNotificationsNewExtensionVersion": "Zobrazit oznámení, když jsou k dispozici nové verze rozšíření",
"cleanupDeletedRecords": "Vyčistit smazané záznamy",
"ldapPortalUserLdapAuth": "Pro uživatele portálu použijte ověřování LDAP",
"ldapPortalUserPortals": "Výchozí portály pro uživatele portálu",
"ldapPortalUserRoles": "Výchozí role pro uživatele portálu",
"fiscalYearShift": "Začátek fiskálního roku",
"jobRunInParallel": "Úlohy běží paralelně",
"jobMaxPortion": "Maximální velikost části úloh",
"jobPoolConcurrencyNumber": "Číslo souběhu úloh",
"daemonInterval": "Interval démona",
"daemonMaxProcessNumber": "Maximální počet procesů démona",
"daemonProcessTimeout": "Timeout procesu démona",
"addressCityList": "Seznam měst při našeptávání políčka adresa",
"addressStateList": "Seznam států pro našeptávání adres",
"cronDisabled": "Zakázat Cron",
"maintenanceMode": "Režim údržby",
"useWebSocket": "Použít WebSocket",
"emailNotificationsDelay": "Zpoždění e-mailových oznámení (v sekundách)",
"massEmailOpenTracking": "Sledování otevření emailů",
"passwordRecoveryDisabled": "Zakázat obnovení hesla",
"passwordRecoveryForAdminDisabled": "Zakázat obnovení hesla pro uživatele správce",
"passwordGenerateLength": "Délka vygenerovaných hesel",
"passwordStrengthLength": "Minimální délka hesla",
"passwordStrengthLetterCount": "Počet písmen požadovaných v hesle",
"passwordStrengthNumberCount": "Počet číslic požadovaných v hesle",
"passwordStrengthBothCases": "Zabraňte vystavení e-mailové adresy ve formuláři pro obnovení hesla",
"auth2FA": "Povolit dvoufaktorové ověřování",
"auth2FAMethodList": "Dostupné metody dvoufaktorové autorizace",
"personNameFormat": "Formát jména osoby",
"newNotificationCountInTitle": "Zobrazit nové číslo oznámení v názvu stránky",
"massEmailVerp": "Použít VERP",
"emailAddressLookupEntityTypeList": "Rozsahy vyhledávání emailových adres",
"busyRangesEntityList": "Seznam volných / zaneprázdněných entit",
"passwordRecoveryForInternalUsersDisabled": "Zakázat obnovení hesla pro uživatele",
"passwordRecoveryNoExposure": "Zabraňte vystavení emailové adresy ve formuláři pro obnovení hesla",
"auth2FAForced": "Přimět uživatele k nastavení dvoufaktorové autorizace",
"smsProvider": "Poskytovatel SMS",
"outboundSmsFromNumber": "SMS z čísla",
"recordsPerPageSelect": "Záznamy na stránku (Výběr)",
"attachmentUploadMaxSize": "Maximální velikost přílohy (Mb)",
"attachmentUploadChunkSize": "Velikost části nahrávání příloh (Mb)",
"workingTimeCalendar": "Pracovní kalendář",
"oidcClientId": "OIDC ID klienta",
"oidcClientSecret": "OIDC tajný klíč klienta",
"oidcAuthorizationRedirectUri": "OIDC URI přesměrování autorizace",
"oidcAuthorizationEndpoint": "OIDC koncový bod autorizace",
"oidcTokenEndpoint": "OIDC koncový bod tokenu",
"oidcJwksEndpoint": "OIDC koncový bod JSON Web Key Set",
"oidcJwtSignatureAlgorithmList": "OIDC povolené podpisové algoritmy JWT",
"oidcScopes": "OIDC rozsahy",
"oidcGroupClaim": "OIDC nárok skupiny",
"oidcCreateUser": "OIDC vytvořit uživatele",
"oidcUsernameClaim": "OIDC nárok uživatelského jména",
"oidcTeams": "OIDC týmy",
"oidcSync": "OIDC synchronizace",
"oidcSyncTeams": "OIDC synchronizace týmů",
"oidcFallback": "OIDC záložní přihlášení",
"oidcAllowRegularUserFallback": "OIDC povolit záložní přihlášení běžným uživatelům",
"oidcAllowAdminUser": "OIDC povolit přihlášení správcům",
"oidcLogoutUrl": "OIDC URL odhlášení",
"pdfEngine": "PDF generátor",
"recordsPerPageKanban": "Záznamy na stránku (Kanban)",
"auth2FAInPortal": "Povolit dvoufaktorové ověřování v portálech",
"massEmailMaxPerBatchCount": "Maximální počet e-mailů odeslaných za dávku",
"phoneNumberNumericSearch": "Číselné vyhledávání telefonních čísel",
"phoneNumberInternational": "Mezinárodní telefonní čísla",
"phoneNumberPreferredCountryList": "Upřednostňované země pro telefonního čísla",
"jobForceUtc": "Vynutit UTC pro úlohy",
"emailAddressSelectEntityTypeList": "Rozsahy výběru emailových adres",
"phoneNumberExtensions": "Přípony telefonních čísel",
"oidcAuthorizationPrompt": "OIDC výzva k autorizaci",
"quickSearchFullTextAppendWildcard": "Rychlé vyhledávání přidat wildcard symbol",
"authIpAddressCheck": "Omezovat přístup na základě IP adresy",
"authIpAddressWhitelist": "Whitelist IP adres",
"authIpAddressCheckExcludedUsers": "Uživatelé vyřazení z kontroly",
"streamEmailWithContentEntityTypeList": "Entity s obsahem emailu v poznámkách streamu",
"emailScheduledBatchCount": "Maximální počet naplánovaných e-mailů odeslaných za dávku",
"passwordStrengthSpecialCharacterCount": "Počet speciálních znaků požadovaných v hesle",
"availableReactions": "Dostupné reakce",
"outboundEmailFromAddress": "Odesílatelská emailová adresa",
"oidcUserInfoEndpoint": "OIDC koncový bod informací o uživateli",
"baselineRole": "Základní role"
},
"tooltips": {
"recordsPerPage": "Počet záznamů původně zobrazených v zobrazení seznamu.",
"recordsPerPageSmall": "Počet záznamů v panelu vztahů.",
"followCreatedEntities": "Pokud uživatel vytvoří záznam, bude jej sledovat automaticky.",
"ldapUsername": "Úplné uživatelské jméno systému, které umožňuje vyhledávat další uživatele. Např. \"CN = uživatel systému LDAP, OU = uživatelé, OU = espocrm, DC = test, DC = lan\".",
"ldapPassword": "Heslo pro přístup k serveru LDAP.",
"ldapAuth": "Přístup k pověření serveru LDAP.",
"ldapUserNameAttribute": "Atribut k identifikaci uživatele. \nNapř. „userPrincipalName“ nebo „sAMAccountName“ pro Active Directory, „uid“ pro OpenLDAP.",
"ldapUserObjectClass": "Atribut ObjectClass pro vyhledávání uživatelů. Např. „osoba“ pro AD, „inetOrgPerson“ pro OpenLDAP.",
"ldapBindRequiresDn": "Možnost formátovat uživatelské jméno ve formuláři DN.",
"ldapBaseDn": "Výchozí základní DN používané pro vyhledávání uživatelů. Např. \"OU = uživatelé, OU = espocrm, DC = test, DC = lan\".",
"ldapTryUsernameSplit": "Možnost rozdělit uživatelské jméno na doménu.",
"ldapOptReferrals": "pokud by měla být sledována doporučení klientovi LDAP.",
"ldapCreateEspoUser": "Tato možnost umožňuje AutoCRM vytvořit uživatele z LDAP.",
"ldapUserFirstNameAttribute": "Atribut LDAP, který se používá k určení křestního jména uživatele. Např. \"křestní jméno\".",
"ldapUserLastNameAttribute": "Atribut LDAP, který se používá k určení příjmení uživatele. Např. \"sn\".",
"ldapUserTitleAttribute": "LDAP atribut pro titul uživatele.",
"ldapUserEmailAddressAttribute": "Atribut LDAP, který se používá k určení e-mailové adresy uživatele. Např. \"pošta\".",
"ldapUserPhoneNumberAttribute": "LDAP atribut pro telefonní číslo uživatele.",
"ldapUserLoginFilter": "Filtr, který umožňuje omezit uživatele, kteří mohou používat AutoCRM. Např. \"memberOf = CN = espoGroup, OU = groups, OU = espocrm, DC = test, DC = lan\".",
"ldapAccountDomainName": "Doména, která se používá k autorizaci k serveru LDAP.",
"ldapAccountDomainNameShort": "Krátká doména, která se používá k autorizaci k serveru LDAP.",
"ldapUserTeams": "LDAP týmy pro uživatele.",
"ldapUserDefaultTeam": "Výchozí tým pro vytvořeného uživatele. Další informace najdete v uživatelském profilu.",
"b2cMode": "Ve výchozím nastavení je AutoCRM přizpůsoben pro B2B. Můžete jej přepnout na B2C.",
"aclStrictMode": "Povoleno: Přístup k rozsahům bude zakázán, pokud není uveden v rolích. \nZakázán: Přístup k rozsahům bude povolen, pokud není uveden v rolích.",
"outboundEmailIsShared": "Povolit posílání emailů uživatelům pomocí SMTP.",
"streamEmailNotificationsEntityList": "Emailová upozornění na aktualizace streamu sledovaných záznamů. Uživatelé budou dostávat e-mailová oznámení pouze pro určené typy entit.",
"authTokenPreventConcurrent": "Uživatelé nebudou moci být přihlášeni na více zařízeních současně.",
"ldapPortalUserLdapAuth": "Umožněte uživatelům portálu používat autentizaci LDAP namísto autentizace Auto.",
"ldapPortalUserPortals": "Výchozí portály pro vytvořeného uživatele portálu",
"ldapPortalUserRoles": "Výchozí role pro vytvořeného uživatele portálu",
"jobPoolConcurrencyNumber": "Maximální počet procesů spuštěných současně.",
"cronDisabled": "Cron se nespustí.",
"maintenanceMode": "Do systému budou mít přístup pouze správci.",
"ldapAccountCanonicalForm": "Typ kanonického formuláře vašeho účtu. K dispozici jsou 4 možnosti: \n- „Dn“ - formulář ve formátu „CN = tester, OU = espocrm, DC = test, DC = lan“. - „Uživatelské jméno“ - formulář „tester“ .- „Zpětné lomítko“ - formulář „SPOLEČNOST \\ tester“. - „Principal“ - formulář „tester@company.com“.",
"massEmailVerp": "Variabilní zpětná cesta obálky. Pro lepší zpracování odražených zpráv. Ujistěte se, že to váš poskytovatel SMTP podporuje.",
"addressStateList": "Návrhy států pro adresní pole.",
"addressCityList": "Návrhy měst pro adresní pole.",
"addressCountryList": "Návrhy zemí pro adresní pole.",
"exportDisabled": "Zakázat export pro běžné uživatele.",
"siteUrl": "URL vašeho CRM systému.",
"useCache": "Nedoporučuje se deaktivovat, pokud se nejedná o účely vývoje.",
"useWebSocket": "WebSocket umožňuje obousměrnou interaktivní komunikaci mezi serverem a prohlížečem. Vyžaduje nastavení démonu WebSocket na vašem serveru. Pro více informací se podívejte do dokumentace.",
"emailNotificationsDelay": "Zprávu lze upravit ve stanoveném časovém rámci před odesláním oznámení.",
"recordsPerPageSelect": "Počet záznamů na stránku ve výběru.",
"workingTimeCalendar": "Pracovní kalendář pro zobrazení pracovní doby.",
"oidcFallback": "Povolit záložní přihlášení.",
"oidcCreateUser": "Automaticky vytvářet nové uživatele z OIDC.",
"oidcSync": "Synchronizovat uživatelské údaje z OIDC.",
"oidcSyncTeams": "Synchronizovat týmy z OIDC.",
"oidcUsernameClaim": "OIDC nárok pro uživatelské jméno.",
"oidcTeams": "OIDC týmy pro uživatele.",
"recordsPerPageKanban": "Počet záznamů na stránku v Kanban zobrazení.",
"jobForceUtc": "Použije časové pásmo UTC pro plánované úlohy. Jinak bude použito časové pásmo nastavené v nastavení.",
"authIpAddressCheckExcludedUsers": "Uživatelé, kteří se budou moci přihlásit z jakéhokoli místa.",
"authIpAddressWhitelist": "Seznam IP adres nebo rozsahů v notaci CIDR.\n\nPortály nejsou omezeny.",
"oidcGroupClaim": "OIDC nárok pro skupinové informace.",
"outboundEmailFromAddress": "Systémová emailová adresa.",
"baselineRole": "Základní role definuje minimální úroveň přístupových práv pro všechny uživatele. Tato role je automaticky aplikována na všechny uživatele bez ohledu na jejich ostatní role.",
"displayListViewRecordCount": "Zobrazit celkový počet záznamů v zobrazení seznamu.",
"currencyList": "Dostupné měny v systému.",
"activitiesEntityList": "Entity, které se považují za aktivity.",
"historyEntityList": "Entity, které se považují za historii.",
"calendarEntityList": "Entity zobrazené v kalendáři.",
"globalSearchEntityList": "Entity dostupné v globálním vyhledávání.",
"passwordRecoveryForInternalUsersDisabled": "Obnovit heslo budou moci pouze uživatelé portálu.",
"passwordRecoveryNoExposure": "Nebude možné určit, zda je v systému zaregistrována konkrétní e-mailová adresa.",
"emailAddressLookupEntityTypeList": "Pro automatické vyplňování emailových adres.",
"emailAddressSelectEntityTypeList": "Rozsahy pro výběr emailových adres.",
"busyRangesEntityList": "Co se bude brát v úvahu při zobrazování časových období zaneprázdnění v plánovači a časové ose.",
"emailMessageMaxSize": "Všechny příchozí emaily přesahující stanovenou velikost budou načteny bez těla a příloh.",
"authTokenLifetime": "Definuje, jak dlouho mohou existovat tokeny. \n0 - znamená žádné vypršení platnosti.",
"authTokenMaxIdleTime": "Definuje, jak dlouho mohou existovat poslední přístupové tokeny. \n0 - znamená žádné vypršení platnosti.",
"userThemesDisabled": "Pokud je zaškrtnuto, uživatelé nebudou moci vybrat jiné téma.",
"currencyDecimalPlaces": "Počet desetinných míst. Pokud jsou prázdné, zobrazí se všechna neprázdná desetinná místa.",
"aclAllowDeleteCreated": "Uživatelé budou moci odebrat záznamy, které vytvořili, i když nemají přístup k odstranění.",
"textFilterUseContainsForVarchar": "Pokud není zaškrtnuto, použije se operátor „začíná na“. Můžete použít zástupný znak '%'.",
"emailAddressIsOptedOutByDefault": "Při vytváření nového záznamu bude emailová adresa označena jako odhlášena.",
"cleanupDeletedRecords": "Odebrané záznamy budou po chvíli z databáze odstraněny.",
"jobRunInParallel": "Úlohy budou prováděny paralelně.",
"jobMaxPortion": "Maximální počet zpracovaných úloh na jedno provedení.",
"daemonInterval": "Interval spouštění démona v sekundách.",
"daemonMaxProcessNumber": "Maximální počet procesů cron běžících současně.",
"daemonProcessTimeout": "Maximální doba provedení (v sekundách) přidělená jednomu procesu cron.",
"oidcLogoutUrl": "URL pro odhlášení z OIDC poskytovatele.",
"quickSearchFullTextAppendWildcard": "Připojte zástupný znak k dotazu automatického dokončování, pokud je povoleno fulltextové vyhledávání. Snižuje to výkon vyhledávání."
},
"labels": {
"System": "Systém",
"Locale": "Lokalizace",
"Configuration": "Konfigurace",
"In-app Notifications": "In-app notifikace",
"Email Notifications": "Email notifikace",
"Currency Settings": "Nastavení měn",
"Currency Rates": "Kurzy měn",
"Mass Email": "Hromadný email",
"Test Connection": "Test připojení",
"Connecting": "Připojování...",
"Activities": "Aktivity",
"Admin Notifications": "Oznámení správce",
"Search": "Vyhledat",
"Misc": "Vedlejší",
"Passwords": "Hesla",
"2-Factor Authentication": "Dvoufaktorové ověřování",
"Group Tab": "Skupina záložek",
"Attachments": "Přílohy",
"IdP Group": "IdP skupina",
"Divider": "Oddělovač",
"General": "Obecné",
"Navbar": "Navigační panel",
"Phone Numbers": "Telefonní čísla",
"Access": "Přístup",
"Strength": "Síla",
"Recovery": "Obnovení",
"Scheduled Send": "Naplánované odeslání"
},
"messages": {
"ldapTestConnection": "Připojení bylo úspěšně navázáno.",
"confirmBaselineRoleChange": "Opravdu chcete změnit základní roli? Tato změna ovlivní přístupová práva všech uživatelů."
},
"options": {
"streamEmailNotificationsTypeList": {
"Post": "Příspěvky",
"Status": "Aktualizace stavu",
"EmailReceived": "Přijaté emaily"
},
"personNameFormat": {
"firstLast": "Jméno Příjmení",
"lastFirst": "Příjmení Jméno",
"firstMiddleLast": "Jméno Prostřední jméno Příjmení",
"lastFirstMiddle": "Příjmení Jméno Prostřední jméno"
},
"auth2FAMethodList": {
"Email": "E-mail"
}
}
}
{
"fields": {
"useCache": "Použít cache",
"dateFormat": "Formát data",
"timeFormat": "Formát času",
"timeZone": "Časové pásmo",
"weekStart": "První den v týdnu",
"thousandSeparator": "Oddělovač tisíců",
"decimalMark": "Desetinný oddělovač",
"defaultCurrency": "Výchozí měna",
"baseCurrency": "Bázová měna",
"currencyRates": "Kurzy měn",
"currencyList": "Seznam měn",
"language": "Jazyk",
"companyLogo": "Logo společnosti",
"ldapPort": "LDAP Port",
"ldapAuth": "LDAP Auth",
"ldapSecurity": "Zabezpečení",
"ldapPassword": "Heslo",
"outboundEmailFromName": "Od (jméno)",
"outboundEmailIsShared": "Sdílení",
"recordsPerPage": "Záznamy na stránku",
"recordsPerPageSmall": "Záznamy na stránku (malý)",
"tabList": "Seznam záložek",
"quickCreateList": "Rychlé odkazy",
"exportDelimiter": "Export oddělovač",
"globalSearchEntityList": "Seznam entit globálního vyhledávání",
"authenticationMethod": "Autentizační metoda",
"ldapHost": "LDAP Host",
"ldapAccountCanonicalForm": "LDAP Account Canonical Form",
"ldapAccountDomainName": "Název domény účtu",
"ldapTryUsernameSplit": "Zkuste rozdělit uživatelské jméno",
"ldapCreateEspoUser": "Vytvořit uživatele v EspoCRM",
"ldapUserLoginFilter": "Filtr uživatelského přihlášení",
"ldapAccountDomainNameShort": "Account Domain Name krátké",
"ldapOptReferrals": "Volit doporučení",
"exportDisabled": "Zakázat export (povolen pouze správce)",
"b2cMode": "Režm B2C",
"avatarsDisabled": "Zakázat avatary",
"displayListViewRecordCount": "Zobrazit celkový počet (v zobrazení seznamu)",
"theme": "Téma",
"userThemesDisabled": "Zakázat uživatelské motivy",
"emailMessageMaxSize": "Maximální velikost emailu (Mb)",
"personalEmailMaxPortionSize": "Maximální velikost emailové části pro načítání osobních účtů",
"inboundEmailMaxPortionSize": "Maximální velikost emailové části pro načítání skupinových účtů",
"authTokenLifetime": "Životnost ověřovacího tokenu (hodiny)",
"authTokenMaxIdleTime": "Maximální doba nečinnosti ověřovacího tokenu (hodiny)",
"dashboardLayout": "Rozvržení Dashboardu (výchozí)",
"siteUrl": "URL stránky",
"addressPreview": "Náhled adresy",
"addressFormat": "Formát adresy",
"notificationSoundsDisabled": "Zakázat zvuky oznámení",
"applicationName": "Název aplikace",
"ldapUsername": "Uživatelské jméno",
"ldapBindRequiresDn": "Přiřazení vyžaduje Dn",
"ldapBaseDn": "Bázové Dn",
"ldapUserNameAttribute": "Atribut uživatelského jména",
"ldapUserObjectClass": "Třída objektu uživatele",
"ldapUserTitleAttribute": "Atribut názvu uživatele",
"ldapUserFirstNameAttribute": "Atribut křestního jména uživatele",
"ldapUserLastNameAttribute": "Atribut příjmení uživatele",
"ldapUserEmailAddressAttribute": "Atribut emailové adresy uživatele",
"ldapUserTeams": "Týmy uživatele",
"ldapUserDefaultTeam": "Výchozí tým uživatele",
"ldapUserPhoneNumberAttribute": "Atribut telefonního čísla uživatele",
"assignmentNotificationsEntityList": "Entity k upozornění podle přiřazení",
"assignmentEmailNotifications": "Poslat emailová upozornění podle přiřazení",
"assignmentEmailNotificationsEntityList": "Entity k upozornění emailem podle přiřazení",
"streamEmailNotifications": "Oznámení o aktualizacích ve streamu pro interní uživatele",
"portalStreamEmailNotifications": "Oznámení o aktualizacích ve streamu pro uživatele portálu",
"streamEmailNotificationsEntityList": "Rozsahy emailových oznámení o streamu",
"calendarEntityList": "Seznam entit kalendáře",
"mentionEmailNotifications": "Zasílejte emailová oznámení o nových příspěvcích",
"massEmailDisableMandatoryOptOutLink": "Zakázat povinný odkaz pro odhlášení",
"activitiesEntityList": "Seznam entit aktivit",
"historyEntityList": "Seznam entit historie",
"currencyFormat": "Formát měny",
"currencyDecimalPlaces": "Počet desetinných míst měny",
"followCreatedEntities": "Sledovat vytvořené entity",
"aclAllowDeleteCreated": "Povolit odebrání vytvořených záznamů",
"adminNotifications": "Systémová oznámení v administračním panelu",
"adminNotificationsNewVersion": "Zobrazit oznámení, až bude k dispozici nová verze CRM",
"massEmailMaxPerHourCount": "Maximální počet e-mailů odeslaných za hodinu",
"maxEmailAccountCount": "Maximální počet osobních emailových účtů na uživatele",
"streamEmailNotificationsTypeList": "Na co upozorňovat",
"authTokenPreventConcurrent": "Pouze jeden ověřovací token na uživatele",
"scopeColorsDisabled": "Zakázat barvy rozsahu",
"tabColorsDisabled": "Zakázat barvy záložek",
"tabIconsDisabled": "Zakázat ikony na kartě",
"textFilterUseContainsForVarchar": "Při filtrování polí varchar používat operátor „obsahuje“",
"emailAddressIsOptedOutByDefault": "Označit nové emailové adresy jako odhlášené",
"outboundEmailBccAddress": "Adresa BCC pro externí klienty",
"adminNotificationsNewExtensionVersion": "Zobrazit oznámení, když jsou k dispozici nové verze rozšíření",
"cleanupDeletedRecords": "Vyčistit smazané záznamy",
"ldapPortalUserLdapAuth": "Pro uživatele portálu použijte ověřování LDAP",
"ldapPortalUserPortals": "Výchozí portály pro uživatele portálu",
"ldapPortalUserRoles": "Výchozí role pro uživatele portálu",
"fiscalYearShift": "Začátek fiskálního roku",
"jobRunInParallel": "Úlohy běží paralelně",
"jobMaxPortion": "Maximální velikost části úloh",
"jobPoolConcurrencyNumber": "Číslo souběhu úloh",
"daemonInterval": "Interval démona",
"daemonMaxProcessNumber": "Maximální počet procesů démona",
"daemonProcessTimeout": "Timeout procesu démona",
"addressCityList": "Seznam měst při našeptávání políčka adresa",
"addressStateList": "Seznam států pro našeptávání adres",
"cronDisabled": "Zakázat Cron",
"maintenanceMode": "Režim údržby",
"useWebSocket": "Použít WebSocket",
"emailNotificationsDelay": "Zpoždění e-mailových oznámení (v sekundách)",
"massEmailOpenTracking": "Sledování otevření emailů",
"passwordRecoveryDisabled": "Zakázat obnovení hesla",
"passwordRecoveryForAdminDisabled": "Zakázat obnovení hesla pro uživatele správce",
"passwordGenerateLength": "Délka vygenerovaných hesel",
"passwordStrengthLength": "Minimální délka hesla",
"passwordStrengthLetterCount": "Počet písmen požadovaných v hesle",
"passwordStrengthNumberCount": "Počet číslic požadovaných v hesle",
"passwordStrengthBothCases": "Zabraňte vystavení e-mailové adresy ve formuláři pro obnovení hesla",
"auth2FA": "Povolit dvoufaktorové ověřování",
"auth2FAMethodList": "Dostupné metody dvoufaktorové autorizace",
"personNameFormat": "Formát jména osoby",
"newNotificationCountInTitle": "Zobrazit nové číslo oznámení v názvu stránky",
"massEmailVerp": "Použít VERP",
"emailAddressLookupEntityTypeList": "Rozsahy vyhledávání emailových adres",
"busyRangesEntityList": "Seznam volných / zaneprázdněných entit",
"passwordRecoveryForInternalUsersDisabled": "Zakázat obnovení hesla pro uživatele",
"passwordRecoveryNoExposure": "Zabraňte vystavení emailové adresy ve formuláři pro obnovení hesla",
"auth2FAForced": "Přimět uživatele k nastavení dvoufaktorové autorizace",
"smsProvider": "Poskytovatel SMS",
"outboundSmsFromNumber": "SMS z čísla",
"recordsPerPageSelect": "Záznamy na stránku (Výběr)",
"attachmentUploadMaxSize": "Maximální velikost přílohy (Mb)",
"attachmentUploadChunkSize": "Velikost části nahrávání příloh (Mb)",
"workingTimeCalendar": "Pracovní kalendář",
"oidcClientId": "OIDC ID klienta",
"oidcClientSecret": "OIDC tajný klíč klienta",
"oidcAuthorizationRedirectUri": "OIDC URI přesměrování autorizace",
"oidcAuthorizationEndpoint": "OIDC koncový bod autorizace",
"oidcTokenEndpoint": "OIDC koncový bod tokenu",
"oidcJwksEndpoint": "OIDC koncový bod JSON Web Key Set",
"oidcJwtSignatureAlgorithmList": "OIDC povolené podpisové algoritmy JWT",
"oidcScopes": "OIDC rozsahy",
"oidcGroupClaim": "OIDC nárok skupiny",
"oidcCreateUser": "OIDC vytvořit uživatele",
"oidcUsernameClaim": "OIDC nárok uživatelského jména",
"oidcTeams": "OIDC týmy",
"oidcSync": "OIDC synchronizace",
"oidcSyncTeams": "OIDC synchronizace týmů",
"oidcFallback": "OIDC záložní přihlášení",
"oidcAllowRegularUserFallback": "OIDC povolit záložní přihlášení běžným uživatelům",
"oidcAllowAdminUser": "OIDC povolit přihlášení správcům",
"oidcLogoutUrl": "OIDC URL odhlášení",
"pdfEngine": "PDF generátor",
"recordsPerPageKanban": "Záznamy na stránku (Kanban)",
"auth2FAInPortal": "Povolit dvoufaktorové ověřování v portálech",
"massEmailMaxPerBatchCount": "Maximální počet e-mailů odeslaných za dávku",
"phoneNumberNumericSearch": "Číselné vyhledávání telefonních čísel",
"phoneNumberInternational": "Mezinárodní telefonní čísla",
"phoneNumberPreferredCountryList": "Upřednostňované země pro telefonního čísla",
"jobForceUtc": "Vynutit UTC pro úlohy",
"emailAddressSelectEntityTypeList": "Rozsahy výběru emailových adres",
"phoneNumberExtensions": "Přípony telefonních čísel",
"oidcAuthorizationPrompt": "OIDC výzva k autorizaci",
"quickSearchFullTextAppendWildcard": "Rychlé vyhledávání přidat wildcard symbol",
"authIpAddressCheck": "Omezovat přístup na základě IP adresy",
"authIpAddressWhitelist": "Whitelist IP adres",
"authIpAddressCheckExcludedUsers": "Uživatelé vyřazení z kontroly",
"streamEmailWithContentEntityTypeList": "Entity s obsahem emailu v poznámkách streamu",
"emailScheduledBatchCount": "Maximální počet naplánovaných e-mailů odeslaných za dávku",
"passwordStrengthSpecialCharacterCount": "Počet speciálních znaků požadovaných v hesle",
"availableReactions": "Dostupné reakce",
"outboundEmailFromAddress": "Odesílatelská emailová adresa",
"oidcUserInfoEndpoint": "OIDC koncový bod informací o uživateli",
"baselineRole": "Základní role"
},
"tooltips": {
"recordsPerPage": "Počet záznamů původně zobrazených v zobrazení seznamu.",
"recordsPerPageSmall": "Počet záznamů v panelu vztahů.",
"followCreatedEntities": "Pokud uživatel vytvoří záznam, bude jej sledovat automaticky.",
"ldapUsername": "Úplné uživatelské jméno systému, které umožňuje vyhledávat další uživatele. Např. \"CN = uživatel systému LDAP, OU = uživatelé, OU = espocrm, DC = test, DC = lan\".",
"ldapPassword": "Heslo pro přístup k serveru LDAP.",
"ldapAuth": "Přístup k pověření serveru LDAP.",
"ldapUserNameAttribute": "Atribut k identifikaci uživatele. \nNapř. „userPrincipalName“ nebo „sAMAccountName“ pro Active Directory, „uid“ pro OpenLDAP.",
"ldapUserObjectClass": "Atribut ObjectClass pro vyhledávání uživatelů. Např. „osoba“ pro AD, „inetOrgPerson“ pro OpenLDAP.",
"ldapBindRequiresDn": "Možnost formátovat uživatelské jméno ve formuláři DN.",
"ldapBaseDn": "Výchozí základní DN používané pro vyhledávání uživatelů. Např. \"OU = uživatelé, OU = espocrm, DC = test, DC = lan\".",
"ldapTryUsernameSplit": "Možnost rozdělit uživatelské jméno na doménu.",
"ldapOptReferrals": "pokud by měla být sledována doporučení klientovi LDAP.",
"ldapCreateEspoUser": "Tato možnost umožňuje EspoCRM vytvořit uživatele z LDAP.",
"ldapUserFirstNameAttribute": "Atribut LDAP, který se používá k určení křestního jména uživatele. Např. \"křestní jméno\".",
"ldapUserLastNameAttribute": "Atribut LDAP, který se používá k určení příjmení uživatele. Např. \"sn\".",
"ldapUserTitleAttribute": "LDAP atribut pro titul uživatele.",
"ldapUserEmailAddressAttribute": "Atribut LDAP, který se používá k určení e-mailové adresy uživatele. Např. \"pošta\".",
"ldapUserPhoneNumberAttribute": "LDAP atribut pro telefonní číslo uživatele.",
"ldapUserLoginFilter": "Filtr, který umožňuje omezit uživatele, kteří mohou používat EspoCRM. Např. \"memberOf = CN = espoGroup, OU = groups, OU = espocrm, DC = test, DC = lan\".",
"ldapAccountDomainName": "Doména, která se používá k autorizaci k serveru LDAP.",
"ldapAccountDomainNameShort": "Krátká doména, která se používá k autorizaci k serveru LDAP.",
"ldapUserTeams": "LDAP týmy pro uživatele.",
"ldapUserDefaultTeam": "Výchozí tým pro vytvořeného uživatele. Další informace najdete v uživatelském profilu.",
"b2cMode": "Ve výchozím nastavení je EspoCRM přizpůsoben pro B2B. Můžete jej přepnout na B2C.",
"aclStrictMode": "Povoleno: Přístup k rozsahům bude zakázán, pokud není uveden v rolích. \nZakázán: Přístup k rozsahům bude povolen, pokud není uveden v rolích.",
"outboundEmailIsShared": "Povolit posílání emailů uživatelům pomocí SMTP.",
"streamEmailNotificationsEntityList": "Emailová upozornění na aktualizace streamu sledovaných záznamů. Uživatelé budou dostávat e-mailová oznámení pouze pro určené typy entit.",
"authTokenPreventConcurrent": "Uživatelé nebudou moci být přihlášeni na více zařízeních současně.",
"ldapPortalUserLdapAuth": "Umožněte uživatelům portálu používat autentizaci LDAP namísto autentizace Auto.",
"ldapPortalUserPortals": "Výchozí portály pro vytvořeného uživatele portálu",
"ldapPortalUserRoles": "Výchozí role pro vytvořeného uživatele portálu",
"jobPoolConcurrencyNumber": "Maximální počet procesů spuštěných současně.",
"cronDisabled": "Cron se nespustí.",
"maintenanceMode": "Do systému budou mít přístup pouze správci.",
"ldapAccountCanonicalForm": "Typ kanonického formuláře vašeho účtu. K dispozici jsou 4 možnosti: \n- „Dn“ - formulář ve formátu „CN = tester, OU = espocrm, DC = test, DC = lan“. - „Uživatelské jméno“ - formulář „tester“ .- „Zpětné lomítko“ - formulář „SPOLEČNOST \\ tester“. - „Principal“ - formulář „tester@company.com“.",
"massEmailVerp": "Variabilní zpětná cesta obálky. Pro lepší zpracování odražených zpráv. Ujistěte se, že to váš poskytovatel SMTP podporuje.",
"addressStateList": "Návrhy států pro adresní pole.",
"addressCityList": "Návrhy měst pro adresní pole.",
"addressCountryList": "Návrhy zemí pro adresní pole.",
"exportDisabled": "Zakázat export pro běžné uživatele.",
"siteUrl": "URL vašeho CRM systému.",
"useCache": "Nedoporučuje se deaktivovat, pokud se nejedná o účely vývoje.",
"useWebSocket": "WebSocket umožňuje obousměrnou interaktivní komunikaci mezi serverem a prohlížečem. Vyžaduje nastavení démonu WebSocket na vašem serveru. Pro více informací se podívejte do dokumentace.",
"emailNotificationsDelay": "Zprávu lze upravit ve stanoveném časovém rámci před odesláním oznámení.",
"recordsPerPageSelect": "Počet záznamů na stránku ve výběru.",
"workingTimeCalendar": "Pracovní kalendář pro zobrazení pracovní doby.",
"oidcFallback": "Povolit záložní přihlášení.",
"oidcCreateUser": "Automaticky vytvářet nové uživatele z OIDC.",
"oidcSync": "Synchronizovat uživatelské údaje z OIDC.",
"oidcSyncTeams": "Synchronizovat týmy z OIDC.",
"oidcUsernameClaim": "OIDC nárok pro uživatelské jméno.",
"oidcTeams": "OIDC týmy pro uživatele.",
"recordsPerPageKanban": "Počet záznamů na stránku v Kanban zobrazení.",
"jobForceUtc": "Použije časové pásmo UTC pro plánované úlohy. Jinak bude použito časové pásmo nastavené v nastavení.",
"authIpAddressCheckExcludedUsers": "Uživatelé, kteří se budou moci přihlásit z jakéhokoli místa.",
"authIpAddressWhitelist": "Seznam IP adres nebo rozsahů v notaci CIDR.\n\nPortály nejsou omezeny.",
"oidcGroupClaim": "OIDC nárok pro skupinové informace.",
"outboundEmailFromAddress": "Systémová emailová adresa.",
"baselineRole": "Základní role definuje minimální úroveň přístupových práv pro všechny uživatele. Tato role je automaticky aplikována na všechny uživatele bez ohledu na jejich ostatní role.",
"displayListViewRecordCount": "Zobrazit celkový počet záznamů v zobrazení seznamu.",
"currencyList": "Dostupné měny v systému.",
"activitiesEntityList": "Entity, které se považují za aktivity.",
"historyEntityList": "Entity, které se považují za historii.",
"calendarEntityList": "Entity zobrazené v kalendáři.",
"globalSearchEntityList": "Entity dostupné v globálním vyhledávání.",
"passwordRecoveryForInternalUsersDisabled": "Obnovit heslo budou moci pouze uživatelé portálu.",
"passwordRecoveryNoExposure": "Nebude možné určit, zda je v systému zaregistrována konkrétní e-mailová adresa.",
"emailAddressLookupEntityTypeList": "Pro automatické vyplňování emailových adres.",
"emailAddressSelectEntityTypeList": "Rozsahy pro výběr emailových adres.",
"busyRangesEntityList": "Co se bude brát v úvahu při zobrazování časových období zaneprázdnění v plánovači a časové ose.",
"emailMessageMaxSize": "Všechny příchozí emaily přesahující stanovenou velikost budou načteny bez těla a příloh.",
"authTokenLifetime": "Definuje, jak dlouho mohou existovat tokeny. \n0 - znamená žádné vypršení platnosti.",
"authTokenMaxIdleTime": "Definuje, jak dlouho mohou existovat poslední přístupové tokeny. \n0 - znamená žádné vypršení platnosti.",
"userThemesDisabled": "Pokud je zaškrtnuto, uživatelé nebudou moci vybrat jiné téma.",
"currencyDecimalPlaces": "Počet desetinných míst. Pokud jsou prázdné, zobrazí se všechna neprázdná desetinná místa.",
"aclAllowDeleteCreated": "Uživatelé budou moci odebrat záznamy, které vytvořili, i když nemají přístup k odstranění.",
"textFilterUseContainsForVarchar": "Pokud není zaškrtnuto, použije se operátor „začíná na“. Můžete použít zástupný znak '%'.",
"emailAddressIsOptedOutByDefault": "Při vytváření nového záznamu bude emailová adresa označena jako odhlášena.",
"cleanupDeletedRecords": "Odebrané záznamy budou po chvíli z databáze odstraněny.",
"jobRunInParallel": "Úlohy budou prováděny paralelně.",
"jobMaxPortion": "Maximální počet zpracovaných úloh na jedno provedení.",
"daemonInterval": "Interval spouštění démona v sekundách.",
"daemonMaxProcessNumber": "Maximální počet procesů cron běžících současně.",
"daemonProcessTimeout": "Maximální doba provedení (v sekundách) přidělená jednomu procesu cron.",
"oidcLogoutUrl": "URL pro odhlášení z OIDC poskytovatele.",
"quickSearchFullTextAppendWildcard": "Připojte zástupný znak k dotazu automatického dokončování, pokud je povoleno fulltextové vyhledávání. Snižuje to výkon vyhledávání."
},
"labels": {
"System": "Systém",
"Locale": "Lokalizace",
"Configuration": "Konfigurace",
"In-app Notifications": "In-app notifikace",
"Email Notifications": "Email notifikace",
"Currency Settings": "Nastavení měn",
"Currency Rates": "Kurzy měn",
"Mass Email": "Hromadný email",
"Test Connection": "Test připojení",
"Connecting": "Připojování...",
"Activities": "Aktivity",
"Admin Notifications": "Oznámení správce",
"Search": "Vyhledat",
"Misc": "Vedlejší",
"Passwords": "Hesla",
"2-Factor Authentication": "Dvoufaktorové ověřování",
"Group Tab": "Skupina záložek",
"Attachments": "Přílohy",
"IdP Group": "IdP skupina",
"Divider": "Oddělovač",
"General": "Obecné",
"Navbar": "Navigační panel",
"Phone Numbers": "Telefonní čísla",
"Access": "Přístup",
"Strength": "Síla",
"Recovery": "Obnovení",
"Scheduled Send": "Naplánované odeslání"
},
"messages": {
"ldapTestConnection": "Připojení bylo úspěšně navázáno.",
"confirmBaselineRoleChange": "Opravdu chcete změnit základní roli? Tato změna ovlivní přístupová práva všech uživatelů."
},
"options": {
"streamEmailNotificationsTypeList": {
"Post": "Příspěvky",
"Status": "Aktualizace stavu",
"EmailReceived": "Přijaté emaily"
},
"personNameFormat": {
"firstLast": "Jméno Příjmení",
"lastFirst": "Příjmení Jméno",
"firstMiddleLast": "Jméno Prostřední jméno Příjmení",
"lastFirstMiddle": "Příjmení Jméno Prostřední jméno"
},
"auth2FAMethodList": {
"Email": "E-mail"
}
}
}

View File

@@ -72,7 +72,7 @@ class UploadUrlService
*/
public function uploadImage(string $url, FieldData $data): Attachment
{
if (!$this->urlCheck->isNotInternalUrl($url)) {
if (!$this->urlCheck->isUrlAndNotIternal($url)) {
throw new ForbiddenSilent("Not allowed URL.");
}
@@ -114,9 +114,20 @@ class UploadUrlService
/**
* @param non-empty-string $url
* @return ?array{string, string} A type and contents.
* @throws ForbiddenSilent
*/
private function getImageDataByUrl(string $url): ?array
{
$resolve = $this->urlCheck->getCurlResolve($url);
if ($resolve === []) {
throw new ForbiddenSilent("Could not resolve the host.");
}
if ($resolve !== null && !$this->urlCheck->validateCurlResolveNotInternal($resolve)) {
throw new ForbiddenSilent("Forbidden host.");
}
$type = null;
if (!function_exists('curl_init')) {
@@ -144,6 +155,10 @@ class UploadUrlService
$opts[\CURLOPT_PROTOCOLS] = \CURLPROTO_HTTPS | \CURLPROTO_HTTP;
$opts[\CURLOPT_REDIR_PROTOCOLS] = \CURLPROTO_HTTPS;
if ($resolve) {
$opts[CURLOPT_RESOLVE] = $resolve;
}
$ch = curl_init();
curl_setopt_array($ch, $opts);

View File

@@ -36,8 +36,11 @@ use Espo\Core\Api\Response;
use Espo\Core\Api\ResponseComposer;
use Espo\Core\Exceptions\BadRequest;
use Espo\Core\Exceptions\Forbidden;
use Espo\Core\Exceptions\NotFound;
use Espo\Entities\Attachment;
use Espo\Entities\Email;
use Espo\Entities\User;
use Espo\ORM\EntityManager;
use Espo\Tools\Email\ImportEmlService;
/**
@@ -49,6 +52,7 @@ class PostImportEml implements Action
private Acl $acl,
private User $user,
private ImportEmlService $service,
private EntityManager $entityManager,
) {}
public function process(Request $request): Response
@@ -61,11 +65,32 @@ class PostImportEml implements Action
throw new BadRequest("No 'fileId'.");
}
$email = $this->service->import($fileId, $this->user->getId());
$attachment = $this->getAttachment($fileId);
$email = $this->service->import($attachment, $this->user->getId());
return ResponseComposer::json(['id' => $email->getId()]);
}
/**
* @throws NotFound
* @throws Forbidden
*/
private function getAttachment(string $fileId): Attachment
{
$attachment = $this->entityManager->getRDBRepositoryByClass(Attachment::class)->getById($fileId);
if (!$attachment) {
throw new NotFound("Attachment not found.");
}
if (!$this->acl->checkEntityRead($attachment)) {
throw new Forbidden("No access to attachment.");
}
return $attachment;
}
/**
* @throws Forbidden
*/

View File

@@ -115,7 +115,7 @@ class PostSendTest implements Action
if (
!$this->addressUtil->isAllowedAddress($smtpParams) &&
!$this->hostCheck->isNotInternalHost($server)
!$this->hostCheck->isHostAndNotInternal($server)
) {
throw new Forbidden("Not allowed internal host.");
}

View File

@@ -31,7 +31,6 @@ namespace Espo\Tools\Email;
use Espo\Core\Exceptions\Conflict;
use Espo\Core\Exceptions\Error;
use Espo\Core\Exceptions\NotFound;
use Espo\Core\FileStorage\Manager;
use Espo\Core\Mail\Exceptions\ImapError;
use Espo\Core\Mail\Importer;
@@ -56,16 +55,13 @@ class ImportEmlService
/**
* Import an EML.
*
* @param string $fileId An attachment ID.
* @param ?string $userId A user ID to relate an email with.
* @return Email An Email.
* @throws NotFound
* @throws Error
* @throws Conflict
*/
public function import(string $fileId, ?string $userId = null): Email
public function import(Attachment $attachment, ?string $userId = null): Email
{
$attachment = $this->getAttachment($fileId);
$contents = $this->fileStorageManager->getContents($attachment);
try {
@@ -93,20 +89,6 @@ class ImportEmlService
return $email;
}
/**
* @throws NotFound
*/
private function getAttachment(string $fileId): Attachment
{
$attachment = $this->entityManager->getRDBRepositoryByClass(Attachment::class)->getById($fileId);
if (!$attachment) {
throw new NotFound("Attachment not found.");
}
return $attachment;
}
/**
* @throws Conflict
*/

View File

@@ -36,6 +36,7 @@ use Espo\Core\Notification\EmailNotificationHandler;
use Espo\Core\Mail\SenderParams;
use Espo\Core\Utils\Config\ApplicationConfig;
use Espo\Core\Utils\DateTime as DateTimeUtil;
use Espo\Core\Utils\Markdown\Markdown;
use Espo\Entities\Note;
use Espo\ORM\Collection;
use Espo\Repositories\Portal as PortalRepository;
@@ -58,8 +59,6 @@ use Espo\Core\Utils\TemplateFileManager;
use Espo\Core\Utils\Util;
use Espo\Tools\Stream\NoteAccessControl;
use Michelf\Markdown;
use Exception;
use DateTime;
use Throwable;
@@ -325,11 +324,10 @@ class Processor
$data['userName'] = $note->get('createdByName');
$post = Markdown::defaultTransform(
$note->get('post') ?? ''
);
$post = $note->getPost() ?? '';
$data['post'] = $post;
$data['post'] = Markdown::transform($post);
$subjectTpl = $this->templateFileManager->getTemplate('mention', 'subject');
$bodyTpl = $this->templateFileManager->getTemplate('mention', 'body');
@@ -486,9 +484,7 @@ class Processor
$data['userName'] = $note->get('createdByName');
$post = Markdown::defaultTransform($note->getPost() ?? '');
$data['post'] = $post;
$data['post'] = Markdown::transform($note->getPost() ?? '');
$parent = null;

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,4 +1,4 @@
/*! espocrm 2026-03-10 */
/*! espocrm 2026-03-24 */
define("modules/crm/views/scheduler/scheduler",["exports","view","vis-data","vis-timeline","moment","jquery"],function(t,e,a,s,n,r){Object.defineProperty(t,"__esModule",{value:!0});t.default=void 0;e=i(e);n=i(n);r=i(r);function i(t){return t&&t.__esModule?t:{default:t}}class o extends e.default{templateContent=`
<div class="timeline"></div>
<link href="{{basePath}}client/modules/crm/css/vis.css" rel="stylesheet">

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@@ -27,23 +27,23 @@
},
"options": {
"syncStatus": {
"neu": "Neu",
"import": "Import",
"synced": "Synchronisiert",
"unclean": "Nicht synchronisiert",
"pending_sync": "Synchronisierung ausstehend"
"pending_sync": "Synchronisierung ausstehend",
"failed": "Fehlgeschlagen"
},
"aktivierungsstatus": {
"new": "Neu",
"import": "Import",
"active": "Aktiv",
"paused": "Pausiert",
"deactivated": "Deaktiviert"
}
},
"tooltips": {
"syncStatus": "Globaler Synchronisationsstatus: synced = Alle Dokumente synchronisiert, unclean = Mindestens ein Dokument ist neu oder hat Änderungen, pending_sync = Synchronisierung wurde gestartet aber noch nicht abgeschlossen. Wird automatisch basierend auf den Dokumenten-Status aktualisiert.",
"syncStatus": "Globaler Synchronisationsstatus: synced = Alle Dokumente synchronisiert, unclean = Mindestens ein Dokument ist neu oder hat Änderungen, pending_sync = Synchronisierung wurde gestartet aber noch nicht abgeschlossen, failed = Synchronisierung fehlgeschlagen. Wird automatisch basierend auf den Dokumenten-Status aktualisiert.",
"lastSync": "Zeitpunkt der letzten erfolgreichen Synchronisation aller Dokumente",
"aktivierungsstatus": "Aktivierungsstatus der Akte: new = Neu angelegt, active = Aktiv synchronisiert, paused = Synchronisation pausiert, deactivated = Synchronisation deaktiviert",
"aktivierungsstatus": "Aktivierungsstatus der Akte: new = Neu angelegt, import = Aus Advoware importiert, active = Aktiv synchronisiert, paused = Synchronisation pausiert, deactivated = Synchronisation deaktiviert",
"aktenpfad": "Windows-Dateipfad zur Akte in Advoware"
}
}

View File

@@ -27,22 +27,24 @@
"Create CAdvowareAkten": "Create Advoware Akten"
},
"options": {
"syncStatus": { "neu": "New",
"import": "Import", "synced": "Synchronized",
"syncStatus": {
"synced": "Synchronized",
"unclean": "Not Synchronized",
"pending_sync": "Synchronization Pending"
"pending_sync": "Synchronization Pending",
"failed": "Failed"
},
"aktivierungsstatus": {
"new": "New",
"import": "Import",
"active": "Active",
"paused": "Paused",
"deactivated": "Deactivated"
}
},
"tooltips": {
"syncStatus": "Global synchronization status: synced = All documents synchronized, unclean = At least one document is new or has changes, pending_sync = Synchronization started but not yet completed. Updated automatically based on document status.",
"syncStatus": "Global synchronization status: synced = All documents synchronized, unclean = At least one document is new or has changes, pending_sync = Synchronization started but not yet completed, failed = Synchronization failed. Updated automatically based on document status.",
"lastSync": "Timestamp of the last successful synchronization of all documents",
"aktivierungsstatus": "Activation status of the file: new = Newly created, active = Actively synchronized, paused = Synchronization paused, deactivated = Synchronization deactivated",
"aktivierungsstatus": "Activation status of the file: new = Newly created, import = Imported from Advoware, active = Actively synchronized, paused = Synchronization paused, deactivated = Synchronization deactivated",
"aktenpfad": "Windows file path to the file in Advoware"
}
}

View File

@@ -24,6 +24,12 @@
{
"name": "lastSync"
}
],
[
{
"name": "aktivierungsstatus"
},
false
]
],
"style": "default",

View File

@@ -66,18 +66,16 @@
"type": "enum",
"required": false,
"options": [
"neu",
"import",
"synced",
"unclean",
"pending_sync"
"pending_sync",
"failed"
],
"style": {
"neu": "primary",
"import": "info",
"synced": "success",
"unclean": "warning",
"pending_sync": "default"
"pending_sync": "default",
"failed": "danger"
},
"default": "unclean",
"tooltip": true,
@@ -95,12 +93,14 @@
"required": false,
"options": [
"new",
"import",
"active",
"paused",
"deactivated"
],
"style": {
"new": "primary",
"import": "info",
"active": "success",
"paused": "warning",
"deactivated": "danger"

0
custom/scripts/install_blake3.sh Executable file → Normal file
View File

View File

@@ -0,0 +1,62 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2026 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Classes\FieldValidators\Common\Host;
use Espo\Core\FieldValidation\Validator;
use Espo\Core\FieldValidation\Validator\Data;
use Espo\Core\FieldValidation\Validator\Failure;
use Espo\Core\Utils\Security\HostCheck;
use Espo\ORM\Entity;
/**
* @implements Validator<Entity>
* @since 9.3.2
*/
class NotInternal implements Validator
{
public function __construct(
private HostCheck $hostCheck,
) {}
public function validate(Entity $entity, string $field, Data $data): ?Failure
{
$value = $entity->get($field);
if (!$value) {
return null;
}
if (!$this->hostCheck->isNotInternalHost($value)) {
return Failure::create();
}
return null;
}
}

View File

@@ -0,0 +1,71 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2026 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Classes\FieldValidators\Webhook\Url;
use Espo\Core\FieldValidation\Validator;
use Espo\Core\FieldValidation\Validator\Data;
use Espo\Core\FieldValidation\Validator\Failure;
use Espo\Core\Utils\Security\UrlCheck;
use Espo\Core\Webhook\AddressUtil;
use Espo\ORM\Entity;
/**
* @implements Validator<Entity>
*/
class NotInternal implements Validator
{
public function __construct(
private UrlCheck $urlCheck,
private AddressUtil $addressUtil,
) {}
public function validate(Entity $entity, string $field, Data $data): ?Failure
{
$value = $entity->get($field);
if (!$value) {
return null;
}
if (!$this->urlCheck->isUrl($value)) {
return null;
}
if ($this->addressUtil->isAllowedUrl($value)) {
return null;
}
if (!$this->urlCheck->isNotInternalUrl($value)) {
return Failure::create();
}
return null;
}
}

View File

@@ -0,0 +1,188 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2026 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Classes\MassAction\User;
use Espo\Core\Exceptions\BadRequest;
use Espo\Core\MassAction\Actions\MassUpdate as MassUpdateOriginal;
use Espo\Core\MassAction\QueryBuilder;
use Espo\Core\MassAction\Params;
use Espo\Core\MassAction\Result;
use Espo\Core\MassAction\Data;
use Espo\Core\MassAction\MassAction;
use Espo\Core\Utils\File\Manager as FileManager;
use Espo\Core\DataManager;
use Espo\Core\Acl;
use Espo\Core\Acl\Table;
use Espo\Core\Exceptions\Forbidden;
use Espo\Core\Utils\SystemUser;
use Espo\Entities\User;
use Espo\ORM\EntityManager;
use Espo\ORM\Name\Attribute;
use Espo\Tools\MassUpdate\Data as MassUpdateData;
class MassUpdate implements MassAction
{
private const PERMISSION = Acl\Permission::MASS_UPDATE;
/** @var string[] */
private array $notAllowedAttributeList = [
'type',
'password',
'emailAddress',
'isAdmin',
'isSuperAdmin',
'isPortalUser',
];
public function __construct(
private MassUpdateOriginal $massUpdateOriginal,
private QueryBuilder $queryBuilder,
private EntityManager $entityManager,
private Acl $acl,
private User $user,
private FileManager $fileManager,
private DataManager $dataManager
) {}
/**
* @throws Forbidden
* @throws BadRequest
*/
public function process(Params $params, Data $data): Result
{
$entityType = $params->getEntityType();
if (!$this->user->isAdmin()) {
throw new Forbidden("Only admin can mass-update users.");
}
if (!$this->acl->check($entityType, Table::ACTION_EDIT)) {
throw new Forbidden("No edit access for '{$entityType}'.");
}
if ($this->acl->getPermissionLevel(self::PERMISSION) !== Table::LEVEL_YES) {
throw new Forbidden("No mass-update permission.");
}
$massUpdateData = MassUpdateData::fromMassActionData($data);
$this->checkAccess($massUpdateData);
$query = $this->queryBuilder->build($params);
$collection = $this->entityManager
->getRDBRepository(User::ENTITY_TYPE)
->clone($query)
->sth()
->select([Attribute::ID, 'userName'])
->find();
foreach ($collection as $entity) {
$this->checkEntity($entity, $massUpdateData);
}
$result = $this->massUpdateOriginal->process($params, $data);
$this->afterProcess($result, $massUpdateData);
return $result;
}
/**
* @throws Forbidden
*/
private function checkAccess(MassUpdateData $data): void
{
foreach ($this->notAllowedAttributeList as $attribute) {
if ($data->has($attribute)) {
throw new Forbidden("Attribute '{$attribute}' not allowed for mass-update.");
}
}
}
/**
* @throws Forbidden
*/
private function checkEntity(User $entity, MassUpdateData $data): void
{
if ($entity->getUserName() === SystemUser::NAME) {
throw new Forbidden("Can't update 'system' user.");
}
if ($entity->getId() === $this->user->getId()) {
if ($data->has('isActive')) {
throw new Forbidden("Can't change 'isActive' field for own user.");
}
}
}
private function afterProcess(Result $result, MassUpdateData $dataWrapped): void
{
$data = $dataWrapped->getValues();
if (
property_exists($data, 'rolesIds') ||
property_exists($data, 'teamsIds') ||
property_exists($data, 'type') ||
property_exists($data, 'portalRolesIds') ||
property_exists($data, 'portalsIds')
) {
foreach ($result->getIds() as $id) {
$this->clearRoleCache($id);
}
$this->dataManager->updateCacheTimestamp();
}
if (
property_exists($data, 'portalRolesIds') ||
property_exists($data, 'portalsIds') ||
property_exists($data, 'contactId') ||
property_exists($data, 'accountsIds')
) {
$this->clearPortalRolesCache();
$this->dataManager->updateCacheTimestamp();
}
}
private function clearRoleCache(string $id): void
{
$this->fileManager->removeFile('data/cache/application/acl/' . $id . '.php');
}
private function clearPortalRolesCache(): void
{
$this->fileManager->removeInDir('data/cache/application/aclPortal');
}
}

View File

@@ -0,0 +1,120 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2026 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Classes\RecordHooks\EmailAccount;
use Espo\Core\Exceptions\Forbidden;
use Espo\Core\Record\Hook\SaveHook;
use Espo\Core\Utils\Config;
use Espo\Core\Utils\Security\HostCheck;
use Espo\Entities\EmailAccount;
use Espo\Entities\InboundEmail;
use Espo\ORM\Entity;
/**
* @implements SaveHook<EmailAccount|InboundEmail>
*/
class BeforeSaveValidateHosts implements SaveHook
{
public function __construct(
private Config $config,
private HostCheck $hostCheck,
) {}
public function process(Entity $entity): void
{
if ($entity->isAttributeChanged('host') || $entity->isAttributeChanged('port')) {
$this->validateImap($entity);
}
if ($entity->isAttributeChanged('smtpHost') || $entity->isAttributeChanged('smtpPort')) {
$this->validateSmtp($entity);
}
}
/**
* @throws Forbidden
*/
private function validateImap(EmailAccount|InboundEmail $entity): void
{
$host = $entity->getHost();
$port = $entity->getPort();
if ($host === null || $port === null) {
return;
}
$address = $host . ':' . $port;
if (in_array($address, $this->getAllowedAddressList())) {
return;
}
if (!$this->hostCheck->isNotInternalHost($host)) {
$message = $this->composeErrorMessage($host, $address);
throw new Forbidden($message);
}
}
/**
* @throws Forbidden
*/
private function validateSmtp(EmailAccount|InboundEmail $entity): void
{
$host = $entity->getSmtpHost();
$port = $entity->getSmtpPort();
if ($host === null || $port === null) {
return;
}
$address = $host . ':' . $port;
if (!$this->hostCheck->isNotInternalHost($host)) {
$message = $this->composeErrorMessage($host, $address);
throw new Forbidden($message);
}
}
/**
* @return string[]
*/
private function getAllowedAddressList(): array
{
return $this->config->get('emailServerAllowedAddressList') ?? [];
}
private function composeErrorMessage(string $host, string $address): string
{
return "Host '$host' is not allowed as it's internal. " .
"To allow, add `$address` to the config parameter `emailServerAllowedAddressList`.";
}
}

View File

@@ -0,0 +1,51 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2026 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Classes\TemplateHelpers;
use Espo\Core\Htmlizer\Helper;
use Espo\Core\Htmlizer\Helper\Data;
use Espo\Core\Htmlizer\Helper\Result;
use Michelf\MarkdownExtra as MarkdownTransformer;
class MarkdownText implements Helper
{
public function render(Data $data): Result
{
$value = $data->getArgumentList()[0] ?? null;
if (!$value || !is_string($value)) {
return Result::createEmpty();
}
$transformed = MarkdownTransformer::defaultTransform($value);
return Result::createSafeString($transformed);
}
}

View File

@@ -0,0 +1,86 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2026 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\Acl\Cache;
use Espo\Core\Utils\File\Manager as FileManager;
use Espo\Entities\Portal;
use Espo\Entities\User;
use Espo\ORM\EntityManager;
use Espo\ORM\Name\Attribute;
/**
* @todo Clear cache in AclManager.
*/
class Clearer
{
public function __construct(private FileManager $fileManager, private EntityManager $entityManager)
{}
public function clearForAllInternalUsers(): void
{
$this->fileManager->removeInDir('data/cache/application/acl');
$this->fileManager->removeInDir('data/cache/application/aclMap');
}
public function clearForAllPortalUsers(): void
{
$this->fileManager->removeInDir('data/cache/application/aclPortal');
$this->fileManager->removeInDir('data/cache/application/aclPortalMap');
}
public function clearForUser(User $user): void
{
if ($user->isPortal()) {
$this->clearForPortalUser($user);
return;
}
$part = $user->getId() . '.php';
$this->fileManager->remove('data/cache/application/acl/' . $part);
$this->fileManager->remove('data/cache/application/aclMap/' . $part);
}
private function clearForPortalUser(User $user): void
{
$portals = $this->entityManager
->getRDBRepositoryByClass(Portal::class)
->select(Attribute::ID)
->find();
foreach ($portals as $portal) {
$part = $portal->getId() . '/' . $user->getId() . '.php';
$this->fileManager->remove('data/cache/application/aclPortal/' . $part);
$this->fileManager->remove('data/cache/application/aclPortalMap/' . $part);
}
}
}

View File

@@ -0,0 +1,121 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2026 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\FileStorage\Storages;
use Espo\Core\FileStorage\Attachment;
use Espo\Core\FileStorage\Local;
use Espo\Core\FileStorage\Storage;
use Espo\Core\Utils\File\Manager as FileManager;
use Espo\Core\Utils\File\Exceptions\FileError;
use Psr\Http\Message\StreamInterface;
use GuzzleHttp\Psr7\Stream;
class EspoUploadDir implements Storage, Local
{
public const NAME = 'EspoUploadDir';
public function __construct(protected FileManager $fileManager)
{}
public function unlink(Attachment $attachment): void
{
$this->fileManager->unlink(
$this->getFilePath($attachment)
);
}
public function exists(Attachment $attachment): bool
{
$filePath = $this->getFilePath($attachment);
return $this->fileManager->isFile($filePath);
}
public function getSize(Attachment $attachment): int
{
$filePath = $this->getFilePath($attachment);
if (!$this->exists($attachment)) {
throw new FileError("Could not get size for non-existing file '$filePath'.");
}
return $this->fileManager->getSize($filePath);
}
public function getStream(Attachment $attachment): StreamInterface
{
$filePath = $this->getFilePath($attachment);
if (!$this->exists($attachment)) {
throw new FileError("Could not get stream for non-existing '$filePath'.");
}
$resource = fopen($filePath, 'r');
if ($resource === false) {
throw new FileError("Could not open '$filePath'.");
}
return new Stream($resource);
}
public function putStream(Attachment $attachment, StreamInterface $stream): void
{
$filePath = $this->getFilePath($attachment);
$stream->rewind();
// @todo Use a resource to write a file (add a method to the file manager).
$contents = $stream->getContents();
$result = $this->fileManager->putContents($filePath, $contents);
if (!$result) {
throw new FileError("Could not store a file '$filePath'.");
}
}
public function getLocalFilePath(Attachment $attachment): string
{
return $this->getFilePath($attachment);
}
/**
* @return string
*/
protected function getFilePath(Attachment $attachment)
{
$sourceId = $attachment->getSourceId();
return 'data/upload/' . $sourceId;
}
}

View File

@@ -0,0 +1,57 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2026 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\Formula\Functions\ExtGroup\MarkdownGroup;
use Espo\Core\Formula\EvaluatedArgumentList;
use Espo\Core\Formula\Exceptions\BadArgumentType;
use Espo\Core\Formula\Exceptions\TooFewArguments;
use Espo\Core\Formula\Func;
use Michelf\Markdown;
/**
* @noinspection PhpUnused
*/
class TransformType implements Func
{
public function process(EvaluatedArgumentList $arguments): string
{
if (count($arguments) < 1) {
throw TooFewArguments::create(1);
}
$string = $arguments[0] ?? '';
if (!is_string($string)) {
throw BadArgumentType::create(1, 'string');
}
return Markdown::defaultTransform($string);
}
}

View File

@@ -0,0 +1,181 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2026 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\Mail\Account\GroupAccount;
use Espo\Core\Exceptions\ErrorSilent;
use Espo\Core\Exceptions\Forbidden;
use Espo\Core\Mail\Account\Account as Account;
use Espo\Core\Exceptions\Error;
use Espo\Core\Mail\Account\Fetcher;
use Espo\Core\Mail\Account\Storage\Params;
use Espo\Core\Mail\Account\StorageFactory;
use Espo\Core\Mail\Account\Util\AddressUtil;
use Espo\Core\Mail\Account\Util\NotificationHelper;
use Espo\Core\Mail\Exceptions\ImapError;
use Espo\Core\Mail\Exceptions\NoImap;
use Espo\Core\Mail\Sender\Message;
use Espo\Core\Utils\Log;
use Espo\Core\Utils\Security\HostCheck;
use Exception;
class Service
{
public function __construct(
private Fetcher $fetcher,
private AccountFactory $accountFactory,
private StorageFactory $storageFactory,
private Log $log,
private NotificationHelper $notificationHelper,
private HostCheck $hostCheck,
private AddressUtil $addressUtil,
) {}
/**
* @param string $id Account ID.
* @throws Error
* @throws NoImap
* @throws ImapError
*/
public function fetch(string $id): void
{
$account = $this->accountFactory->create($id);
try {
$this->fetcher->fetch($account);
} catch (ImapError $e) {
$this->notificationHelper->processImapError($account);
throw $e;
}
$account->updateConnectedAt();
}
/**
* @return string[]
* @throws Error
* @throws ImapError
* @throws Forbidden
*/
public function getFolderList(Params $params): array
{
if (
$params->getHost() &&
!$this->addressUtil->isAllowedAddress($params) &&
!$this->hostCheck->isNotInternalHost($params->getHost())
) {
throw new Forbidden("Not allowed internal host.");
}
if ($params->getId()) {
$account = $this->accountFactory->create($params->getId());
$params = $params
->withPassword($this->getPassword($params, $account))
->withImapHandlerClassName($account->getImapHandlerClassName());
}
$storage = $this->storageFactory->createWithParams($params);
return $storage->getFolderNames();
}
/**
* @throws Error
* @throws Forbidden
*/
public function testConnection(Params $params): void
{
if ($params->getId()) {
$account = $this->accountFactory->create($params->getId());
$params = $params
->withPassword($this->getPassword($params, $account))
->withImapHandlerClassName($account->getImapHandlerClassName());
}
if (
$params->getHost() &&
!$this->addressUtil->isAllowedAddress($params) &&
!$this->hostCheck->isNotInternalHost($params->getHost())
) {
throw new Forbidden("Not allowed internal host.");
}
try {
$storage = $this->storageFactory->createWithParams($params);
$storage->getFolderNames();
} catch (Exception $e) {
$this->log->warning("IMAP test connection failed; {message}", [
'exception' => $e,
'message' => $e->getMessage(),
]);
$message = $e instanceof ImapError ?
$e->getMessage() : '';
throw new ErrorSilent($message, previous: $e);
}
}
private function getPassword(Params $params, Account $account): ?string
{
$password = $params->getPassword();
if ($password !== null) {
return $password;
}
$imapParams = $account->getImapParams();
return $imapParams?->getPassword();
}
/**
* @param string $id Account ID.
* @throws Error
* @throws ImapError
* @throws NoImap
*/
public function storeSentMessage(string $id, Message $message): void
{
$account = $this->accountFactory->create($id);
$folder = $account->getSentFolder();
if (!$folder) {
throw new Error("No sent folder for Group Email Account $id.");
}
$storage = $this->storageFactory->create($account);
$storage->appendMessage($message->toString(), $folder);
}
}

View File

@@ -0,0 +1,216 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2026 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\Mail\Account\PersonalAccount;
use Espo\Core\Exceptions\ErrorSilent;
use Espo\Core\Mail\Account\Util\AddressUtil;
use Espo\Core\Mail\Account\Util\NotificationHelper;
use Espo\Core\Mail\Exceptions\ImapError;
use Espo\Core\Mail\Exceptions\NoImap;
use Espo\Core\Utils\Config;
use Espo\Core\Utils\Log;
use Espo\Core\Mail\Account\Account as Account;
use Espo\Core\Exceptions\Forbidden;
use Espo\Core\Exceptions\Error;
use Espo\Core\Mail\Account\Fetcher;
use Espo\Core\Mail\Account\Storage\Params;
use Espo\Core\Mail\Account\StorageFactory;
use Espo\Core\Utils\Security\HostCheck;
use Espo\Entities\User;
use Espo\Core\Mail\Sender\Message;
use Exception;
class Service
{
public function __construct(
private Fetcher $fetcher,
private AccountFactory $accountFactory,
private StorageFactory $storageFactory,
private User $user,
private Log $log,
private NotificationHelper $notificationHelper,
private HostCheck $hostCheck,
private AddressUtil $addressUtil,
) {}
/**
* @param string $id Account ID.
* @throws Error
* @throws NoImap
* @throws ImapError
*/
public function fetch(string $id): void
{
$account = $this->accountFactory->create($id);
try {
$this->fetcher->fetch($account);
} catch (ImapError $e) {
$this->notificationHelper->processImapError($account);
throw $e;
}
$account->updateConnectedAt();
}
/**
* @return string[]
* @throws Forbidden
* @throws Error
* @throws ImapError
*/
public function getFolderList(Params $params): array
{
$userId = $params->getUserId();
if (
$userId &&
!$this->user->isAdmin() &&
$userId !== $this->user->getId()
) {
throw new Forbidden();
}
if (
$params->getHost() &&
!$this->addressUtil->isAllowedAddress($params) &&
!$this->hostCheck->isNotInternalHost($params->getHost())
) {
throw new Forbidden("Not allowed internal host.");
}
if ($params->getId()) {
$account = $this->accountFactory->create($params->getId());
$params = $params
->withPassword($this->getPassword($params, $account))
->withImapHandlerClassName($account->getImapHandlerClassName());
}
$storage = $this->storageFactory->createWithParams($params);
return $storage->getFolderNames();
}
/**
* @throws Forbidden
* @throws Error
*/
public function testConnection(Params $params): void
{
$userId = $params->getUserId();
if (
$userId &&
!$this->user->isAdmin() &&
$userId !== $this->user->getId()
) {
throw new Forbidden();
}
if (!$params->getId() && $params->getPassword() === null) {
throw new Forbidden();
}
if (
$params->getHost() &&
!$this->addressUtil->isAllowedAddress($params) &&
!$this->hostCheck->isNotInternalHost($params->getHost())
) {
throw new Forbidden("Not allowed host.");
}
if ($params->getId()) {
$account = $this->accountFactory->create($params->getId());
if (
!$this->user->isAdmin() &&
$account->getUser()->getId() !== $this->user->getId()
) {
throw new Forbidden();
}
$params = $params
->withPassword($this->getPassword($params, $account))
->withImapHandlerClassName($account->getImapHandlerClassName());
}
try {
$storage = $this->storageFactory->createWithParams($params);
$storage->getFolderNames();
} catch (Exception $e) {
$this->log->warning("IMAP test connection failed; {message}", [
'exception' => $e,
'message' => $e->getMessage(),
]);
$message = $e instanceof ImapError ?
$e->getMessage() : '';
throw new ErrorSilent($message);
}
}
private function getPassword(Params $params, Account $account): ?string
{
$password = $params->getPassword();
if ($password !== null) {
return $password;
}
$imapParams = $account->getImapParams();
return $imapParams?->getPassword();
}
/**
* @param string $id Account ID.
* @throws Error
* @throws ImapError
* @throws NoImap
*/
public function storeSentMessage(string $id, Message $message): void
{
$account = $this->accountFactory->create($id);
$folder = $account->getSentFolder();
if (!$folder) {
throw new Error("No sent folder for Email Account $id.");
}
$storage = $this->storageFactory->create($account);
$storage->appendMessage($message->toString(), $folder);
}
}

View File

@@ -0,0 +1,64 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2026 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\Portal\Api;
use Espo\Core\Api\MiddlewareProvider;
use Espo\Core\Api\Starter as StarterBase;
use Espo\Core\ApplicationState;
use Espo\Core\Portal\Utils\Route as RouteUtil;
use Espo\Core\Api\RouteProcessor;
use Espo\Core\Api\Route\RouteParamsFetcher;
use Espo\Core\Utils\Config\SystemConfig;
use Espo\Core\Utils\Log;
class Starter extends StarterBase
{
public function __construct(
RouteProcessor $requestProcessor,
RouteUtil $routeUtil,
RouteParamsFetcher $routeParamsFetcher,
MiddlewareProvider $middlewareProvider,
Log $log,
SystemConfig $systemConfig,
ApplicationState $applicationState
) {
$routeCacheFile = 'data/cache/application/slim-routes-portal-' . $applicationState->getPortalId() . '.php';
parent::__construct(
$requestProcessor,
$routeUtil,
$routeParamsFetcher,
$middlewareProvider,
$log,
$systemConfig,
$routeCacheFile
);
}
}

View File

@@ -0,0 +1,75 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2026 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\Utils\Security;
use const DNS_A;
use const FILTER_FLAG_NO_PRIV_RANGE;
use const FILTER_FLAG_NO_RES_RANGE;
use const FILTER_VALIDATE_IP;
class HostCheck
{
public function isNotInternalHost(string $host): bool
{
$records = dns_get_record($host, DNS_A);
if (filter_var($host, FILTER_VALIDATE_IP)) {
return $this->ipAddressIsNotInternal($host);
}
if (!$records) {
return true;
}
foreach ($records as $record) {
/** @var ?string $idAddress */
$idAddress = $record['ip'] ?? null;
if (!$idAddress) {
return false;
}
if (!$this->ipAddressIsNotInternal($idAddress)) {
return false;
}
}
return true;
}
private function ipAddressIsNotInternal(string $ipAddress): bool
{
return (bool) filter_var(
$ipAddress,
FILTER_VALIDATE_IP,
FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE
);
}
}

View File

@@ -0,0 +1,63 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2026 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\Utils\Security;
use const FILTER_VALIDATE_URL;
use const PHP_URL_HOST;
class UrlCheck
{
public function __construct(
private HostCheck $hostCheck,
) {}
public function isUrl(string $url): bool
{
return filter_var($url, FILTER_VALIDATE_URL) !== false;
}
/**
* Checks whether a URL does not follow to an internal host.
*/
public function isNotInternalUrl(string $url): bool
{
if (!$this->isUrl($url)) {
return false;
}
$host = parse_url($url, PHP_URL_HOST);
if (!is_string($host)) {
return false;
}
return $this->hostCheck->isNotInternalHost($host);
}
}

View File

@@ -0,0 +1,161 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2026 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\Utils;
use Espo\Core\Utils\File\Manager as FileManager;
use Espo\Core\Utils\Resource\FileReader;
use Espo\Core\Utils\Resource\FileReader\Params as FileReaderParams;
use Espo\Core\Utils\Metadata;
class TemplateFileManager
{
public function __construct(
private Config $config,
private FileManager $fileManager,
private FileReader $fileReader,
private Metadata $metadata,
) {}
public function getTemplate(
string $type,
string $name,
?string $entityType = null,
): string {
$templates = $this->metadata->get(['app', 'templates']);
$moduleName = null;
if (isset($templates[$type]) && isset($templates[$type]["module"])) {
$moduleName = $templates[$type]["module"];
}
$params = FileReaderParams::create()
->withScope($entityType)
->withModuleName($moduleName);
if ($entityType) {
$path1 = $this->getPath($type, $name, $entityType);
$exists1 = $this->fileReader->exists($path1, $params);
if ($exists1) {
return $this->fileReader->read($path1, $params);
}
}
$path2 = $this->getPath($type, $name);
$exists2 = $this->fileReader->exists($path2, $params);
if ($exists2) {
return $this->fileReader->read($path2, $params);
}
if ($entityType) {
$path3 = $this->getDefaultLanguagePath($type, $name, $entityType);
$exists3 = $this->fileReader->exists($path3, $params);
if ($exists3) {
return $this->fileReader->read($path3, $params);
}
}
$path4 = $this->getDefaultLanguagePath($type, $name);
return $this->fileReader->read($path4, $params);
}
public function saveTemplate(
string $type,
string $name,
string $contents,
?string $entityType = null
): void {
$language = $this->config->get('language');
$filePath = $this->getCustomFilePath($language, $type, $name, $entityType);
$this->fileManager->putContents($filePath, $contents);
}
public function resetTemplate(string $type, string $name, ?string $entityType = null): void
{
$language = $this->config->get('language');
$filePath = $this->getCustomFilePath($language, $type, $name, $entityType);
$this->fileManager->removeFile($filePath);
}
private function getCustomFilePath(
string $language,
string $type,
string $name,
?string $entityType = null
): string {
if ($entityType) {
return "custom/Espo/Custom/Resources/templates/{$type}/{$language}/{$entityType}/{$name}.tpl";
}
return "custom/Espo/Custom/Resources/templates/{$type}/{$language}/{$name}.tpl";
}
private function getPath(string $type, string $name, ?string $entityType = null): string
{
$language = $this->config->get('language');
return $this->getPathForLanguage($language, $type, $name, $entityType);
}
private function getDefaultLanguagePath(string $type, string $name, ?string $entityType = null): string
{
$language = 'en_US';
return $this->getPathForLanguage($language, $type, $name, $entityType);
}
private function getPathForLanguage(
string $language,
string $type,
string $name,
?string $entityType = null
): string {
if ($entityType) {
return "templates/{$type}/{$language}/{$entityType}/{$name}.tpl";
}
return "templates/{$type}/{$language}/{$name}.tpl";
}
}

View File

@@ -0,0 +1,162 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2026 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\Webhook;
use Espo\Core\Exceptions\Error;
use Espo\Core\Utils\Config;
use Espo\Core\Utils\Json;
use Espo\Core\Utils\Security\UrlCheck;
use Espo\Entities\Webhook;
/**
* Sends a portion.
*/
class Sender
{
private const CONNECT_TIMEOUT = 5;
private const TIMEOUT = 10;
public function __construct(
private Config $config,
private UrlCheck $urlCheck,
private AddressUtil $addressUtil,
) {}
/**
* @param array<int, mixed> $dataList
* @throws Error
*/
public function send(Webhook $webhook, array $dataList): int
{
$payload = Json::encode($dataList);
$signature = null;
$legacySignature = null;
$secretKey = $webhook->getSecretKey();
if ($secretKey) {
$signature = $this->buildSignature($webhook, $payload, $secretKey);
$legacySignature = $this->buildSignatureLegacy($webhook, $payload, $secretKey);
}
$connectTimeout = $this->config->get('webhookConnectTimeout', self::CONNECT_TIMEOUT);
$timeout = $this->config->get('webhookTimeout', self::TIMEOUT);
$headerList = [];
$headerList[] = 'Content-Type: application/json';
$headerList[] = 'Content-Length: ' . strlen($payload);
if ($signature) {
$headerList[] = 'Signature: ' . $signature;
}
if ($legacySignature) {
$headerList[] = 'X-Signature: ' . $legacySignature;
}
$url = $webhook->getUrl();
if (!$url) {
throw new Error("Webhook does not have URL.");
}
if (!$this->urlCheck->isUrl($url)) {
throw new Error("'$url' is not valid URL.");
}
if (
!$this->addressUtil->isAllowedUrl($url) &&
!$this->urlCheck->isNotInternalUrl($url)
) {
throw new Error("URL '$url' points to an internal host, not allowed.");
}
$handler = curl_init($url);
if ($handler === false) {
throw new Error("Could not init CURL for URL {$url}.");
}
curl_setopt($handler, \CURLOPT_RETURNTRANSFER, true);
curl_setopt($handler, \CURLOPT_FOLLOWLOCATION, true);
curl_setopt($handler, \CURLOPT_SSL_VERIFYPEER, true);
curl_setopt($handler, \CURLOPT_HEADER, true);
curl_setopt($handler, \CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($handler, \CURLOPT_CONNECTTIMEOUT, $connectTimeout);
curl_setopt($handler, \CURLOPT_TIMEOUT, $timeout);
curl_setopt($handler, \CURLOPT_PROTOCOLS, \CURLPROTO_HTTPS | \CURLPROTO_HTTP);
curl_setopt($handler, \CURLOPT_REDIR_PROTOCOLS, \CURLPROTO_HTTPS);
curl_setopt($handler, \CURLOPT_HTTPHEADER, $headerList);
curl_setopt($handler, \CURLOPT_POSTFIELDS, $payload);
curl_exec($handler);
$code = curl_getinfo($handler, \CURLINFO_HTTP_CODE);
if (!is_numeric($code)) {
$code = 0;
}
if (!is_int($code)) {
$code = intval($code);
}
$errorNumber = curl_errno($handler);
if (
$errorNumber &&
in_array($errorNumber, [\CURLE_OPERATION_TIMEDOUT, \CURLE_OPERATION_TIMEOUTED])
) {
$code = 408;
}
curl_close($handler);
return $code;
}
private function buildSignature(Webhook $webhook, string $payload, string $secretKey): string
{
$webhookId = $webhook->getId();
$hash = hash_hmac('sha256', $payload, $secretKey);
return base64_encode("$webhookId:$hash");
}
/**
* @todo Remove in v11.0.
*/
private function buildSignatureLegacy(Webhook $webhook, string $payload, string $secretKey): string
{
return base64_encode($webhook->getId() . ':' . hash_hmac('sha256', $payload, $secretKey, true));
}
}

View File

@@ -0,0 +1,109 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2026 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\EntryPoints;
use Espo\Core\Utils\Metadata;
use Espo\Entities\Attachment as AttachmentEntity;
use Espo\Core\Acl;
use Espo\Core\Api\Request;
use Espo\Core\Api\Response;
use Espo\Core\EntryPoint\EntryPoint;
use Espo\Core\Exceptions\BadRequest;
use Espo\Core\Exceptions\Forbidden;
use Espo\Core\Exceptions\NotFound;
use Espo\Core\FileStorage\Manager as FileStorageManager;
use Espo\Core\ORM\EntityManager;
class Attachment implements EntryPoint
{
public function __construct(
private FileStorageManager $fileStorageManager,
private EntityManager $entityManager,
private Acl $acl,
private Metadata $metadata
) {}
public function run(Request $request, Response $response): void
{
$id = $request->getQueryParam('id');
if (!$id) {
throw new BadRequest("No id.");
}
$attachment = $this->entityManager
->getRDBRepositoryByClass(AttachmentEntity::class)
->getById($id);
if (!$attachment) {
throw new NotFound("Attachment not found.");
}
if (!$this->acl->checkEntity($attachment)) {
throw new Forbidden("No access to attachment.");
}
if (!$this->fileStorageManager->exists($attachment)) {
throw new NotFound("File not found.");
}
$fileType = $attachment->getType();
if (!in_array($fileType, $this->getAllowedFileTypeList())) {
throw new Forbidden("Not allowed file type '{$fileType}'.");
}
if ($attachment->isBeingUploaded()) {
throw new Forbidden("Attachment is being-uploaded.");
}
if ($fileType) {
$response->setHeader('Content-Type', $fileType);
}
$stream = $this->fileStorageManager->getStream($attachment);
$size = $stream->getSize() ?? $this->fileStorageManager->getSize($attachment);
$response
->setHeader('Content-Length', (string) $size)
->setHeader('Cache-Control', 'private, max-age=864000, immutable')
->setHeader('Content-Security-Policy', "default-src 'self'")
->setBody($stream);
}
/**
* @return string[]
*/
private function getAllowedFileTypeList(): array
{
return $this->metadata->get(['app', 'image', 'allowedFileTypeList']) ?? [];
}
}

View File

@@ -0,0 +1,108 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2026 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\EntryPoints;
use Espo\Entities\Attachment as AttachmentEntity;
use Espo\Core\Acl;
use Espo\Core\Api\Request;
use Espo\Core\Api\Response;
use Espo\Core\EntryPoint\EntryPoint;
use Espo\Core\Exceptions\BadRequest;
use Espo\Core\Exceptions\Forbidden;
use Espo\Core\Exceptions\NotFoundSilent;
use Espo\Core\FileStorage\Manager as FileStorageManager;
use Espo\Core\ORM\EntityManager;
use Espo\Core\Utils\Metadata;
class Download implements EntryPoint
{
public function __construct(
protected FileStorageManager $fileStorageManager,
protected Acl $acl,
protected EntityManager $entityManager,
private Metadata $metadata
) {}
public function run(Request $request, Response $response): void
{
$id = $request->getQueryParam('id');
if (!$id) {
throw new BadRequest("No id.");
}
/** @var ?AttachmentEntity $attachment */
$attachment = $this->entityManager->getEntityById(AttachmentEntity::ENTITY_TYPE, $id);
if (!$attachment) {
throw new NotFoundSilent("Attachment not found.");
}
if (!$this->acl->checkEntity($attachment)) {
throw new Forbidden("No access to attachment.");
}
if ($attachment->isBeingUploaded()) {
throw new Forbidden("Attachment is being uploaded.");
}
$stream = $this->fileStorageManager->getStream($attachment);
$outputFileName = str_replace("\"", "\\\"", $attachment->getName() ?? '');
$type = $attachment->getType();
$disposition = 'attachment';
/** @var string[] $inlineMimeTypeList */
$inlineMimeTypeList = $this->metadata->get(['app', 'file', 'inlineMimeTypeList']) ?? [];
if (in_array($type, $inlineMimeTypeList)) {
$disposition = 'inline';
$response->setHeader('Content-Security-Policy', "default-src 'self'");
}
$response->setHeader('Content-Description', 'File Transfer');
if ($type) {
$response->setHeader('Content-Type', $type);
}
$size = $stream->getSize() ?? $this->fileStorageManager->getSize($attachment);
$response
->setHeader('Content-Disposition', $disposition . ";filename=\"" . $outputFileName . "\"")
->setHeader('Expires', '0')
->setHeader('Cache-Control', 'must-revalidate')
->setHeader('Content-Length', (string) $size)
->setBody($stream);
}
}

View File

@@ -0,0 +1,460 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2026 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\EntryPoints;
use Espo\Repositories\Attachment as AttachmentRepository;
use Espo\Core\Acl;
use Espo\Core\Api\Request;
use Espo\Core\Api\Response;
use Espo\Core\EntryPoint\EntryPoint;
use Espo\Core\Exceptions\BadRequest;
use Espo\Core\Exceptions\Error;
use Espo\Core\Exceptions\ForbiddenSilent;
use Espo\Core\Exceptions\NotFound;
use Espo\Core\Exceptions\NotFoundSilent;
use Espo\Core\FileStorage\Manager as FileStorageManager;
use Espo\Core\ORM\EntityManager;
use Espo\Core\Utils\Config;
use Espo\Core\Utils\File\Manager as FileManager;
use Espo\Core\Utils\Metadata;
use Espo\Entities\Attachment;
use GdImage;
use RuntimeException;
use Throwable;
class Image implements EntryPoint
{
/** @var ?string[] */
protected $allowedRelatedTypeList = null;
/** @var ?string[] */
protected $allowedFieldList = null;
public function __construct(
private FileStorageManager $fileStorageManager,
private FileManager $fileManager,
protected Acl $acl,
protected EntityManager $entityManager,
protected Config $config,
protected Metadata $metadata
) {}
public function run(Request $request, Response $response): void
{
$id = $request->getQueryParam('id');
$size = $request->getQueryParam('size') ?? null;
if (!$id) {
throw new BadRequest("No id.");
}
$this->show($response, $id, $size);
}
/**
* @throws Error
* @throws NotFoundSilent
* @throws NotFound
* @throws ForbiddenSilent
*/
protected function show(
Response $response,
string $id,
?string $size,
bool $disableAccessCheck = false,
bool $noCacheHeaders = false,
): void {
$attachment = $this->entityManager->getRDBRepositoryByClass(Attachment::class)->getById($id);
if (!$attachment) {
throw new NotFoundSilent("Attachment not found.");
}
if (!$disableAccessCheck && !$this->acl->checkEntity($attachment)) {
throw new ForbiddenSilent("No access to attachment.");
}
$fileType = $attachment->getType();
if (!in_array($fileType, $this->getAllowedFileTypeList())) {
throw new ForbiddenSilent("Not allowed file type '$fileType'.");
}
if ($this->allowedRelatedTypeList) {
if (!in_array($attachment->getRelatedType(), $this->allowedRelatedTypeList)) {
throw new NotFoundSilent("Not allowed related type.");
}
}
if ($this->allowedFieldList) {
if (!in_array($attachment->getTargetField(), $this->allowedFieldList)) {
throw new NotFoundSilent("Not allowed field.");
}
}
$fileSize = 0;
$fileName = $attachment->getName();
$toResize = $size && in_array($fileType, $this->getResizableFileTypeList());
if ($toResize) {
$contents = $this->getThumbContents($attachment, $size);
if ($contents) {
$fileName = $size . '-' . $attachment->getName();
$fileSize = strlen($contents);
$response->writeBody($contents);
} else {
$toResize = false;
}
}
if (!$toResize) {
$stream = $this->fileStorageManager->getStream($attachment);
$fileSize = $stream->getSize() ?? $this->fileStorageManager->getSize($attachment);
$response->setBody($stream);
}
if ($fileType) {
$response->setHeader('Content-Type', $fileType);
}
$response
->setHeader('Content-Disposition', 'inline;filename="' . $fileName . '"')
->setHeader('Content-Length', (string) $fileSize)
->setHeader('Content-Security-Policy', "default-src 'self'");
if (!$noCacheHeaders) {
$response->setHeader('Cache-Control', 'private, max-age=864000, immutable');
}
}
/**
* @throws Error
* @throws NotFound
*/
private function getThumbContents(Attachment $attachment, string $size): ?string
{
if (!array_key_exists($size, $this->getSizes())) {
throw new Error("Bad size.");
}
$useCache = !$this->config->get('thumbImageCacheDisabled', false);
$sourceId = $attachment->getSourceId();
$cacheFilePath = "data/upload/thumbs/{$sourceId}_$size";
if ($useCache && $this->fileManager->isFile($cacheFilePath)) {
return $this->fileManager->getContents($cacheFilePath);
}
$filePath = $this->getAttachmentRepository()->getFilePath($attachment);
if (!$this->fileManager->isFile($filePath)) {
throw new NotFound("File not found.");
}
$fileType = $attachment->getType() ?? '';
$targetImage = $this->createThumbImage($filePath, $fileType, $size);
if (!$targetImage) {
return null;
}
ob_start();
switch ($fileType) {
case 'image/jpeg':
imagejpeg($targetImage);
break;
case 'image/png':
imagepng($targetImage);
break;
case 'image/gif':
imagegif($targetImage);
break;
case 'image/webp':
imagewebp($targetImage);
break;
}
$contents = ob_get_contents() ?: '';
ob_end_clean();
imagedestroy($targetImage);
if ($useCache) {
$this->fileManager->putContents($cacheFilePath, $contents);
}
return $contents;
}
/**
* @throws Error
*/
private function createThumbImage(string $filePath, string $fileType, string $size): ?GdImage
{
if (!is_array(getimagesize($filePath))) {
throw new Error();
}
[$originalWidth, $originalHeight] = getimagesize($filePath);
[$width, $height] = $this->getSizes()[$size];
if ($originalWidth <= $width && $originalHeight <= $height) {
$targetWidth = $originalWidth;
$targetHeight = $originalHeight;
} else {
if ($originalWidth > $originalHeight) {
$targetWidth = $width;
$targetHeight = (int) ($originalHeight / ($originalWidth / $width));
if ($targetHeight > $height) {
$targetHeight = $height;
$targetWidth = (int) ($originalWidth / ($originalHeight / $height));
}
} else {
$targetHeight = $height;
$targetWidth = (int) ($originalWidth / ($originalHeight / $height));
if ($targetWidth > $width) {
$targetWidth = $width;
$targetHeight = (int) ($originalHeight / ($originalWidth / $width));
}
}
}
if ($targetWidth < 1) {
$targetWidth = 1;
}
if ($targetHeight < 1) {
$targetHeight = 1;
}
$targetImage = imagecreatetruecolor($targetWidth, $targetHeight);
if ($targetImage === false) {
return null;
}
switch ($fileType) {
case 'image/jpeg':
$sourceImage = imagecreatefromjpeg($filePath);
if ($sourceImage === false) {
return null;
}
$this->resample(
$targetImage,
$sourceImage,
$targetWidth,
$targetHeight,
$originalWidth,
$originalHeight
);
break;
case 'image/png':
$sourceImage = imagecreatefrompng($filePath);
if ($sourceImage === false) {
return null;
}
imagealphablending($targetImage, false);
imagesavealpha($targetImage, true);
$transparent = imagecolorallocatealpha($targetImage, 255, 255, 255, 127);
if ($transparent !== false) {
imagefilledrectangle($targetImage, 0, 0, $targetWidth, $targetHeight, $transparent);
}
$this->resample(
$targetImage,
$sourceImage,
$targetWidth,
$targetHeight,
$originalWidth,
$originalHeight
);
break;
case 'image/gif':
$sourceImage = imagecreatefromgif($filePath);
if ($sourceImage === false) {
return null;
}
$this->resample(
$targetImage,
$sourceImage,
$targetWidth,
$targetHeight,
$originalWidth,
$originalHeight
);
break;
case 'image/webp':
try {
$sourceImage = imagecreatefromwebp($filePath);
} catch (Throwable) {
return null;
}
if ($sourceImage === false) {
return null;
}
$this->resample(
$targetImage,
$sourceImage,
$targetWidth,
$targetHeight,
$originalWidth,
$originalHeight
);
break;
}
if (in_array($fileType, $this->getFixOrientationFileTypeList())) {
$targetImage = $this->fixOrientation($targetImage, $filePath);
}
return $targetImage;
}
/**
* @param string $filePath
* @return ?int
*/
private function getOrientation(string $filePath)
{
if (!function_exists('exif_read_data')) {
return 0;
}
$data = exif_read_data($filePath) ?: [];
return $data['Orientation'] ?? null;
}
private function fixOrientation(GdImage $targetImage, string $filePath): GdImage
{
$orientation = $this->getOrientation($filePath);
if ($orientation) {
$angle = [0, 0, 0, 180, 0, 0, -90, 0, 90][$orientation] ?? 0;
$targetImage = imagerotate($targetImage, $angle, 0) ?: $targetImage;
}
return $targetImage;
}
/**
* @return string[]
*/
private function getAllowedFileTypeList(): array
{
return $this->metadata->get(['app', 'image', 'allowedFileTypeList']) ?? [];
}
/**
* @return string[]
*/
private function getResizableFileTypeList(): array
{
return $this->metadata->get(['app', 'image', 'resizableFileTypeList']) ?? [];
}
/**
* @return string[]
*/
private function getFixOrientationFileTypeList(): array
{
return $this->metadata->get(['app', 'image', 'fixOrientationFileTypeList']) ?? [];
}
/**
* @return array<string, array{int, int}>
*/
protected function getSizes(): array
{
return $this->metadata->get(['app', 'image', 'sizes']) ?? [];
}
private function getAttachmentRepository(): AttachmentRepository
{
/** @var AttachmentRepository */
return $this->entityManager->getRepository(Attachment::ENTITY_TYPE);
}
private function resample(
GdImage $targetImage,
GdImage $sourceImage,
int $targetWidth,
int $targetHeight,
int $originalWidth,
int $originalHeight
): void {
imagecopyresampled(
$targetImage,
$sourceImage,
0, 0, 0, 0,
$targetWidth, $targetHeight, $originalWidth, $originalHeight
);
}
}

View File

@@ -0,0 +1,99 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2026 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Hooks\Attachment;
use Espo\Core\FileStorage\Manager as FileStorageManager;
use Espo\Core\Hook\Hook\AfterRemove;
use Espo\Core\Utils\File\Manager as FileManager;
use Espo\Core\Utils\Metadata;
use Espo\Entities\Attachment;
use Espo\ORM\Entity;
use Espo\ORM\EntityManager;
use Espo\ORM\Repository\Option\RemoveOptions;
/**
* @implements AfterRemove<Attachment>
*/
class RemoveFile implements AfterRemove
{
public function __construct(
private Metadata $metadata,
private EntityManager $entityManager,
private FileManager $fileManager,
private FileStorageManager $fileStorageManager
) {}
/**
* @param Attachment $entity
*/
public function afterRemove(Entity $entity, RemoveOptions $options): void
{
$duplicateCount = $this->entityManager
->getRDBRepositoryByClass(Attachment::class)
->where([
'OR' => [
'sourceId' => $entity->getSourceId(),
'id' => $entity->getSourceId(),
]
])
->count();
if ($duplicateCount) {
return;
}
if ($this->fileStorageManager->exists($entity)) {
$this->fileStorageManager->unlink($entity);
}
$this->removeThumbs($entity);
}
private function removeThumbs(Attachment $entity): void
{
/** @var string[] $typeList */
$typeList = $this->metadata->get(['app', 'image', 'resizableFileTypeList']) ?? [];
if (!in_array($entity->getType(), $typeList)) {
return;
}
/** @var string[] $sizeList */
$sizeList = array_keys($this->metadata->get(['app', 'image', 'sizes']) ?? []);
foreach ($sizeList as $size) {
$filePath = "data/upload/thumbs/{$entity->getSourceId()}_{$size}";
if ($this->fileManager->isFile($filePath)) {
$this->fileManager->removeFile($filePath);
}
}
}
}

View File

@@ -0,0 +1,380 @@
{
"labels": {
"Enabled": "Povoleno",
"Disabled": "Zakázáno",
"System": "Systém",
"Users": "Uživatelé",
"Customization": "Přizpůsobení",
"Available Fields": "Dostupná pole",
"Layout": "Vzhled",
"Entity Manager": "Správa entit",
"Add Panel": "Přidat panel",
"Add Field": "Přidat pole",
"Settings": "Nastavení",
"Scheduled Jobs": "Naplánované akce",
"Upgrade": "Aktualizace",
"Clear Cache": "Vyčistit cache",
"Rebuild": "Přestavět",
"Teams": "Týmy",
"Roles": "Role",
"Portal": "Portál",
"Portals": "Portály",
"Portal Roles": "Role portálu",
"Outbound Emails": "Odchozí emaily",
"Group Email Accounts": "Skupinové e-mailové účty",
"Personal Email Accounts": "Osobní e-mailové účty",
"Inbound Emails": "Příchozí emaily",
"Email Templates": "Šablony emailů",
"Layout Manager": "Správa layoutu",
"User Interface": "Uživatelské rozhraní",
"Auth Tokens": "Autentizační tokeny",
"Authentication": "Autentizace",
"Currency": "Měna",
"Integrations": "Integrace",
"Extensions": "Rozšíření",
"Upload": "Nahrát",
"Installing...": "Instaluji...",
"Upgrading...": "Upgraduji...",
"Upgraded successfully": "Úspěšně upgradováno",
"Installed successfully": "Úspěšně nainstalováno",
"Ready for upgrade": "Připraveno k upgradu",
"Run Upgrade": "Spustit upgrade",
"Install": "Instalovat",
"Ready for installation": "Připraveno k instalaci",
"Uninstalling...": "Odebírám...",
"Uninstalled": "Odebráno",
"Create Entity": "Vytvořit entitu",
"Edit Entity": "Upravit entitu",
"Create Link": "Vytvořit vazbu",
"Edit Link": "Upravit link",
"Notifications": "Upozornění",
"Jobs": "Joby",
"Reset to Default": "Obnovit do základního nastavení",
"Email Filters": "E-mailové filtry",
"Portal Users": "Uživatelé portálu",
"Action History": "Historie akcí",
"Label Manager": "Správce labelů",
"Auth Log": "Log autentizace",
"Lead Capture": "Zachycení potenciálů",
"Attachments": "Přílohy",
"API Users": "API uživatelé",
"Template Manager": "Správce šablon",
"System Requirements": "Požadavky na systém",
"PHP Settings": "Nastavení PHP",
"Database Settings": "Nastavení databáze",
"Permissions": "Oprávnění",
"Success": "Úspěch",
"Fail": "Selhání",
"is recommended": "je doporučeno",
"extension is missing": "rozšíření chybí",
"PDF Templates": "PDF Šablony",
"Webhooks": "Webhooky",
"Dashboard Templates": "Šablony hlavních panelů",
"Email Addresses": "Emailové adresy",
"Phone Numbers": "Telefonní čísla",
"Layout Sets": "Sady vzhledů",
"Messaging": "Zprávy",
"Misc": "Vedlejší",
"Job Settings": "Nastavení jobů",
"Configuration Instructions": "Instrukce k nastavení",
"Formula Sandbox": "Pískoviště pro formula skripty",
"Working Time Calendars": "Kalendáře pracovní doby",
"Group Email Folders": "Složky skupinových e-mailů",
"Authentication Providers": "Poskytovatelé autentizace",
"Setup": "Nastavení",
"App Log": "Log aplikace",
"Address Countries": "Seznam zemí",
"App Secrets": "Tajemství aplikace",
"OAuth Providers": "OAuth poskytovatelé"
},
"layouts": {
"list": "Seznam",
"listSmall": "Seznam (malý)",
"detailSmall": "Detail (malý)",
"filters": "Vyhledávací filtry",
"massUpdate": "Hromadný update",
"relationships": "Vztah",
"sidePanelsDetail": "Boční panely (Detail)",
"sidePanelsEdit": "Boční panely (Upravit)",
"sidePanelsDetailSmall": "Boční panely (Detail malé)",
"sidePanelsEditSmall": "Boční panely (Upravit malé)",
"detailPortal": "Detail (Portál)",
"detailSmallPortal": "Detail (Small, Portál)",
"listSmallPortal": "Seznam malý (Portál)",
"listPortal": "Seznam (portál)",
"relationshipsPortal": "Panely vztahů (Portál)",
"defaultSidePanel": "Pole bočního panelu",
"bottomPanelsDetail": "Spodní panely",
"bottomPanelsEdit": "Spodní panely (Upravit)",
"bottomPanelsDetailSmall": "Spodní panely (Detail malé)",
"bottomPanelsEditSmall": "Spodní panely (Upravit malé)"
},
"fieldTypes": {
"address": "Adresa",
"array": "Pole",
"foreign": "Cizí pole",
"duration": "Trvání",
"password": "Heslo",
"personName": "Jméno osoby",
"autoincrement": "Číslo (automaticky zvyšované)",
"bool": "Ano/Ne",
"currency": "Měna",
"date": "Datum",
"enum": "Výběr",
"enumInt": "Výběr (číslo)",
"enumFloat": "Výběr (desetinné číslo)",
"float": "Číslo (desetinné)",
"link": "Vazba",
"linkMultiple": "Vazba (vícenásobná)",
"linkParent": "Vazba (rodič)",
"phone": "Telefon",
"url": "URL adresa",
"file": "Soubor",
"image": "Obrázek",
"multiEnum": "Výběr (vícenásobný)",
"attachmentMultiple": "Více příloh",
"rangeInt": "Rozsah (celé číslo)",
"rangeFloat": "Rozsah (desetinné číslo)",
"rangeCurrency": "Rozsah (měna)",
"wysiwyg": "WYSIWYG editor",
"map": "Mapa",
"currencyConverted": "Měna (převedená)",
"colorpicker": "Výběr barvy",
"int": "Číslo (celé)",
"number": "Číslo faktury",
"jsonArray": "JSON pole",
"jsonObject": "JSON objekt",
"datetime": "Datum a čas",
"datetimeOptional": "Datum/Datum a čas",
"checklist": "Ano/Ne (seznam)",
"linkOne": "Vazba (jednonásobná)",
"barcode": "Čárový kód",
"urlMultiple": "URL adresy (více)",
"base": "Výchozí",
"decimal": "Desetinné číslo"
},
"fields": {
"type": "Typ",
"name": "Jméno",
"label": "Popisek",
"required": "Povinné",
"default": "Výchozí",
"maxLength": "Maximální délka",
"options": "Možnosti",
"after": "Po (pole)",
"before": "Před (pole)",
"link": "Odkaz",
"field": "Pole",
"min": "Minimum",
"max": "Maximum",
"translation": "Překlad",
"previewSize": "Velikost náhledu",
"defaultType": "Výchozí typ",
"seeMoreDisabled": "Zakázat ořez textu",
"entityList": "Seznam entit",
"isSorted": "Je seřazeno (abecedně)",
"audited": "Auditováno",
"trim": "Oříznout",
"height": "Výška (px)",
"minHeight": "Minimální výška (px)",
"provider": "Poskytovatel",
"typeList": "Seznam typů",
"lengthOfCut": "Délka řezu",
"sourceList": "Seznam zdrojů",
"tooltipText": "Text nápovědy",
"prefix": "Předpona",
"nextNumber": "Další číslo",
"padLength": "Délka výplně",
"disableFormatting": "Zakázat formátování",
"dynamicLogicVisible": "Podmínky, za kterých je pole viditelné",
"dynamicLogicReadOnly": "Podmínky, za kterých je pole jenom pro čtení",
"dynamicLogicRequired": "Podmínky, za kterých je pole povinné",
"dynamicLogicOptions": "Podmíněné možnosti",
"probabilityMap": "Pravděpodobnosti fáze (%)",
"readOnly": "Pouze ke čtení",
"noEmptyString": "Neprázdný řetězec",
"maxFileSize": "Maximální velikost souboru (Mb)",
"isPersonalData": "Jsou osobní údaje",
"useIframe": "Použít iframe",
"useNumericFormat": "Použít číselný formát",
"strip": "Odstranit",
"cutHeight": "Oříznout výšku (px)",
"minuteStep": "Minutový krok",
"inlineEditDisabled": "Zakázat samostatnou úpravu",
"displayAsLabel": "Zobrazit jako štítek",
"allowCustomOptions": "Povolit vlastní možnosti",
"maxCount": "Maximální počet položek",
"displayRawText": "Zobrazit holý text (bez označení)",
"notActualOptions": "Neopravdové možnosti",
"accept": "Přijmout",
"displayAsList": "Zobrazit jako seznam",
"viewMap": "Zobrazit mapu",
"codeType": "Typ kódu",
"lastChar": "Poslední znak",
"listPreviewSize": "Velikost náhledu seznamu",
"onlyDefaultCurrency": "Pouze výchozí měna",
"dynamicLogicInvalid": "Podmínky, které pole dělají neplatným",
"conversionDisabled": "Konverze zakázána",
"decimalPlaces": "Počet desetinných míst",
"pattern": "Vzor",
"globalRestrictions": "Globální omezení",
"decimal": "Desetinné",
"optionsReference": "Odkaz na možnosti",
"copyToClipboard": "Tlačítko na zkopírování do schránky",
"rows": "Počet řádků textové oblasti",
"readOnlyAfterCreate": "Pouze ke čtení po vytvoření",
"createButton": "Tlačítko pro vytváření",
"autocompleteOnEmpty": "Doplňování při prázdném poli",
"relateOnImport": "Provázat při importu",
"aclScope": "Entita pro acl",
"onlyAdmin": "Pouze pro administrátory",
"activeOptions": "Aktivní možnosti",
"labelType": "Typ zobrazení",
"preview": "Náhled",
"attachmentField": "Pole pro přílohu",
"dynamicLogicReadOnlySaved": "Podmínky, za kterých je pole jenom pro čtení (po uložení)",
"notStorable": "Neuložitelné",
"itemsEditable": "Upravitelné položky"
},
"messages": {
"selectEntityType": "Vybrat entitu v levém menu.",
"selectUpgradePackage": "Vybrat upgrade balíček",
"selectLayout": "Vybrat požadovaný layout v levém menu a upravit ho.",
"selectExtensionPackage": "Vybrat soubor s rozšířením",
"extensionInstalled": "Rozšíření {name} {version} bylo nainstalováno.",
"installExtension": "Rozšíření {name} {version} je připraveno k instalaci.",
"upgradeBackup": "Doporučujeme zálohovat soubory a data EspoCRM před upgradem.",
"thousandSeparatorEqualsDecimalMark": "Oddělovač tisíců nemůže být stejný jako desetinný symbol.",
"userHasNoEmailAddress": "Uživatel nemá emailovou adresu.",
"uninstallConfirmation": "Opravdu odinstalovat vybrané rozšíření?",
"cronIsNotConfigured": "Naplánované úlohy nejsou spuštěny. Příchozí e-maily, oznámení a připomenutí proto nefungují. Postupujte podle [pokynů](https://www.espocrm.com/documentation/administration/server-configuration/#user-content-setup-a-crontab) k nastavení úlohy cron.",
"newExtensionVersionIsAvailable": "Je k dispozici nová verze {latestName} {latestVersion}.",
"upgradeVersion": "EspoCRM bude upgradováno na verzi <strong>{version}</strong>. Toto může chvíli trvat.",
"upgradeDone": "EspoCRM bylo upgradováno na verzi <strong>{version}</strong>.",
"downloadUpgradePackage": "Stáhnout upgradovací balíčky na [tomto]({url}) odkaze.",
"upgradeInfo": "Přečtěte si [dokumentaci]({url}) o tom, jak upgradovat instanci AutoCRM.",
"upgradeRecommendation": "Tento způsob upgradu se nedoporučuje. Je lepší upgradovat z CLI.",
"newVersionIsAvailable": "K dispozici je nová verze AutoCRM {latestVersion}. Při aktualizaci instance postupujte podle [pokynů](https://www.espocrm.com/documentation/administration/upgrading/).",
"formulaFunctions": "Funkce formula skriptů",
"rebuildRequired": "Musíte spustit znovu rebuild z CLI.",
"cronIsDisabled": "Cron je zakázán",
"cacheIsDisabled": "Cache je zakázána"
},
"descriptions": {
"settings": "Systémová nastavení aplikace.",
"scheduledJob": "Činnosti vykonávané CRONem.",
"upgrade": "Upgradovat EspoCRM.",
"clearCache": "Vyčistit veškerou cache.",
"rebuild": "Přestavět backend a vyčistit cache.",
"users": "Správa uživatelů.",
"teams": "Správa týmů.",
"roles": "Správa rolí.",
"portals": "Správa portálů.",
"portalRoles": "Role pro portál.",
"outboundEmails": "Nastavení SMTP pro odchozí emaily.",
"groupEmailAccounts": "Skupinové IMAP emailové účty. Import emailů",
"personalEmailAccounts": "E-mailové účty uživatelů.",
"emailTemplates": "Šablony pro odchozí emaily.",
"import": "Importovat data z CSV souboru.",
"layoutManager": "Přizpůsobit layouty (seznam, detail, upravit, hledat, hromadný update).",
"userInterface": "Nastavit uživatelské rozhraní.",
"authTokens": "Aktivní autentizační sessions. IP adresa a datum posledního přístupu.",
"authentication": "Nastavení autentizace.",
"currency": "Nastavení měn a kurzů.",
"extensions": "Instalovat a odebrat rozšíření.",
"integrations": "Integrace se službami třetích stran.",
"notifications": "Nastavení In-app a emailových upozornění.",
"inboundEmails": "Nastavení příchozích mailů",
"portalUsers": "Uživatelé portálu.",
"entityManager": "Vytvořit vlastní entity, úpravit existující. Správa polí a vztahů.",
"emailFilters": "E-mailové zprávy, které odpovídají zadanému filtru, nebudou importovány.",
"actionHistory": "Protokol akcí uživatelů.",
"labelManager": "Upravit popisky",
"authLog": "Historie přihlášení.",
"attachments": "Všechny přílohy souborů uložené v systému.",
"templateManager": "Přizpůsobte si šablony zpráv.",
"systemRequirements": "Systémové požadavky na AutoCRM.",
"apiUsers": "Oddělte uživatele pro účely integrace.",
"jobs": "Spustit akce na pozadí.",
"pdfTemplates": "Šablony pro tisk do PDF.",
"webhooks": "Správa webhooků.",
"dashboardTemplates": "Umožňuje přidávat dashboardy uživatelům.",
"phoneNumbers": "Všechna telefonní čísla uložená v systému.",
"emailAddresses": "Všechny e-mailové adresy uložené v systému.",
"layoutSets": "Kolekce layoutů, které lze přiřadit týmům a portálům.",
"jobsSettings": "Nastavení zpracování jobů. Joby vykonávají úkoly na pozadí.",
"sms": "Nastavení SMS.",
"formulaSandbox": "Pískoviště pro testování formula skriptů bez ukládání změn.",
"workingTimeCalendars": "Pracovní plány.",
"groupEmailFolders": "Složky sdílené pro týmy",
"authenticationProviders": "Další poskytovatelé autentizace pro portály.",
"appLog": "Log aplikace.",
"addressCountries": "Dostupné země pro políčka typu 'adresa'.",
"appSecrets": "Pro ukládání citlivých informací jako jsou API klíče, hesla, a jiná tajemství.",
"leadCapture": "Koncové body pro zachycení potenciálů a webové formuláře.",
"oAuthProviders": "OAuth poskytovatelé pro integrace."
},
"options": {
"previewSize": {
"x-small": "Extra-malý",
"small": "Malý",
"medium": "Střední",
"large": "Velký",
"": "Prázdné"
},
"labelType": {
"state": "Stav",
"regular": "Výchozí"
}
},
"logicalOperators": {
"and": "a zároveň",
"or": "nebo",
"not": "negace"
},
"systemRequirements": {
"requiredPhpVersion": "Požadovaná verze PHP",
"requiredMysqlVersion": "Požadovaná verze MySQL",
"host": "Jméno hostitele",
"dbname": "Název databáze",
"user": "Uživatel",
"writable": "Zapisovatelné",
"readable": "Čitelné",
"requiredMariadbVersion": "Požadovaná verze MariaDB",
"requiredPostgresqlVersion": "Požadovaná verze PostgreSQL"
},
"templates": {
"accessInfo": "Přístupové údaje",
"accessInfoPortal": "Přístupové údaje na portály",
"assignment": "Úkol",
"mention": "Zmínka",
"notePost": "Poznámka k příspěvku",
"notePostNoParent": "Poznámka k příspěvku (bez rodiče)",
"noteStatus": "Poznámka k aktualizaci stavu",
"passwordChangeLink": "Odkaz na změnu hesla",
"noteEmailReceived": "Poznámka o přijatém e-mailu",
"twoFactorCode": "Dvoufaktorový kód"
},
"strings": {
"rebuildRequired": "Rebuild je vyžadován."
},
"keywords": {
"settings": "nastavení",
"userInterface": "uživatelské rozhraní",
"scheduledJob": "naplánovaná akce",
"integrations": "integrace",
"authLog": "log autentizace",
"authTokens": "autentizační tokeny",
"entityManager": "správce entit",
"templateManager": "správce šablon",
"jobs": "úlohy",
"authentication": "autentizace",
"labelManager": "správce popisků",
"appSecrets": "tajemství aplikace",
"leadCapture": "zachycení potenciálů"
},
"tooltips": {
"tabUrl": "URL záložky",
"tabUrlAclScope": "ACL rozsah pro záložku URL"
}
}

View File

@@ -0,0 +1,947 @@
{
"scopeNames": {
"User": "Uživatel",
"Team": "Tým",
"EmailTemplate": "Emailová šablona",
"EmailAccount": "Emailový účet",
"EmailAccountScope": "Emailový účet",
"OutboundEmail": "Odchozí email",
"ScheduledJob": "Naplánovaná činnost",
"ExternalAccount": "Externí účet",
"Extension": "Rozšíření",
"Dashboard": "Nástěnka",
"InboundEmail": "Účet příchozích emailů",
"Job": "Úloha",
"EmailFilter": "Emailový filter",
"Portal": "Portál",
"PortalRole": "Portálová role",
"Attachment": "Příloha",
"EmailFolder": "Adresář emailů",
"PortalUser": "Portálový uživatel",
"ScheduledJobLogRecord": "Záznam logu plánované úlohy",
"PasswordChangeRequest": "Změna hesla",
"ActionHistoryRecord": "Záznam historie",
"AuthToken": "Autentizační token",
"UniqueId": "Jedinečné ID",
"LastViewed": "Naposledy zobrazeno",
"Settings": "Nastavení",
"FieldManager": "Správa polí",
"Integration": "Integrace",
"LayoutManager": "Správa vzhledu",
"EntityManager": "Správa entit",
"DynamicLogic": "Dynamická logika",
"DashletOptions": "Volby dashletu",
"Global": "Globální",
"Preferences": "Předvolby",
"EmailAddress": "Emailová adresa",
"PhoneNumber": "Telefonní číslo",
"AuthLogRecord": "Záznam přihlášení",
"AuthFailLogRecord": "Seznam selhaných přihlášení",
"EmailTemplateCategory": "Kategorie emailových šablon",
"LeadCapture": "Zachycení Potenciálu",
"LeadCaptureLogRecord": "Log zachycení Potenciálu",
"ArrayValue": "Hodnota pole",
"ApiUser": "API uživatel",
"DashboardTemplate": "Šablona hlavního panelu",
"Currency": "Měna",
"LayoutSet": "Nastavení vzhledu",
"Mass Action": "Hromadná akce",
"Note": "Poznámka",
"ImportError": "Chyba importu",
"WorkingTimeCalendar": "Pracovní kalendář",
"GroupEmailFolder": "Skupinová emailová složka",
"AuthenticationProvider": "Poskytovatel autentizace",
"GlobalStream": "Globální události",
"WebhookQueueItem": "Položka fronty webhook",
"AppLogRecord": "Záznam aplikačního logu",
"WorkingTimeRange": "Rozmezí pracovní doby",
"AddressCountry": "Země adresy",
"AppSecret": "Aplikační tajemství",
"OAuthProvider": "OAuth poskytovatel",
"OAuthAccount": "OAuth účet",
"WebhookEventQueueItem": "Položka fronty webhook událostí",
"Template": "Šablona"
},
"scopeNamesPlural": {
"Email": "Emaily",
"User": "Uživatelé",
"Team": "Týmy",
"Role": "Role",
"EmailTemplate": "Emailové šablony",
"EmailAccount": "Emailové účty",
"EmailAccountScope": "Emailové účty",
"OutboundEmail": "Odchozí emaily",
"ScheduledJob": "Naplánované činnosti",
"ExternalAccount": "Externí účty",
"Extension": "Rozšíření",
"Dashboard": "Nástěnka",
"InboundEmail": "Účty příchozích emailů",
"Job": "Úlohy",
"EmailFilter": "Filtry emailu",
"Portal": "Portály",
"PortalRole": "Role portálu",
"Attachment": "Přilohy",
"EmailFolder": "Adresáře emailu",
"PortalUser": "Uživatelé portálu",
"ScheduledJobLogRecord": "Záznamy logu plánovaných úloh",
"PasswordChangeRequest": "Požadavek na změnu hesla",
"ActionHistoryRecord": "Historie akcí",
"AuthToken": "Autentizační tokeny",
"UniqueId": "Jedinečná ID",
"LastViewed": "Naposledy zobrazeno",
"AuthLogRecord": "Autentizační log",
"AuthFailLogRecord": "Log selhání autentizace",
"EmailTemplateCategory": "Kategorie e-mailových šablon",
"Import": "Výsledky importu",
"LeadCapture": "Zachycení potenciálů",
"LeadCaptureLogRecord": "Log zachycení potenciálů",
"ArrayValue": "Hodnoty pole",
"ApiUser": "API uživatelé",
"DashboardTemplate": "Šablona hlavního panelu",
"Webhook": "Webhooky",
"EmailAddress": "Emailové adresy",
"PhoneNumber": "Tel. čísla",
"Currency": "Měna",
"LayoutSet": "Sady rozložení",
"Note": "Poznámky",
"ImportError": "Chyby importu",
"WorkingTimeCalendar": "Pracovní kalendáře",
"GroupEmailFolder": "Skupinové emailové složky",
"AuthenticationProvider": "Poskytovatelé autentizace",
"GlobalStream": "Globální stream",
"WebhookQueueItem": "Položky fronty webhook",
"AppLogRecord": "Aplikační log",
"WorkingTimeRange": "Rozmezí pracovní doby",
"AddressCountry": "Země adres",
"AppSecret": "Aplikační tajemství",
"OAuthProvider": "OAuth poskytovatelé",
"OAuthAccount": "OAuth účty",
"WebhookEventQueueItem": "Položky fronty webhook událostí",
"Template": "Šablony"
},
"labels": {
"Misc": "Vedlejší",
"Merge": "Sloučit",
"None": "-",
"Home": "Domů",
"by": "dle",
"Saved": "Uloženo",
"Error": "Chyba",
"Select": "Vybrat",
"Not valid": "Neplatné",
"Please wait...": "Prosím počkejte...",
"Please wait": "Prosím počkejte",
"Loading...": "Nahrávání...",
"Uploading...": "Uploaduje se...",
"Sending...": "Posílá se...",
"Merged": "Sloučeno",
"Removed": "Odstraněno",
"Posted": "Zasláno",
"Linked": "Nalinkováno",
"Unlinked": "Odlinkováno",
"Done": "Hotovo",
"Access denied": "Přístup odepřen",
"Not found": "Nenalezeno",
"Access": "Přístup",
"Are you sure?": "Jste si jisti?",
"Record has been removed": "Záznam byl odstraněn",
"Wrong username/password": "Neplatné přihlašovací jméno/heslo",
"Post cannot be empty": "Příspěvek nemůže být prázdný",
"Username can not be empty!": "Přihlašovací jméno nemůže být prázdné!",
"Cache is not enabled": "Cache není povolena",
"Cache has been cleared": "Cache byla vyčištěna",
"Rebuild has been done": "Přestavba byla dokončena",
"Modified": "Modifikováno",
"Created": "Vytvořeno",
"Create": "Vytvořit",
"create": "vytvořit",
"Overview": "Přehled",
"Details": "Detaily",
"Add Field": "Přidat pole",
"Add Dashlet": "Přidat panel",
"Filter": "Filtr",
"Edit Dashboard": "Upravit nástěnku",
"Add": "Přidat",
"Add Item": "Přidat položku",
"Reset": "Resetovat",
"More": "Více",
"Search": "Hledat",
"Only My": "Pouze moje",
"Open": "Otevřený",
"About": "O AutoCRM",
"Refresh": "Obnovit",
"Remove": "Odebrat",
"Options": "Možnosti",
"Username": "Uživatelské jméno",
"Password": "Heslo",
"Login": "Přihlásit",
"Log Out": "Odhlásit",
"Preferences": "Předvolby",
"State": "Kraj",
"Street": "Ulice",
"Country": "Země",
"City": "Město",
"PostalCode": "PSČ",
"Followed": "Sledováno",
"Follow": "Sledovat",
"Followers": "Sledující",
"Clear Local Cache": "Vyčistit lokální cache",
"Actions": "Akce",
"Delete": "Smazat",
"Update": "Aktualizovat",
"Save": "Uložit",
"Edit": "Upravit",
"View": "Zobrazit",
"Cancel": "Zrušit",
"Apply": "Použít",
"Unlink": "Odlinkovat",
"Mass Update": "Hromadný update",
"No Data": "Žádná data",
"No Access": "Nepřístupno",
"All": "Vše",
"Active": "Aktivní",
"Inactive": "Neaktivní",
"Write your comment here": "Napište Vás komentář",
"Post": "Poslat",
"Show more": "Ukázat více",
"Dashlet Options": "Možnosti panelu",
"Full Form": "Plný formulář",
"Insert": "Vložit",
"Person": "Osoba",
"First Name": "Křestní jméno",
"Last Name": "Příjmení",
"Original": "Originál",
"You": "Vy",
"you": "vy",
"change": "změna",
"Change": "Změna",
"Primary": "Primární",
"Save Filter": "Uložit filtr",
"Administration": "Administrace",
"Run Import": "Spustit import",
"Duplicate": "Duplikovat",
"Notifications": "Upozornění",
"Mark all read": "Označit jako přečtené",
"See more": "Zobrazit více",
"Today": "Dnes",
"Tomorrow": "Zítra",
"Yesterday": "Včera",
"Submit": "Vložit",
"Close": "Zavřít",
"Yes": "Ano",
"No": "Ne",
"Value": "Hodnota",
"Current version": "Současná verze",
"List View": "Seznam",
"Tree View": "Stromový pohled",
"Unlink All": "Odlinkovat vše",
"Total": "Celkem",
"Print to PDF": "Tisknout do PDF",
"Default": "Výchozí",
"Number": "Číslo",
"From": "Od",
"To": "Do",
"Create Post": "Vytvořit příspěvek",
"Previous Entry": "Předchozí položka",
"Next Entry": "Další položka",
"View List": "Ukázat seznam",
"Attach File": "Přiložit soubor",
"Skip": "Přeskočit",
"Attribute": "Atribut",
"Function": "Funkce",
"Self-Assign": "Přiřadit sobě",
"Self-Assigned": "Přiřazeno sobě",
"Return to Application": "Návrat do aplikace",
"Select All Results": "Vybrat celý výsledek",
"Expand": "Rozbalit",
"Collapse": "Sbalit",
"New notifications": "Nové notifikace",
"Manage Categories": "Spravovat kategorie",
"Manage Folders": "Spravovat složky",
"Convert to": "Převést na",
"View Personal Data": "Zobrazit osobní data",
"Personal Data": "Osobní data",
"Erase": "Smazat",
"Move Over": "Přesunout",
"Restore": "Obnovit",
"View Followers": "Zobrazit sledující",
"Convert Currency": "Převod měny",
"Middle Name": "Prostřední jméno",
"View on Map": "Zobrazit na mapě",
"Proceed": "Pokračovat",
"Attached": "Přiloženo",
"Preview": "Náhled",
"Up": "Nahoru",
"Save & Continue Editing": "Uložit a pokračovat v úpravách",
"Save & New": "Uložit a nový",
"Field": "Pole",
"Resolution": "Řešení",
"Resolve Conflict": "Řešení konfliktu",
"Download": "Stáhnout",
"Sort": "Seřadit",
"Log in": "Přihlásit se",
"Log in as": "Přihlásit se jako",
"Sign in": "Přihlásit se",
"Global Search": "Globální vyhledávání",
"Navigation Panel": "Zobrazit navigační panel",
"Print": "Tisk",
"Copy to Clipboard": "Zkopírovat do schránky",
"Copied to clipboard": "Zkopírováno do schránky",
"Audit Log": "Historie změn",
"View Audit Log": "Zobrazit historii změn",
"Previous Page": "Předchozí strana",
"Next Page": "Další strana",
"First Page": "První strana",
"Last Page": "Poslední strana",
"Page": "Strana",
"Star": "Označit jako oblíbené",
"Unstar": "Odebrat z oblíbených",
"Starred": "Označeno jako oblíbené",
"Remove Filter": "Odebrat filtr",
"Ready": "Připraveno",
"Column Resize": "Změna velikosti sloupců",
"General": "Obecné",
"Send": "Odeslat",
"Timeout": "Časový limit",
"No internet": "Bez internetu",
"Scheduled": "Naplánováno",
"Now": "Nyní",
"Expanded": "Rozbaleno",
"Collapsed": "Sbaleno",
"Top Level": "Nejvyšší úroveň",
"Fields": "Pole",
"View User Access": "Zobrazit přístup uživatele",
"Reacted": "Reagováno",
"Reaction Removed": "Reakce odebrána",
"Reactions": "Reakce",
"Network error": "Chyba sítě",
"Edit Item": "Upravit položku"
},
"messages": {
"pleaseWait": "Prosím, čekejte...",
"confirmLeaveOutMessage": "Opravdu chcete opustit formulář?",
"notModified": "Záznam nebyl změněn",
"fieldIsRequired": "{field} je povinné",
"fieldShouldAfter": "{field} musí být po {otherField}",
"fieldShouldBefore": "{field} musí být před {otherField}",
"fieldShouldBeBetween": "{field} musí být mezi {min} a {max}",
"fieldBadPasswordConfirm": "{field} nebylo potvrzeno správně",
"resetPreferencesDone": "Preference byly resetovány na výchozí hodnoty",
"confirmation": "Opravdu?",
"unlinkAllConfirmation": "Opravdu chcete odpojit všechny související záznamy?",
"resetPreferencesConfirmation": "Opravdu chcete resetovat preference na výchozí hodnoty?",
"removeRecordConfirmation": "Opravdu chcete odstranit záznam?",
"unlinkRecordConfirmation": "Opravdu chcete odpojit související záznam?",
"removeSelectedRecordsConfirmation": "Opravdu chcete odstranit vybrané záznamy?",
"massUpdateResult": "Bylo aktualizováno {count} záznamů",
"massUpdateResultSingle": "Byl aktualizován {count} záznam",
"noRecordsUpdated": "Žádné záznamy nebyly aktualizovány",
"massRemoveResult": "Bylo odstraněno {count} záznamů",
"massRemoveResultSingle": "Byl odstraněn {count} záznam",
"noRecordsRemoved": "Žádné záznamy nebyly odstraněny",
"clickToRefresh": "Klikněte pro obnovení",
"writeYourCommentHere": "Napište komentář",
"writeMessageToUser": "Napište zprávu uživateli {user}",
"typeAndPressEnter": "Pište a pro odeslání stiskněte enter",
"checkForNewNotifications": "Zkontrolovat nové notifikace",
"duplicate": "Záznam, který vytváříte, pravděpodobně již existuje",
"dropToAttach": "Přetáhněte pro příložení",
"writeMessageToSelf": "Napište poznámku do svého seznamu poznámek",
"checkForNewNotes": "Zkontrolovat nové poznámky",
"internalPost": "Příspěvek uvidí pouze interní uživatelé",
"done": "Hotovo",
"confirmMassFollow": "Opravdu chcete sledovat vybrané záznamy?",
"confirmMassUnfollow": "Opravdu chcete zrušit sledování vybraných záznamů?",
"massFollowResult": "{count} záznamů je nyní sledováno",
"massUnfollowResult": "{count} záznamy nyní nejsou sledovány",
"massFollowResultSingle": "{count} záznam je nyní sledován",
"massUnfollowResultSingle": "{count} záznam nyní není sledován",
"massFollowZeroResult": "dné sledované záznamy",
"massUnfollowZeroResult": "dné záznamy",
"fieldShouldBeEmail": "{field} musí být platný email",
"fieldShouldBeFloat": "{field} musí být platné desetinné číslo",
"fieldShouldBeInt": "{field} musí být platné celé číslo",
"fieldShouldBeDate": "{field} musí být platné datum",
"fieldShouldBeDatetime": "{field} musí být platný datum/čas",
"internalPostTitle": "Příspěvek vidí pouze interní uživatelé",
"loading": "Nahrávání...",
"saving": "Ukládám ...",
"fieldMaxFileSizeError": "Soubor by neměl překročit {max} Mb",
"fieldIsUploading": "Nahrávám",
"erasePersonalDataConfirmation": "Zaškrtnutá pole budou trvale vymazána. Jste si jisti?",
"massPrintPdfMaxCountError": "Není možné vytisknout více než {maxCount} záznamů.",
"fieldValueDuplicate": "Duplicitní hodnota",
"unlinkSelectedRecordsConfirmation": "Opravdu chcete odlinkovat vybrané záznamy?",
"recalculateFormulaConfirmation": "Jste si jisti, že chcete přepočítat záznamy?",
"fieldExceedsMaxCount": "Překročen maximální počet {maxCount}",
"notUpdated": "Soubor nebyl nahrán",
"maintenanceMode": "Aplikace je momentálně v maintanance módu",
"fieldInvalid": "{field} je neplatné",
"fieldPhoneInvalid": "{field} je neplatné",
"resolveSaveConflict": "Záznam byl změněn. Musíte vyřešit konflikt před uložením záznamu.",
"massActionProcessed": "Hromadná operace byla provedena.",
"fieldUrlExceedsMaxLength": "Zakódovaná URL přesahuje maximální délku {maxLength}",
"fieldNotMatchingPattern": "{field} neodpovídá vzoru `{pattern}`",
"fieldNotMatchingPattern$noBadCharacters": "{field} obsahuje nepovolené znaky",
"fieldNotMatchingPattern$noAsciiSpecialCharacters": "{field} by neměl obsahovat speciální ASCII znaky",
"fieldNotMatchingPattern$latinLetters": "{field} může obsahovat pouze latinská písmena",
"fieldNotMatchingPattern$latinLettersDigits": "{field} může obsahovat pouze latinská písmena a číslice",
"fieldNotMatchingPattern$latinLettersDigitsWhitespace": "{field} může obsahovat pouze latinská písmena, číslice a mezery",
"fieldNotMatchingPattern$latinLettersWhitespace": "{field} může obsahovat pouze latinská písmena a mezery",
"fieldNotMatchingPattern$digits": "{field} může obsahovat pouze číslice",
"fieldPhoneInvalidCharacters": "Jsou povoleny pouze číslice, latinská písmena a znaky `-+_@:#().`",
"arrayItemMaxLength": "Položka nesmí být delší než {max} znaků",
"validationFailure": "Serverová validace selhala.\n\nPole: `{field}`\n\nValidace: {type}",
"confirmAppRefresh": "Aplikace byla aktualizována. Doporučujeme obnovit stránku, abyste zajistili její správné fungování.",
"error404": "Vámi požadovaná url nelze zpracovat.",
"extensionLicenseInvalid": "Neplatná licence rozšíření '{name}'.",
"extensionLicenseExpired": "Licence rozšíření '{name}' vypršela.",
"extensionLicenseSoftExpired": "Licence rozšíření '{name}' vypršela.",
"loggedOutLeaveOut": "Byli jste odhlášeni. Relace je neaktivní. Po obnovení stránky můžete ztratit neuložená data formuláře. Možná budete muset vytvořit kopii.",
"noAccessToRecord": "Operace vyžaduje přístup `{action}` k záznamu.",
"noAccessToForeignRecord": "Operace vyžaduje přístup `{action}` k cizímu záznamu.",
"fieldShouldBeNumber": "{field} musí být platné číslo",
"maintenanceModeError": "Aplikace je momentálně v režimu údržby.",
"cannotRelateNonExisting": "Nelze propojit s neexistujícím {foreignEntityType} záznamem.",
"cannotRelateForbidden": "Nelze propojit se zakázaným {foreignEntityType} záznamem. Vyžaduje se přístup `{action}`.",
"cannotRelateForbiddenLink": "K odkazu '{link}' nemáte přístup.",
"emptyMassUpdate": "Žádná pole nejsou k dispozici pro hromadnou aktualizaci.",
"fieldNotMatchingPattern$uriOptionalProtocol": "{field} musí být platná URL",
"fieldShouldBeLess": "{field} nesmí být větší než {value}",
"fieldShouldBeGreater": "{field} nesmí být menší než {value}",
"cannotUnrelateRequiredLink": "Nelze odstranit požadovaný odkaz.",
"fieldPhoneInvalidCode": "Neplatný kód země",
"fieldPhoneTooShort": "{field} je příliš krátké",
"fieldPhoneTooLong": "{field} je příliš dlouhé",
"barcodeInvalid": "{field} není platný {type}",
"noLinkAccess": "Nelze propojit s {foreignEntityType} záznamem pomocí odkazu '{link}'. Přístup zamítnut.",
"attemptIntervalFailure": "Operace není povolena v určeném časovém intervalu. Počkejte před dalším pokusem.",
"confirmRestoreFromAudit": "Předchozí hodnoty budou nastaveny ve formuláři. Poté můžete záznam uložit a obnovit původní hodnoty.",
"pageNumberIsOutOfBound": "Číslo stránky je mimo rozsah",
"fieldPhoneExtensionTooLong": "Přípona by neměla být delší než {maxLength}",
"cannotLinkAlreadyLinked": "Nelze propojit již propojený záznam.",
"starsLimitExceeded": "Počet hvězdiček přesáhl limit.",
"select2OrMoreRecords": "Vyberte 2 nebo více záznamů",
"selectNotMoreThanNumberRecords": "Vyberte nejvýše {number} záznamů",
"selectAtLeastOneRecord": "Vyberte alespoň jeden záznam",
"fieldNotMatchingPattern$phoneNumberLoose": "{field} obsahuje znaky, které nejsou povoleny v telefonním čísle",
"duplicateConflict": "Záznam již existuje.",
"confirmMassUpdate": "Opravdu chcete hromadně aktualizovat vybrané záznamy?",
"cannotRemoveCategoryWithChildCategory": "Nelze odstranit kategorii, která obsahuje podkategorii.",
"cannotRemoveNotEmptyCategory": "Nelze odstranit neprazdnou kategorii.",
"arrayInputNotEmpty": "Položka je zadána, ale není přidána",
"error403": "Do této sekce nemáte přístup.",
"sameRecordIsAlreadyBeingEdited": "Záznam je již editován.",
"changesLossConfirmation": "Neuložené změny budou ztraceny. Opravdu chcete pokračovat?"
},
"boolFilters": {
"onlyMy": "Pouze moje",
"followed": "Sledované",
"onlyMyTeam": "Můj tým",
"shared": "Sdílené"
},
"presetFilters": {
"followed": "Sledované",
"all": "Vše",
"starred": "Označeno jako oblíbené",
"active": "Aktivní"
},
"massActions": {
"remove": "Odstranit",
"merge": "Sloučit",
"massUpdate": "Hromadně upravit",
"export": "Exportovat",
"follow": "Sledovat",
"unfollow": "Přestat sledovat",
"convertCurrency": "Převést měnu",
"printPdf": "Vytisknout do PDF",
"unlink": "Odlinkovat",
"recalculateFormula": "Přepočítat vzorec",
"update": "Aktualizovat",
"delete": "Smazat"
},
"fields": {
"name": "Název",
"firstName": "Křestní jméno",
"lastName": "Příjmení",
"salutationName": "Oslovení",
"assignedUser": "Přiřazený uživatel",
"assignedUsers": "Přiřazení uživatelé",
"assignedUserName": "Přiřazená uživatelská jména",
"teams": "Týmy",
"createdAt": "Vytvořeno",
"modifiedAt": "Upraveno",
"createdBy": "Vytvořil",
"modifiedBy": "Upravil",
"description": "Popis",
"address": "Adresa",
"phoneNumber": "Telefon",
"phoneNumberMobile": "Telefon (Mobil)",
"phoneNumberHome": "Telefon (Domácí)",
"phoneNumberFax": "Telefon (Fax)",
"phoneNumberOffice": "Telefon (Kancelář)",
"phoneNumberOther": "Telefon (Další)",
"order": "Pořadí",
"parent": "Rodič",
"children": "Potomci",
"emailAddressData": "Údaje o e-mailové adrese",
"phoneNumberData": "Telefonní data",
"ids": "ID",
"names": "Názvy",
"emailAddressIsOptedOut": "E-mailová adresa je odhlášena",
"targetListIsOptedOut": "Je vyřazen z listu",
"type": "Typ",
"phoneNumberIsOptedOut": "Telefonní číslo je odhlášené",
"types": "Typy",
"middleName": "Prostřední jméno",
"emailAddressIsInvalid": "E-mailová adresa je neplatná",
"phoneNumberIsInvalid": "Telefonní číslo je neplatné",
"users": "Uživatelé",
"childList": "Seznam potomků",
"collaborators": "Spolupracovníci",
"streamUpdatedAt": "Poslední aktualizace streamu"
},
"links": {
"assignedUser": "Přiřazený uživatel",
"createdBy": "Vytvořil",
"modifiedBy": "Upravil",
"team": "Tým",
"roles": "Role",
"teams": "Týmy",
"users": "Uživatelé",
"parent": "Rodič",
"children": "Potomci",
"assignedUsers": "Přiřazení uživatelé",
"collaborators": "Spolupracovníci"
},
"dashlets": {
"Emails": "Můj Inbox",
"Records": "Záznamy",
"Memo": "Poznámka"
},
"notificationMessages": {
"assign": "{entityType} {entity} Ti byla přiřazena.",
"emailReceived": "Email přijat od {from}",
"entityRemoved": "{user} odstranil {entityType} {entity}",
"emailInbox": "{user} přidal e-mail {entity} do vaší schránky",
"userPostReaction": "{user} reagoval na váš {post}",
"userPostInParentReaction": "{user} reagoval na váš {post} v {entityType} {entity}",
"addedToCollaborators": "{user} vás přidal jako spolupracovníka k {entityType} {entity}"
},
"streamMessages": {
"post": "{user} poslal {entityType} {entity}",
"attach": "{user} připojený {entityType} {entity}",
"status": "{user} upravil {field} k {entityType} {entity}",
"update": "{user} upravil {entityType} {entity}",
"postTargetTeam": "{user} přidal (a) příspěvek do týmu {target}",
"postTargetTeams": "{user} přidal příspěvek týmům {target}",
"postTargetPortal": "{user} přidal na portál {target}",
"postTargetPortals": "{user} přidal na portály {target}",
"postTarget": "{user} přidal příspěvek na {target}",
"postTargetYou": "{user} vám poslal příspěvek",
"postTargetYouAndOthers": "{user} přidal příspěvek na {target} a vám",
"postTargetAll": "{user} přidal příspěvek všem",
"mentionInPost": "{user} zmínil {mentioned} v {entityType} {entity}",
"mentionYouInPost": "{user} se o vás zmínil v {entityType} {entity}",
"mentionInPostTarget": "{uživatel} zmínil {zmínil} v příspěvku",
"mentionYouInPostTarget": "{user} se o vás zmínil v příspěvku pro {target}",
"mentionYouInPostTargetAll": "{user} se o vás zmínil v příspěvku všem",
"mentionYouInPostTargetNoTarget": "{user} se o vás zmínil v příspěvku",
"create": "{user} vytvořil {entityType} {entity}",
"createThis": "{user} vytvořil {entityType}",
"createAssignedThis": "{user} vytvořil {entityType} přiřazené {assignee}",
"createAssigned": "{user} vytvořil {entityType} {entity} přiřazené {assignee}",
"assign": "{user} přiřadil {entityType} {entity} {assignee}",
"assignThis": "{user} přiřadil tento {entityType} uživateli {assignee}",
"assignMultiAddThis": "{user} přiřadil tento {entityType} uživateli {assignee}",
"postThis": "{user} poslal",
"attachThis": "{user} připojil",
"statusThis": "{user} upravil {field}",
"updateThis": "{user} upravil {entityType}",
"createRelatedThis": "{user} vytvořil {relatedEntityType} {relatedEntity} související s {entityType}",
"createRelated": "{user} vytvořil {relatedEntityType} {relatedEntity} související s {entityType} {entity}",
"relate": "{user} propojen {relatedEntityType} {relatedEntity} s {entityType} {entity}",
"relateThis": "{user} propojil {relatedEntityType} {relatedEntity} s tímto {entityType}",
"emailReceivedFromThis": "Email přijat od {from}",
"emailReceivedInitialFromThis": "Tento email byl přijat od {from}, vytvořen {entityType}",
"emailReceivedThis": "Email přijat",
"emailReceivedInitialThis": "Email přijat, vytvořen {entityType}",
"emailReceivedFrom": "Email přijat od {from}, související s {entityType} {entity}",
"emailReceivedFromInitial": "Email přijak od {from}, vytvořeno {entityType} {entity}",
"emailReceivedInitialFrom": "Email přijak od {from}, vytvořeno {entityType} {entity}",
"emailReceived": "Email přijat, související s {entityType} {entity}",
"emailReceivedInitial": "Email přijat: {entityType} {entity} vytvořeno",
"emailSent": "{by} poslal email související s {entityType} {entity}",
"emailSentThis": "{by} poslal email",
"postTargetSelf": "{user} sám přidal příspěvek",
"postTargetSelfAndOthers": "{user} přidal příspěvek na {target} a sebe",
"createAssignedYou": "Uživatel {user} vytvořil {entityType} {entity}, který vám byl přidělen",
"createAssignedThisSelf": "{user} vytvořil tento {entityType}, který si sám přidělil",
"createAssignedSelf": "{user} vytvořil {entityType} {entity} s vlastním přiřazením",
"assignYou": "Uživatel {user} vám přidělil {entityType} {entity}",
"assignThisVoid": "{user} zrušil přiřazení {entityType}",
"assignVoid": "{user} nepřiřazeno {entityType} {entity}",
"assignThisSelf": "{user} si sám přidělil tento {entityType}",
"assignSelf": "Uživatel {user} si přiřadil {entityType} {entity}",
"unrelate": "{user} odpojil {relatedEntityType} {relatedEntity} od {entityType} {entity}",
"unrelateThis": "{user} odpojil {relatedEntityType} {relatedEntity} od tohoto {entityType}",
"assignMultiAdd": "{user} přiřadil {entity} uživateli {assignee}",
"assignMultiRemove": "{user} zrušil přiřazení {entity} od {removedAssignee}",
"assignMultiAddRemove": "{user} přiřadil {entity} k {assignee} a zrušil přiřazení od {removedAssignee}",
"assignMultiRemoveThis": "{user} zrušil přiřazení tohoto {entityType} od {removedAssignee}",
"assignMultiAddRemoveThis": "{user} přiřadil tento {entityType} k {assignee} a zrušil přiřazení od {removedAssignee}"
},
"lists": {
"monthNames": [
"Leden",
"Únor",
"Březen",
"Duben",
"Květen",
"Červen",
"Červenec",
"Srpen",
"Září",
"Říjen",
"Listopad",
"Prosinec"
],
"monthNamesShort": [
"Led",
"Úno",
"Bře",
"Dub",
"Kvě",
"Črv",
"Črc",
"Srp",
"Zář",
"Říj",
"Lis",
"Pro"
],
"dayNames": [
"Neděle",
"Pondělí",
"Úterý",
"Středa",
"Čtvrtek",
"Pátek",
"Sobota"
],
"dayNamesShort": [
"Ned",
"Pon",
"Úte",
"Stř",
"Čtv",
"Pát",
"Sob"
],
"dayNamesMin": [
"Ne",
"Po",
"Út",
"St",
"Čt",
"Pá",
"So"
]
},
"options": {
"salutationName": {
"Mr.": "Pan",
"Mrs.": "Paní",
"Ms.": "Slečna",
"Dr.": "Doktor(ka)"
},
"dateSearchRanges": {
"on": "Dne",
"notOn": "Jiného dne než",
"after": "Po",
"before": "Před",
"between": "Mezi",
"today": "Dnes",
"past": "V minulosti",
"future": "V budoucnosti",
"currentMonth": "Tento měsíc",
"lastMonth": "Minulý měsíc",
"currentQuarter": "Toto čtvrtletí",
"lastQuarter": "Minulé čtvrtletí",
"currentYear": "Tento rok",
"lastYear": "Minulý rok",
"lastSevenDays": "Posledních 7 dní",
"lastXDays": "Posledních X dní",
"nextXDays": "Následujících X dní",
"ever": "Kdykoli",
"isEmpty": "Je prázdné",
"olderThanXDays": "Starší než X dní",
"afterXDays": "Po X dnech",
"nextMonth": "Následující měsíc",
"currentFiscalYear": "Tento fiskální rok",
"lastFiscalYear": "Poslední fiskální rok",
"currentFiscalQuarter": "Toto fiskální čtvrtletí",
"lastFiscalQuarter": "Poslední fiskální čtvrtletí"
},
"searchRanges": {
"is": "Je",
"isEmpty": "Je prázdné",
"isNotEmpty": "Není prázdné",
"isFromTeams": "Je z týmu",
"isOneOf": "Kterýkoli z vybraných",
"anyOf": "Kterýkoli z vybraných",
"isNot": "Není",
"isNotOneOf": "Není z",
"noneOf": "Není z",
"allOf": "Všechny z",
"any": "Kterýkoli"
},
"varcharSearchRanges": {
"equals": "Rovná se",
"like": "Je jako (%)",
"startsWith": "Začíná",
"endsWith": "Končí na",
"contains": "Obsahuje",
"isEmpty": "Je prázdný",
"isNotEmpty": "Není prázdný",
"notLike": "Není jako (%)",
"notContains": "Neobsahuje",
"notEquals": "Nerovná se",
"anyOf": "Kterýkoli z",
"noneOf": "Žádný z"
},
"intSearchRanges": {
"equals": "Rovná se",
"notEquals": "Nerovná se",
"greaterThan": "Větší než",
"lessThan": "Menší než",
"greaterThanOrEquals": "Větší než nebo se rovná",
"lessThanOrEquals": "Menší než nebo se rovná",
"between": "Mezi",
"isEmpty": "Je prázdné",
"isNotEmpty": "Není prázdné"
},
"autorefreshInterval": {
"0": "Není",
"1": "1 minuta",
"2": "2 minuty",
"5": "5 minuty",
"10": "10 minut",
"0.5": "30 sekund"
},
"phoneNumber": {
"Mobile": "Mobilní",
"Office": "Kancelář",
"Home": "Domácí",
"Other": "Další"
},
"saveConflictResolution": {
"current": "Současné",
"actual": "Skutečné",
"original": "Původní"
}
},
"sets": {
"summernote": {
"NOTICE": "Překlady můžete najít zde: https://github.com/HackerWins/summernote/tree/master/lang",
"font": {
"bold": "Tučné",
"italic": "Kurzíva",
"underline": "Podtržené",
"strike": "Přeškrtnuté",
"clear": "Odebrat styl písma",
"height": "Velikost řádku",
"name": "Rodina písma",
"size": "Velikost písma"
},
"image": {
"image": "Obrázek",
"insert": "Vložit obrázek",
"resizeFull": "Změna velikost 1/1",
"resizeHalf": "Změna velikosti 1/2",
"resizeQuarter": "Změna velikosti 1/4",
"floatLeft": "Plavat vlevo",
"floatRight": "Plavat vpravo",
"floatNone": "Neplavat",
"dragImageHere": "Přesunout obrázek sem.",
"selectFromFiles": "Vybrat ze souboru",
"url": "URL obrázku",
"remove": "Odebrat obrázek"
},
"link": {
"link": "Odkaz",
"insert": "Vložit link",
"unlink": "Odebrat link",
"edit": "Upravit",
"textToDisplay": "Text k zobrazení",
"url": "Na jaké URL má link směřovat?",
"openInNewWindow": "Otevřít v novém okně"
},
"video": {
"videoLink": "Video link",
"insert": "Vložit video",
"url": "URL",
"providers": "(YouTube, Vimeo, Vine, Instagram, nebo DailyMotion)"
},
"table": {
"table": "Tabulka"
},
"hr": {
"insert": "Vložit horizontální čáru"
},
"style": {
"style": "Styl",
"normal": "Normální",
"blockquote": "Citace",
"pre": "Kód",
"h1": "Nadpis 1",
"h2": "Nadpis 2",
"h3": "Nadpis 3",
"h4": "Nadpis 4",
"h5": "Nadpis 5",
"h6": "Nadpis 6"
},
"lists": {
"unordered": "Neřazený seznam",
"ordered": "Řazený seznam"
},
"options": {
"help": "Nápověda",
"fullscreen": "Celá obrazovka",
"codeview": "Zobrazit kód"
},
"paragraph": {
"paragraph": "Odstavec",
"outdent": "Předsadit",
"indent": "Odsadit",
"left": "Zarovnat vlevo",
"center": "Zarovnat na střed",
"right": "Zarovnat vpravo",
"justify": "Zarovnat do bloku"
},
"color": {
"recent": "Nedávná baarva",
"more": "Víc barev",
"transparent": "Průhlednost",
"setTransparent": "Nastavení průhlednosti",
"reset": "Resetovat",
"resetToDefault": "Resetovat na výchozí",
"background": "Pozadí",
"foreground": "Popředí"
},
"shortcut": {
"shortcuts": "Klávesové zkratky",
"close": "Zavřít",
"textFormatting": "Formát textu",
"action": "Akce",
"paragraphFormatting": "Formát odstavce",
"documentStyle": "Styl dokumentu"
},
"history": {
"undo": "Zpět",
"redo": "Znovu"
}
}
},
"streamMessagesMale": {
"postTargetSelfAndOthers": "{user} napsal {target} a sobě"
},
"streamMessagesFemale": {
"postTargetSelfAndOthers": "{user} napsala {target} a sobě"
},
"listViewModes": {
"list": "Seznam"
},
"themes": {
"Dark": "Tmavý",
"Espo": "Classic",
"Sakura": "Classic Sakura",
"Violet": "Classic Violet",
"Hazyblue": "Classic Hazy",
"Glass": "Sklo",
"Light": "Světlý"
},
"themeNavbars": {
"side": "Boční navigační lišta",
"top": "Horní navigační lišta"
},
"fieldValidations": {
"required": "Povinné",
"maxCount": "Maximální počet",
"maxLength": "Maximální délka",
"pattern": "Shoda vzoru",
"emailAddress": "Platná e-mailová adresa",
"phoneNumber": "Platné telefonní číslo",
"array": "Pole",
"arrayOfString": "Pole řetězců",
"noEmptyString": "Žádný prázdný řetězec",
"max": "Maximální hodnota",
"min": "Minimální hodnota",
"valid": "Platnost"
},
"fieldValidationExplanations": {
"url_valid": "Neplatná hodnota URL.",
"currency_valid": "Neplatná hodnota částky.",
"currency_validCurrency": "Hodnota kódu měny je neplatná nebo není povolena.",
"varchar_pattern": "Hodnota pravděpodobně obsahuje nepovolené znaky.",
"email_emailAddress": "Neplatná hodnota e-mailové adresy.",
"phone_phoneNumber": "Neplatná hodnota telefonního čísla.",
"datetimeOptional_valid": "Neplatná hodnota data a času.",
"datetime_valid": "Neplatná hodnota data a času.",
"date_valid": "Neplatná hodnota data.",
"enum_valid": "Neplatná hodnota výčtu. Hodnota musí být jedna z definovaných možností výčtu. Prázdná hodnota je povolena pouze tehdy, pokud má pole prázdnou možnost.",
"multiEnum_valid": "Neplatná hodnota vícenásobného výčtu. Hodnoty musí být jednou z definovaných možností pole.",
"int_valid": "Neplatná hodnota celého čísla.",
"float_valid": "Neplatná hodnota čísla.",
"valid": "Neplatná hodnota.",
"maxLength": "Délka hodnoty překračuje maximální hodnotu.",
"phone_valid": "Telefonní číslo není platné. Může být způsobeno chybným nebo prázdným kódem země."
},
"navbarTabs": {
"Business": "Obchod",
"Support": "Podpora",
"Activities": "Aktivity"
},
"wysiwygLabels": {
"cell": "Buňka",
"align": "Zarovnání",
"width": "Šířka",
"height": "Výška",
"borderWidth": "Šířka okraje",
"borderColor": "Barva okraje",
"cellPadding": "Odsazení buňky",
"backgroundColor": "Barva pozadí",
"verticalAlign": "Svislé zarovnání"
},
"wysiwygOptions": {
"align": {
"left": "Vlevo",
"center": "Na střed",
"right": "Vpravo"
},
"verticalAlign": {
"top": "Nahoru",
"middle": "Na střed",
"bottom": "Dolů"
}
},
"strings": {
"yesterdayShort": "Včera"
},
"reactions": {
"Smile": "Úsměv",
"Surprise": "Překvapení",
"Laugh": "Smích",
"Meh": "Nic moc",
"Sad": "Smutek",
"Love": "Láska",
"Like": "Líbí se",
"Dislike": "Nelíbí se"
},
"recordActions": {
"create": "Vytvořit",
"read": "Číst",
"edit": "Upravit",
"delete": "Smazat"
}
}

View File

@@ -0,0 +1,313 @@
{
"fields": {
"useCache": "Použít cache",
"dateFormat": "Formát data",
"timeFormat": "Formát času",
"timeZone": "Časové pásmo",
"weekStart": "První den v týdnu",
"thousandSeparator": "Oddělovač tisíců",
"decimalMark": "Desetinný oddělovač",
"defaultCurrency": "Výchozí měna",
"baseCurrency": "Bázová měna",
"currencyRates": "Kurzy měn",
"currencyList": "Seznam měn",
"language": "Jazyk",
"companyLogo": "Logo společnosti",
"ldapPort": "LDAP Port",
"ldapAuth": "LDAP Auth",
"ldapSecurity": "Zabezpečení",
"ldapPassword": "Heslo",
"outboundEmailFromName": "Od (jméno)",
"outboundEmailIsShared": "Sdílení",
"recordsPerPage": "Záznamy na stránku",
"recordsPerPageSmall": "Záznamy na stránku (malý)",
"tabList": "Seznam záložek",
"quickCreateList": "Rychlé odkazy",
"exportDelimiter": "Export oddělovač",
"globalSearchEntityList": "Seznam entit globálního vyhledávání",
"authenticationMethod": "Autentizační metoda",
"ldapHost": "LDAP Host",
"ldapAccountCanonicalForm": "LDAP Account Canonical Form",
"ldapAccountDomainName": "Název domény účtu",
"ldapTryUsernameSplit": "Zkuste rozdělit uživatelské jméno",
"ldapCreateEspoUser": "Vytvořit uživatele v EspoCRM",
"ldapUserLoginFilter": "Filtr uživatelského přihlášení",
"ldapAccountDomainNameShort": "Account Domain Name krátké",
"ldapOptReferrals": "Volit doporučení",
"exportDisabled": "Zakázat export (povolen pouze správce)",
"b2cMode": "Režm B2C",
"avatarsDisabled": "Zakázat avatary",
"displayListViewRecordCount": "Zobrazit celkový počet (v zobrazení seznamu)",
"theme": "Téma",
"userThemesDisabled": "Zakázat uživatelské motivy",
"emailMessageMaxSize": "Maximální velikost emailu (Mb)",
"personalEmailMaxPortionSize": "Maximální velikost emailové části pro načítání osobních účtů",
"inboundEmailMaxPortionSize": "Maximální velikost emailové části pro načítání skupinových účtů",
"authTokenLifetime": "Životnost ověřovacího tokenu (hodiny)",
"authTokenMaxIdleTime": "Maximální doba nečinnosti ověřovacího tokenu (hodiny)",
"dashboardLayout": "Rozvržení Dashboardu (výchozí)",
"siteUrl": "URL stránky",
"addressPreview": "Náhled adresy",
"addressFormat": "Formát adresy",
"notificationSoundsDisabled": "Zakázat zvuky oznámení",
"applicationName": "Název aplikace",
"ldapUsername": "Uživatelské jméno",
"ldapBindRequiresDn": "Přiřazení vyžaduje Dn",
"ldapBaseDn": "Bázové Dn",
"ldapUserNameAttribute": "Atribut uživatelského jména",
"ldapUserObjectClass": "Třída objektu uživatele",
"ldapUserTitleAttribute": "Atribut názvu uživatele",
"ldapUserFirstNameAttribute": "Atribut křestního jména uživatele",
"ldapUserLastNameAttribute": "Atribut příjmení uživatele",
"ldapUserEmailAddressAttribute": "Atribut emailové adresy uživatele",
"ldapUserTeams": "Týmy uživatele",
"ldapUserDefaultTeam": "Výchozí tým uživatele",
"ldapUserPhoneNumberAttribute": "Atribut telefonního čísla uživatele",
"assignmentNotificationsEntityList": "Entity k upozornění podle přiřazení",
"assignmentEmailNotifications": "Poslat emailová upozornění podle přiřazení",
"assignmentEmailNotificationsEntityList": "Entity k upozornění emailem podle přiřazení",
"streamEmailNotifications": "Oznámení o aktualizacích ve streamu pro interní uživatele",
"portalStreamEmailNotifications": "Oznámení o aktualizacích ve streamu pro uživatele portálu",
"streamEmailNotificationsEntityList": "Rozsahy emailových oznámení o streamu",
"calendarEntityList": "Seznam entit kalendáře",
"mentionEmailNotifications": "Zasílejte emailová oznámení o nových příspěvcích",
"massEmailDisableMandatoryOptOutLink": "Zakázat povinný odkaz pro odhlášení",
"activitiesEntityList": "Seznam entit aktivit",
"historyEntityList": "Seznam entit historie",
"currencyFormat": "Formát měny",
"currencyDecimalPlaces": "Počet desetinných míst měny",
"followCreatedEntities": "Sledovat vytvořené entity",
"aclAllowDeleteCreated": "Povolit odebrání vytvořených záznamů",
"adminNotifications": "Systémová oznámení v administračním panelu",
"adminNotificationsNewVersion": "Zobrazit oznámení, až bude k dispozici nová verze CRM",
"massEmailMaxPerHourCount": "Maximální počet e-mailů odeslaných za hodinu",
"maxEmailAccountCount": "Maximální počet osobních emailových účtů na uživatele",
"streamEmailNotificationsTypeList": "Na co upozorňovat",
"authTokenPreventConcurrent": "Pouze jeden ověřovací token na uživatele",
"scopeColorsDisabled": "Zakázat barvy rozsahu",
"tabColorsDisabled": "Zakázat barvy záložek",
"tabIconsDisabled": "Zakázat ikony na kartě",
"textFilterUseContainsForVarchar": "Při filtrování polí varchar používat operátor „obsahuje“",
"emailAddressIsOptedOutByDefault": "Označit nové emailové adresy jako odhlášené",
"outboundEmailBccAddress": "Adresa BCC pro externí klienty",
"adminNotificationsNewExtensionVersion": "Zobrazit oznámení, když jsou k dispozici nové verze rozšíření",
"cleanupDeletedRecords": "Vyčistit smazané záznamy",
"ldapPortalUserLdapAuth": "Pro uživatele portálu použijte ověřování LDAP",
"ldapPortalUserPortals": "Výchozí portály pro uživatele portálu",
"ldapPortalUserRoles": "Výchozí role pro uživatele portálu",
"fiscalYearShift": "Začátek fiskálního roku",
"jobRunInParallel": "Úlohy běží paralelně",
"jobMaxPortion": "Maximální velikost části úloh",
"jobPoolConcurrencyNumber": "Číslo souběhu úloh",
"daemonInterval": "Interval démona",
"daemonMaxProcessNumber": "Maximální počet procesů démona",
"daemonProcessTimeout": "Timeout procesu démona",
"addressCityList": "Seznam měst při našeptávání políčka adresa",
"addressStateList": "Seznam států pro našeptávání adres",
"cronDisabled": "Zakázat Cron",
"maintenanceMode": "Režim údržby",
"useWebSocket": "Použít WebSocket",
"emailNotificationsDelay": "Zpoždění e-mailových oznámení (v sekundách)",
"massEmailOpenTracking": "Sledování otevření emailů",
"passwordRecoveryDisabled": "Zakázat obnovení hesla",
"passwordRecoveryForAdminDisabled": "Zakázat obnovení hesla pro uživatele správce",
"passwordGenerateLength": "Délka vygenerovaných hesel",
"passwordStrengthLength": "Minimální délka hesla",
"passwordStrengthLetterCount": "Počet písmen požadovaných v hesle",
"passwordStrengthNumberCount": "Počet číslic požadovaných v hesle",
"passwordStrengthBothCases": "Zabraňte vystavení e-mailové adresy ve formuláři pro obnovení hesla",
"auth2FA": "Povolit dvoufaktorové ověřování",
"auth2FAMethodList": "Dostupné metody dvoufaktorové autorizace",
"personNameFormat": "Formát jména osoby",
"newNotificationCountInTitle": "Zobrazit nové číslo oznámení v názvu stránky",
"massEmailVerp": "Použít VERP",
"emailAddressLookupEntityTypeList": "Rozsahy vyhledávání emailových adres",
"busyRangesEntityList": "Seznam volných / zaneprázdněných entit",
"passwordRecoveryForInternalUsersDisabled": "Zakázat obnovení hesla pro uživatele",
"passwordRecoveryNoExposure": "Zabraňte vystavení emailové adresy ve formuláři pro obnovení hesla",
"auth2FAForced": "Přimět uživatele k nastavení dvoufaktorové autorizace",
"smsProvider": "Poskytovatel SMS",
"outboundSmsFromNumber": "SMS z čísla",
"recordsPerPageSelect": "Záznamy na stránku (Výběr)",
"attachmentUploadMaxSize": "Maximální velikost přílohy (Mb)",
"attachmentUploadChunkSize": "Velikost části nahrávání příloh (Mb)",
"workingTimeCalendar": "Pracovní kalendář",
"oidcClientId": "OIDC ID klienta",
"oidcClientSecret": "OIDC tajný klíč klienta",
"oidcAuthorizationRedirectUri": "OIDC URI přesměrování autorizace",
"oidcAuthorizationEndpoint": "OIDC koncový bod autorizace",
"oidcTokenEndpoint": "OIDC koncový bod tokenu",
"oidcJwksEndpoint": "OIDC koncový bod JSON Web Key Set",
"oidcJwtSignatureAlgorithmList": "OIDC povolené podpisové algoritmy JWT",
"oidcScopes": "OIDC rozsahy",
"oidcGroupClaim": "OIDC nárok skupiny",
"oidcCreateUser": "OIDC vytvořit uživatele",
"oidcUsernameClaim": "OIDC nárok uživatelského jména",
"oidcTeams": "OIDC týmy",
"oidcSync": "OIDC synchronizace",
"oidcSyncTeams": "OIDC synchronizace týmů",
"oidcFallback": "OIDC záložní přihlášení",
"oidcAllowRegularUserFallback": "OIDC povolit záložní přihlášení běžným uživatelům",
"oidcAllowAdminUser": "OIDC povolit přihlášení správcům",
"oidcLogoutUrl": "OIDC URL odhlášení",
"pdfEngine": "PDF generátor",
"recordsPerPageKanban": "Záznamy na stránku (Kanban)",
"auth2FAInPortal": "Povolit dvoufaktorové ověřování v portálech",
"massEmailMaxPerBatchCount": "Maximální počet e-mailů odeslaných za dávku",
"phoneNumberNumericSearch": "Číselné vyhledávání telefonních čísel",
"phoneNumberInternational": "Mezinárodní telefonní čísla",
"phoneNumberPreferredCountryList": "Upřednostňované země pro telefonního čísla",
"jobForceUtc": "Vynutit UTC pro úlohy",
"emailAddressSelectEntityTypeList": "Rozsahy výběru emailových adres",
"phoneNumberExtensions": "Přípony telefonních čísel",
"oidcAuthorizationPrompt": "OIDC výzva k autorizaci",
"quickSearchFullTextAppendWildcard": "Rychlé vyhledávání přidat wildcard symbol",
"authIpAddressCheck": "Omezovat přístup na základě IP adresy",
"authIpAddressWhitelist": "Whitelist IP adres",
"authIpAddressCheckExcludedUsers": "Uživatelé vyřazení z kontroly",
"streamEmailWithContentEntityTypeList": "Entity s obsahem emailu v poznámkách streamu",
"emailScheduledBatchCount": "Maximální počet naplánovaných e-mailů odeslaných za dávku",
"passwordStrengthSpecialCharacterCount": "Počet speciálních znaků požadovaných v hesle",
"availableReactions": "Dostupné reakce",
"outboundEmailFromAddress": "Odesílatelská emailová adresa",
"oidcUserInfoEndpoint": "OIDC koncový bod informací o uživateli",
"baselineRole": "Základní role"
},
"tooltips": {
"recordsPerPage": "Počet záznamů původně zobrazených v zobrazení seznamu.",
"recordsPerPageSmall": "Počet záznamů v panelu vztahů.",
"followCreatedEntities": "Pokud uživatel vytvoří záznam, bude jej sledovat automaticky.",
"ldapUsername": "Úplné uživatelské jméno systému, které umožňuje vyhledávat další uživatele. Např. \"CN = uživatel systému LDAP, OU = uživatelé, OU = espocrm, DC = test, DC = lan\".",
"ldapPassword": "Heslo pro přístup k serveru LDAP.",
"ldapAuth": "Přístup k pověření serveru LDAP.",
"ldapUserNameAttribute": "Atribut k identifikaci uživatele. \nNapř. „userPrincipalName“ nebo „sAMAccountName“ pro Active Directory, „uid“ pro OpenLDAP.",
"ldapUserObjectClass": "Atribut ObjectClass pro vyhledávání uživatelů. Např. „osoba“ pro AD, „inetOrgPerson“ pro OpenLDAP.",
"ldapBindRequiresDn": "Možnost formátovat uživatelské jméno ve formuláři DN.",
"ldapBaseDn": "Výchozí základní DN používané pro vyhledávání uživatelů. Např. \"OU = uživatelé, OU = espocrm, DC = test, DC = lan\".",
"ldapTryUsernameSplit": "Možnost rozdělit uživatelské jméno na doménu.",
"ldapOptReferrals": "pokud by měla být sledována doporučení klientovi LDAP.",
"ldapCreateEspoUser": "Tato možnost umožňuje AutoCRM vytvořit uživatele z LDAP.",
"ldapUserFirstNameAttribute": "Atribut LDAP, který se používá k určení křestního jména uživatele. Např. \"křestní jméno\".",
"ldapUserLastNameAttribute": "Atribut LDAP, který se používá k určení příjmení uživatele. Např. \"sn\".",
"ldapUserTitleAttribute": "LDAP atribut pro titul uživatele.",
"ldapUserEmailAddressAttribute": "Atribut LDAP, který se používá k určení e-mailové adresy uživatele. Např. \"pošta\".",
"ldapUserPhoneNumberAttribute": "LDAP atribut pro telefonní číslo uživatele.",
"ldapUserLoginFilter": "Filtr, který umožňuje omezit uživatele, kteří mohou používat AutoCRM. Např. \"memberOf = CN = espoGroup, OU = groups, OU = espocrm, DC = test, DC = lan\".",
"ldapAccountDomainName": "Doména, která se používá k autorizaci k serveru LDAP.",
"ldapAccountDomainNameShort": "Krátká doména, která se používá k autorizaci k serveru LDAP.",
"ldapUserTeams": "LDAP týmy pro uživatele.",
"ldapUserDefaultTeam": "Výchozí tým pro vytvořeného uživatele. Další informace najdete v uživatelském profilu.",
"b2cMode": "Ve výchozím nastavení je AutoCRM přizpůsoben pro B2B. Můžete jej přepnout na B2C.",
"aclStrictMode": "Povoleno: Přístup k rozsahům bude zakázán, pokud není uveden v rolích. \nZakázán: Přístup k rozsahům bude povolen, pokud není uveden v rolích.",
"outboundEmailIsShared": "Povolit posílání emailů uživatelům pomocí SMTP.",
"streamEmailNotificationsEntityList": "Emailová upozornění na aktualizace streamu sledovaných záznamů. Uživatelé budou dostávat e-mailová oznámení pouze pro určené typy entit.",
"authTokenPreventConcurrent": "Uživatelé nebudou moci být přihlášeni na více zařízeních současně.",
"ldapPortalUserLdapAuth": "Umožněte uživatelům portálu používat autentizaci LDAP namísto autentizace Auto.",
"ldapPortalUserPortals": "Výchozí portály pro vytvořeného uživatele portálu",
"ldapPortalUserRoles": "Výchozí role pro vytvořeného uživatele portálu",
"jobPoolConcurrencyNumber": "Maximální počet procesů spuštěných současně.",
"cronDisabled": "Cron se nespustí.",
"maintenanceMode": "Do systému budou mít přístup pouze správci.",
"ldapAccountCanonicalForm": "Typ kanonického formuláře vašeho účtu. K dispozici jsou 4 možnosti: \n- „Dn“ - formulář ve formátu „CN = tester, OU = espocrm, DC = test, DC = lan“. - „Uživatelské jméno“ - formulář „tester“ .- „Zpětné lomítko“ - formulář „SPOLEČNOST \\ tester“. - „Principal“ - formulář „tester@company.com“.",
"massEmailVerp": "Variabilní zpětná cesta obálky. Pro lepší zpracování odražených zpráv. Ujistěte se, že to váš poskytovatel SMTP podporuje.",
"addressStateList": "Návrhy států pro adresní pole.",
"addressCityList": "Návrhy měst pro adresní pole.",
"addressCountryList": "Návrhy zemí pro adresní pole.",
"exportDisabled": "Zakázat export pro běžné uživatele.",
"siteUrl": "URL vašeho CRM systému.",
"useCache": "Nedoporučuje se deaktivovat, pokud se nejedná o účely vývoje.",
"useWebSocket": "WebSocket umožňuje obousměrnou interaktivní komunikaci mezi serverem a prohlížečem. Vyžaduje nastavení démonu WebSocket na vašem serveru. Pro více informací se podívejte do dokumentace.",
"emailNotificationsDelay": "Zprávu lze upravit ve stanoveném časovém rámci před odesláním oznámení.",
"recordsPerPageSelect": "Počet záznamů na stránku ve výběru.",
"workingTimeCalendar": "Pracovní kalendář pro zobrazení pracovní doby.",
"oidcFallback": "Povolit záložní přihlášení.",
"oidcCreateUser": "Automaticky vytvářet nové uživatele z OIDC.",
"oidcSync": "Synchronizovat uživatelské údaje z OIDC.",
"oidcSyncTeams": "Synchronizovat týmy z OIDC.",
"oidcUsernameClaim": "OIDC nárok pro uživatelské jméno.",
"oidcTeams": "OIDC týmy pro uživatele.",
"recordsPerPageKanban": "Počet záznamů na stránku v Kanban zobrazení.",
"jobForceUtc": "Použije časové pásmo UTC pro plánované úlohy. Jinak bude použito časové pásmo nastavené v nastavení.",
"authIpAddressCheckExcludedUsers": "Uživatelé, kteří se budou moci přihlásit z jakéhokoli místa.",
"authIpAddressWhitelist": "Seznam IP adres nebo rozsahů v notaci CIDR.\n\nPortály nejsou omezeny.",
"oidcGroupClaim": "OIDC nárok pro skupinové informace.",
"outboundEmailFromAddress": "Systémová emailová adresa.",
"baselineRole": "Základní role definuje minimální úroveň přístupových práv pro všechny uživatele. Tato role je automaticky aplikována na všechny uživatele bez ohledu na jejich ostatní role.",
"displayListViewRecordCount": "Zobrazit celkový počet záznamů v zobrazení seznamu.",
"currencyList": "Dostupné měny v systému.",
"activitiesEntityList": "Entity, které se považují za aktivity.",
"historyEntityList": "Entity, které se považují za historii.",
"calendarEntityList": "Entity zobrazené v kalendáři.",
"globalSearchEntityList": "Entity dostupné v globálním vyhledávání.",
"passwordRecoveryForInternalUsersDisabled": "Obnovit heslo budou moci pouze uživatelé portálu.",
"passwordRecoveryNoExposure": "Nebude možné určit, zda je v systému zaregistrována konkrétní e-mailová adresa.",
"emailAddressLookupEntityTypeList": "Pro automatické vyplňování emailových adres.",
"emailAddressSelectEntityTypeList": "Rozsahy pro výběr emailových adres.",
"busyRangesEntityList": "Co se bude brát v úvahu při zobrazování časových období zaneprázdnění v plánovači a časové ose.",
"emailMessageMaxSize": "Všechny příchozí emaily přesahující stanovenou velikost budou načteny bez těla a příloh.",
"authTokenLifetime": "Definuje, jak dlouho mohou existovat tokeny. \n0 - znamená žádné vypršení platnosti.",
"authTokenMaxIdleTime": "Definuje, jak dlouho mohou existovat poslední přístupové tokeny. \n0 - znamená žádné vypršení platnosti.",
"userThemesDisabled": "Pokud je zaškrtnuto, uživatelé nebudou moci vybrat jiné téma.",
"currencyDecimalPlaces": "Počet desetinných míst. Pokud jsou prázdné, zobrazí se všechna neprázdná desetinná místa.",
"aclAllowDeleteCreated": "Uživatelé budou moci odebrat záznamy, které vytvořili, i když nemají přístup k odstranění.",
"textFilterUseContainsForVarchar": "Pokud není zaškrtnuto, použije se operátor „začíná na“. Můžete použít zástupný znak '%'.",
"emailAddressIsOptedOutByDefault": "Při vytváření nového záznamu bude emailová adresa označena jako odhlášena.",
"cleanupDeletedRecords": "Odebrané záznamy budou po chvíli z databáze odstraněny.",
"jobRunInParallel": "Úlohy budou prováděny paralelně.",
"jobMaxPortion": "Maximální počet zpracovaných úloh na jedno provedení.",
"daemonInterval": "Interval spouštění démona v sekundách.",
"daemonMaxProcessNumber": "Maximální počet procesů cron běžících současně.",
"daemonProcessTimeout": "Maximální doba provedení (v sekundách) přidělená jednomu procesu cron.",
"oidcLogoutUrl": "URL pro odhlášení z OIDC poskytovatele.",
"quickSearchFullTextAppendWildcard": "Připojte zástupný znak k dotazu automatického dokončování, pokud je povoleno fulltextové vyhledávání. Snižuje to výkon vyhledávání."
},
"labels": {
"System": "Systém",
"Locale": "Lokalizace",
"Configuration": "Konfigurace",
"In-app Notifications": "In-app notifikace",
"Email Notifications": "Email notifikace",
"Currency Settings": "Nastavení měn",
"Currency Rates": "Kurzy měn",
"Mass Email": "Hromadný email",
"Test Connection": "Test připojení",
"Connecting": "Připojování...",
"Activities": "Aktivity",
"Admin Notifications": "Oznámení správce",
"Search": "Vyhledat",
"Misc": "Vedlejší",
"Passwords": "Hesla",
"2-Factor Authentication": "Dvoufaktorové ověřování",
"Group Tab": "Skupina záložek",
"Attachments": "Přílohy",
"IdP Group": "IdP skupina",
"Divider": "Oddělovač",
"General": "Obecné",
"Navbar": "Navigační panel",
"Phone Numbers": "Telefonní čísla",
"Access": "Přístup",
"Strength": "Síla",
"Recovery": "Obnovení",
"Scheduled Send": "Naplánované odeslání"
},
"messages": {
"ldapTestConnection": "Připojení bylo úspěšně navázáno.",
"confirmBaselineRoleChange": "Opravdu chcete změnit základní roli? Tato změna ovlivní přístupová práva všech uživatelů."
},
"options": {
"streamEmailNotificationsTypeList": {
"Post": "Příspěvky",
"Status": "Aktualizace stavu",
"EmailReceived": "Přijaté emaily"
},
"personNameFormat": {
"firstLast": "Jméno Příjmení",
"lastFirst": "Příjmení Jméno",
"firstMiddleLast": "Jméno Prostřední jméno Příjmení",
"lastFirstMiddle": "Příjmení Jméno Prostřední jméno"
},
"auth2FAMethodList": {
"Email": "E-mail"
}
}
}

View File

@@ -0,0 +1,209 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2026 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Tools\Attachment;
use Espo\Core\Exceptions\Error;
use Espo\Core\Exceptions\ErrorSilent;
use Espo\Core\Exceptions\Forbidden;
use Espo\Core\Exceptions\ForbiddenSilent;
use Espo\Core\Utils\File\MimeType;
use Espo\Core\Utils\Metadata;
use Espo\Core\Utils\Security\UrlCheck;
use Espo\Entities\Attachment as Attachment;
use Espo\ORM\EntityManager;
use Espo\Repositories\Attachment as AttachmentRepository;
class UploadUrlService
{
private AccessChecker $accessChecker;
private Metadata $metadata;
private EntityManager $entityManager;
private MimeType $mimeType;
private DetailsObtainer $detailsObtainer;
public function __construct(
AccessChecker $accessChecker,
Metadata $metadata,
EntityManager $entityManager,
MimeType $mimeType,
DetailsObtainer $detailsObtainer,
private UrlCheck $urlCheck
) {
$this->accessChecker = $accessChecker;
$this->metadata = $metadata;
$this->entityManager = $entityManager;
$this->mimeType = $mimeType;
$this->detailsObtainer = $detailsObtainer;
}
/**
* Upload an image from and URL and store as attachment.
*
* @param non-empty-string $url
* @throws Forbidden
* @throws Error
*/
public function uploadImage(string $url, FieldData $data): Attachment
{
if (!$this->urlCheck->isNotInternalUrl($url)) {
throw new ForbiddenSilent("Not allowed URL.");
}
$attachment = $this->getAttachmentRepository()->getNew();
$this->accessChecker->check($data);
[$type, $contents] = $this->getImageDataByUrl($url) ?? [null, null];
if (!$type || !$contents) {
throw new ErrorSilent("Bad image data.");
}
$attachment->set([
'name' => $url,
'type' => $type,
'contents' => $contents,
'role' => Attachment::ROLE_ATTACHMENT,
]);
$attachment->set('parentType', $data->getParentType());
$attachment->set('relatedType', $data->getRelatedType());
$attachment->set('field', $data->getField());
$size = mb_strlen($contents, '8bit');
$maxSize = $this->detailsObtainer->getUploadMaxSize($attachment);
if ($maxSize && $size > $maxSize) {
throw new Error("File size should not exceed {$maxSize}Mb.");
}
$this->getAttachmentRepository()->save($attachment);
$attachment->clear('contents');
return $attachment;
}
/**
* @param non-empty-string $url
* @return ?array{string, string} A type and contents.
*/
private function getImageDataByUrl(string $url): ?array
{
$type = null;
if (!function_exists('curl_init')) {
return null;
}
$opts = [];
$httpHeaders = [];
$httpHeaders[] = 'Expect:';
$opts[\CURLOPT_URL] = $url;
$opts[\CURLOPT_HTTPHEADER] = $httpHeaders;
$opts[\CURLOPT_CONNECTTIMEOUT] = 10;
$opts[\CURLOPT_TIMEOUT] = 10;
$opts[\CURLOPT_HEADER] = true;
$opts[\CURLOPT_VERBOSE] = true;
$opts[\CURLOPT_SSL_VERIFYPEER] = true;
$opts[\CURLOPT_SSL_VERIFYHOST] = 2;
$opts[\CURLOPT_RETURNTRANSFER] = true;
// Prevents Server Side Request Forgery by redirecting to an internal host.
$opts[\CURLOPT_FOLLOWLOCATION] = false;
$opts[\CURLOPT_MAXREDIRS] = 2;
$opts[\CURLOPT_IPRESOLVE] = \CURL_IPRESOLVE_V4;
$opts[\CURLOPT_PROTOCOLS] = \CURLPROTO_HTTPS | \CURLPROTO_HTTP;
$opts[\CURLOPT_REDIR_PROTOCOLS] = \CURLPROTO_HTTPS;
$ch = curl_init();
curl_setopt_array($ch, $opts);
/** @var string|false $response */
$response = curl_exec($ch);
if ($response === false) {
curl_close($ch);
return null;
}
$headerSize = curl_getinfo($ch, \CURLINFO_HEADER_SIZE);
$header = substr($response, 0, $headerSize);
$body = substr($response, $headerSize);
$headLineList = explode("\n", $header);
foreach ($headLineList as $i => $line) {
if ($i === 0) {
continue;
}
if (strpos(strtolower($line), strtolower('Content-Type:')) === 0) {
$part = trim(substr($line, 13));
if ($part) {
$type = trim(explode(";", $part)[0]);
}
}
}
if (!$type) {
/** @var string $extension */
$extension = preg_replace('#\?.*#', '', pathinfo($url, \PATHINFO_EXTENSION));
$type = $this->mimeType->getMimeTypeByExtension($extension);
}
curl_close($ch);
if (!$type) {
return null;
}
/** @var string[] $imageTypeList */
$imageTypeList = $this->metadata->get(['app', 'image', 'allowedFileTypeList']) ?? [];
if (!in_array($type, $imageTypeList)) {
return null;
}
return [$type, $body];
}
private function getAttachmentRepository(): AttachmentRepository
{
/** @var AttachmentRepository */
return $this->entityManager->getRepositoryByClass(Attachment::class);
}
}

View File

@@ -0,0 +1,82 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2026 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Tools\Email\Api;
use Espo\Core\Acl;
use Espo\Core\Api\Action;
use Espo\Core\Api\Request;
use Espo\Core\Api\Response;
use Espo\Core\Api\ResponseComposer;
use Espo\Core\Exceptions\BadRequest;
use Espo\Core\Exceptions\Forbidden;
use Espo\Entities\Email;
use Espo\Entities\User;
use Espo\Tools\Email\ImportEmlService;
/**
* @noinspection PhpUnused
*/
class PostImportEml implements Action
{
public function __construct(
private Acl $acl,
private User $user,
private ImportEmlService $service,
) {}
public function process(Request $request): Response
{
$this->checkAccess();
$fileId = $request->getParsedBody()->fileId ?? null;
if (!is_string($fileId)) {
throw new BadRequest("No 'fileId'.");
}
$email = $this->service->import($fileId, $this->user->getId());
return ResponseComposer::json(['id' => $email->getId()]);
}
/**
* @throws Forbidden
*/
private function checkAccess(): void
{
if (!$this->acl->checkScope(Email::ENTITY_TYPE, Acl\Table::ACTION_CREATE)) {
throw new Forbidden("No 'create' access.");
}
if (!$this->acl->checkScope('Import')) {
throw new Forbidden("No access to 'Import'.");
}
}
}

View File

@@ -0,0 +1,129 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2026 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Tools\Email\Api;
use Espo\Core\Acl;
use Espo\Core\Api\Action;
use Espo\Core\Api\Request;
use Espo\Core\Api\Response;
use Espo\Core\Api\ResponseComposer;
use Espo\Core\Exceptions\BadRequest;
use Espo\Core\Exceptions\Error;
use Espo\Core\Exceptions\Forbidden;
use Espo\Core\Exceptions\NotFound;
use Espo\Core\Mail\Account\Util\AddressUtil;
use Espo\Core\Mail\Exceptions\NoSmtp;
use Espo\Core\Mail\SmtpParams;
use Espo\Core\Utils\Security\HostCheck;
use Espo\Entities\Email;
use Espo\Tools\Email\SendService;
use Espo\Tools\Email\TestSendData;
/**
* Sends test emails.
*/
class PostSendTest implements Action
{
public function __construct(
private SendService $sendService,
private Acl $acl,
private HostCheck $hostCheck,
private AddressUtil $addressUtil,
) {}
/**
* @throws BadRequest
* @throws Forbidden
* @throws Error
* @throws NoSmtp
* @throws NotFound
*/
public function process(Request $request): Response
{
if (!$this->acl->checkScope(Email::ENTITY_TYPE)) {
throw new Forbidden();
}
$data = $request->getParsedBody();
$type = $data->type ?? null;
$id = $data->id ?? null;
$server = $data->server ?? null;
$port = $data->port ?? null;
$username = $data->username ?? null;
$password = $data->password ?? null;
$auth = $data->auth ?? null;
$authMechanism = $data->authMechanism ?? null;
$security = $data->security ?? null;
$userId = $data->userId ?? null;
$fromAddress = $data->fromAddress ?? null;
$fromName = $data->fromName ?? null;
$emailAddress = $data->emailAddress ?? null;
if (!is_string($server)) {
throw new BadRequest("No `server`");
}
if (!is_int($port)) {
throw new BadRequest("No or bad `port`.");
}
if (!is_string($emailAddress)) {
throw new BadRequest("No `emailAddress`.");
}
$smtpParams = SmtpParams
::create($server, $port)
->withSecurity($security)
->withFromName($fromName)
->withFromAddress($fromAddress)
->withAuth($auth);
if ($auth) {
$smtpParams = $smtpParams
->withUsername($username)
->withPassword($password)
->withAuthMechanism($authMechanism);
}
if (
!$this->addressUtil->isAllowedAddress($smtpParams) &&
!$this->hostCheck->isNotInternalHost($server)
) {
throw new Forbidden("Not allowed internal host.");
}
$data = new TestSendData($emailAddress, $type, $id, $userId);
$this->sendService->sendTestEmail($smtpParams, $data);
return ResponseComposer::json(true);
}
}

View File

@@ -0,0 +1,138 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2026 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Tools\Email;
use Espo\Core\Exceptions\Conflict;
use Espo\Core\Exceptions\Error;
use Espo\Core\Exceptions\NotFound;
use Espo\Core\FileStorage\Manager;
use Espo\Core\Mail\Exceptions\ImapError;
use Espo\Core\Mail\Importer;
use Espo\Core\Mail\Importer\Data;
use Espo\Core\Mail\MessageWrapper;
use Espo\Core\Mail\Parsers\MailMimeParser;
use Espo\Entities\Attachment;
use Espo\Entities\Email;
use Espo\ORM\EntityManager;
use RuntimeException;
class ImportEmlService
{
public function __construct(
private Importer $importer,
private Importer\DuplicateFinder $duplicateFinder,
private EntityManager $entityManager,
private Manager $fileStorageManager,
private MailMimeParser $parser,
) {}
/**
* Import an EML.
*
* @param string $fileId An attachment ID.
* @param ?string $userId A user ID to relate an email with.
* @return Email An Email.
* @throws NotFound
* @throws Error
* @throws Conflict
*/
public function import(string $fileId, ?string $userId = null): Email
{
$attachment = $this->getAttachment($fileId);
$contents = $this->fileStorageManager->getContents($attachment);
try {
$message = new MessageWrapper(1, null, $this->parser, $contents);
} catch (ImapError $e) {
throw new RuntimeException(previous: $e);
}
$this->checkDuplicate($message);
$email = $this->importer->import($message, Data::create());
if (!$email) {
throw new Error("Could not import.");
}
if ($userId) {
$this->entityManager->getRDBRepositoryByClass(Email::class)
->getRelation($email, 'users')
->relateById($userId);
}
$this->entityManager->removeEntity($attachment);
return $email;
}
/**
* @throws NotFound
*/
private function getAttachment(string $fileId): Attachment
{
$attachment = $this->entityManager->getRDBRepositoryByClass(Attachment::class)->getById($fileId);
if (!$attachment) {
throw new NotFound("Attachment not found.");
}
return $attachment;
}
/**
* @throws Conflict
*/
private function checkDuplicate(MessageWrapper $message): void
{
$messageId = $this->parser->getMessageId($message);
if (!$messageId) {
return;
}
$email = $this->entityManager->getRDBRepositoryByClass(Email::class)->getNew();
$email->setMessageId($messageId);
$duplicate = $this->duplicateFinder->find($email, $message);
if (!$duplicate) {
return;
}
throw Conflict::createWithBody(
'Email is already imported.',
Error\Body::create()->withMessageTranslation('alreadyImported', Email::ENTITY_TYPE, [
'id' => $duplicate->getId(),
'link' => '#Email/view/' . $duplicate->getId(),
])
);
}
}

View File

@@ -0,0 +1,896 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2026 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Tools\EmailNotification;
use Espo\Core\Field\LinkParent;
use Espo\Core\Name\Field;
use Espo\Core\Name\Link;
use Espo\Core\Notification\EmailNotificationHandler;
use Espo\Core\Mail\SenderParams;
use Espo\Core\Utils\Config\ApplicationConfig;
use Espo\Core\Utils\DateTime as DateTimeUtil;
use Espo\Entities\Note;
use Espo\ORM\Collection;
use Espo\Repositories\Portal as PortalRepository;
use Espo\Entities\Email;
use Espo\Entities\Notification;
use Espo\Entities\Portal;
use Espo\Entities\Preferences;
use Espo\Entities\User;
use Espo\ORM\EntityManager;
use Espo\ORM\Query\SelectBuilder as SelectBuilder;
use Espo\Core\Htmlizer\Htmlizer;
use Espo\Core\Htmlizer\HtmlizerFactory as HtmlizerFactory;
use Espo\Core\InjectableFactory;
use Espo\Core\Mail\EmailSender as EmailSender;
use Espo\Core\Utils\Config;
use Espo\Core\Utils\Language;
use Espo\Core\Utils\Log;
use Espo\Core\Utils\Metadata;
use Espo\Core\Utils\TemplateFileManager;
use Espo\Core\Utils\Util;
use Espo\Tools\Stream\NoteAccessControl;
use Michelf\Markdown;
use Exception;
use DateTime;
use Throwable;
class Processor
{
private const HOURS_THRESHOLD = 5;
private const PROCESS_MAX_COUNT = 200;
private const TYPE_STATUS = 'Status';
private ?Htmlizer $htmlizer = null;
/** @var array<string,?EmailNotificationHandler> */
private $emailNotificationEntityHandlerHash = [];
/** @var array<string,?Portal> */
private $userIdPortalCacheMap = [];
public function __construct(
private EntityManager $entityManager,
private HtmlizerFactory $htmlizerFactory,
private EmailSender $emailSender,
private Config $config,
private InjectableFactory $injectableFactory,
private TemplateFileManager $templateFileManager,
private Metadata $metadata,
private Language $language,
private Log $log,
private NoteAccessControl $noteAccessControl,
private ApplicationConfig $applicationConfig,
) {}
public function process(): void
{
$mentionEmailNotifications = $this->config->get('mentionEmailNotifications');
$streamEmailNotifications = $this->config->get('streamEmailNotifications');
$portalStreamEmailNotifications = $this->config->get('portalStreamEmailNotifications');
$typeList = [];
if ($mentionEmailNotifications) {
$typeList[] = Notification::TYPE_MENTION_IN_POST;
}
if ($streamEmailNotifications || $portalStreamEmailNotifications) {
$typeList[] = Notification::TYPE_NOTE;
}
if (empty($typeList)) {
return;
}
$fromDt = new DateTime();
$fromDt->modify('-' . self::HOURS_THRESHOLD . ' hours');
$where = [
'createdAt>' => $fromDt->format(DateTimeUtil::SYSTEM_DATE_TIME_FORMAT),
'read' => false,
'emailIsProcessed' => false,
];
$delay = $this->config->get('emailNotificationsDelay');
if ($delay) {
$delayDt = new DateTime();
$delayDt->modify('-' . $delay . ' seconds');
$where[] = ['createdAt<' => $delayDt->format(DateTimeUtil::SYSTEM_DATE_TIME_FORMAT)];
}
$queryList = [];
foreach ($typeList as $type) {
$itemBuilder = null;
if ($type === Notification::TYPE_MENTION_IN_POST) {
$itemBuilder = $this->getNotificationQueryBuilderMentionInPost();
}
if ($type === Notification::TYPE_NOTE) {
$itemBuilder = $this->getNotificationQueryBuilderNote();
}
if (!$itemBuilder) {
continue;
}
$itemBuilder->where($where);
$queryList[] = $itemBuilder->build();
}
$builder = $this->entityManager
->getQueryBuilder()
->union()
->order('number')
->limit(0, self::PROCESS_MAX_COUNT);
foreach ($queryList as $query) {
$builder->query($query);
}
$unionQuery = $builder->build();
$sql = $this->entityManager
->getQueryComposer()
->compose($unionQuery);
/** @var Collection<Notification> $notifications */
$notifications = $this->entityManager
->getRDBRepository(Notification::ENTITY_TYPE)
->findBySql($sql);
foreach ($notifications as $notification) {
$notification->set('emailIsProcessed', true);
$type = $notification->getType();
try {
if ($type === Notification::TYPE_NOTE) {
$this->processNotificationNote($notification);
} else if ($type === Notification::TYPE_MENTION_IN_POST) {
$this->processNotificationMentionInPost($notification);
} else {
// For bc.
$methodName = 'processNotification' . ucfirst($type ?? 'Dummy');
if (method_exists($this, $methodName)) {
$this->$methodName($notification);
}
}
} catch (Throwable $e) {
$this->log->error("Email notification: {$e->getMessage()}", ['exception' => $e]);
}
$this->entityManager->saveEntity($notification);
}
}
protected function getNotificationQueryBuilderMentionInPost(): SelectBuilder
{
return $this->entityManager
->getQueryBuilder()
->select()
->from(Notification::ENTITY_TYPE)
->where([
'type' => Notification::TYPE_MENTION_IN_POST,
]);
}
protected function getNotificationQueryBuilderNote(): SelectBuilder
{
$builder = $this->entityManager
->getQueryBuilder()
->select()
->from(Notification::ENTITY_TYPE)
->join(Note::ENTITY_TYPE, 'note', ['note.id:' => 'relatedId'])
->join('user')
->where([
'type' => Notification::TYPE_NOTE,
'relatedType' => Note::ENTITY_TYPE,
'note.type' => $this->getNoteNotificationTypeList(),
]);
$entityList = $this->config->get('streamEmailNotificationsEntityList');
if (empty($entityList)) {
$builder->where([
'relatedParentType' => null,
]);
} else {
$builder->where([
'OR' => [
[
'relatedParentType' => $entityList,
],
[
'relatedParentType' => null,
],
],
]);
}
$forInternal = $this->config->get('streamEmailNotifications');
$forPortal = $this->config->get('portalStreamEmailNotifications');
if ($forInternal && !$forPortal) {
$builder->where([
'user.type!=' => User::TYPE_PORTAL,
]);
} else if (!$forInternal && $forPortal) {
$builder->where([
'user.type' => User::TYPE_PORTAL,
]);
}
return $builder;
}
protected function processNotificationMentionInPost(Notification $notification): void
{
if (!$notification->get('userId')) {
return;
}
$userId = $notification->get('userId');
/** @var ?User $user */
$user = $this->entityManager->getEntityById(User::ENTITY_TYPE, $userId);
if (!$user) {
return;
}
$emailAddress = $user->get('emailAddress');
if (!$emailAddress) {
return;
}
$preferences = $this->entityManager->getEntityById(Preferences::ENTITY_TYPE, $userId);
if (!$preferences) {
return;
}
if (!$preferences->get('receiveMentionEmailNotifications')) {
return;
}
if (!$notification->getRelated() || $notification->getRelated()->getEntityType() !== Note::ENTITY_TYPE) {
return;
}
/** @var ?Note $note */
$note = $this->entityManager->getEntityById(Note::ENTITY_TYPE, $notification->getRelated()->getId());
if (!$note) {
return;
}
$parent = null;
$parentId = $note->getParentId();
$parentType = $note->getParentType();
$data = [];
if ($parentId && $parentType) {
$parent = $this->entityManager->getEntityById($parentType, $parentId);
if (!$parent) {
return;
}
$data['url'] = "{$this->getSiteUrl($user)}/#$parentType/view/$parentId";
$data['parentName'] = $parent->get(Field::NAME);
$data['parentType'] = $parentType;
$data['parentId'] = $parentId;
} else {
$data['url'] = $this->getSiteUrl($user) . '/#Notification';
}
$data['userName'] = $note->get('createdByName');
$post = Markdown::defaultTransform(
$note->get('post') ?? ''
);
$data['post'] = $post;
$subjectTpl = $this->templateFileManager->getTemplate('mention', 'subject');
$bodyTpl = $this->templateFileManager->getTemplate('mention', 'body');
$subjectTpl = str_replace(["\n", "\r"], '', $subjectTpl);
$subject = $this->getHtmlizer()->render($note, $subjectTpl, 'mention-email-subject', $data, true);
$body = $this->getHtmlizer()->render($note, $bodyTpl, 'mention-email-body', $data, true);
$email = $this->entityManager->getRDBRepositoryByClass(Email::class)->getNew();
$email
->setSubject($subject)
->setBody($body)
->setIsHtml()
->addToAddress($emailAddress);
$email->set('isSystem', true);
if ($parentId && $parentType) {
$email->setParent(LinkParent::create($parentType, $parentId));
}
$senderParams = SenderParams::create();
if ($parent && $parentType) {
$handler = $this->getHandler('mention', $parentType);
if ($handler) {
$handler->prepareEmail($email, $parent, $user);
$senderParams = $handler->getSenderParams($parent, $user) ?? $senderParams;
}
}
$sender = $this->emailSender
->withParams($senderParams);
if ($note->getType() !== Note::TYPE_POST) {
$sender = $sender->withAddedHeader('Auto-Submitted', 'auto-generated');
}
try {
$sender->send($email);
} catch (Exception $e) {
$this->log->error("Email notification: {$e->getMessage()}", ['exception' => $e]);
}
}
protected function processNotificationNote(Notification $notification): void
{
if (
!$notification->getRelated() ||
$notification->getRelated()->getEntityType() !== Note::ENTITY_TYPE
) {
return;
}
$noteId = $notification->getRelated()->getId();
$note = $this->entityManager->getRDBRepositoryByClass(Note::class)->getById($noteId);
if (
!$note ||
!in_array($note->getType(), $this->getNoteNotificationTypeList()) ||
!$notification->getUserId()
) {
return;
}
$userId = $notification->getUserId();
$user = $this->entityManager->getRDBRepositoryByClass(User::class)->getById($userId);
if (!$user) {
return;
}
if (!$user->getEmailAddress()) {
return;
}
$preferences = $this->entityManager->getEntityById(Preferences::ENTITY_TYPE, $userId);
if (!$preferences) {
return;
}
if (!$preferences->get('receiveStreamEmailNotifications')) {
return;
}
$type = $note->getType();
if ($type === Note::TYPE_POST) {
$this->processNotificationNotePost($note, $user);
return;
}
if ($type === Note::TYPE_UPDATE && isset($note->getData()->value)) {
$this->processNotificationNoteStatus($note, $user);
return;
}
if ($type === Note::TYPE_EMAIL_RECEIVED) {
$this->processNotificationNoteEmailReceived($note, $user);
return;
}
/** For bc. */
$methodName = 'processNotificationNote' . $type;
if (method_exists($this, $methodName)) {
$this->$methodName($note, $user);
}
}
protected function getHandler(string $type, string $entityType): ?EmailNotificationHandler
{
$key = $type . '-' . $entityType;
if (!array_key_exists($key, $this->emailNotificationEntityHandlerHash)) {
$this->emailNotificationEntityHandlerHash[$key] = null;
/** @var ?class-string<EmailNotificationHandler> $className */
$className = $this->metadata
->get(['notificationDefs', $entityType, 'emailNotificationHandlerClassNameMap', $type]);
if ($className && class_exists($className)) {
$handler = $this->injectableFactory->create($className);
$this->emailNotificationEntityHandlerHash[$key] = $handler;
}
}
/** @noinspection PhpExpressionAlwaysNullInspection */
return $this->emailNotificationEntityHandlerHash[$key];
}
protected function processNotificationNotePost(Note $note, User $user): void
{
$parentId = $note->getParentId();
$parentType = $note->getParentType();
$emailAddress = $user->getEmailAddress();
if (!$emailAddress) {
return;
}
$data = [];
$data['userName'] = $note->get('createdByName');
$post = Markdown::defaultTransform($note->getPost() ?? '');
$data['post'] = $post;
$parent = null;
if ($parentId && $parentType) {
$parent = $this->entityManager->getEntityById($parentType, $parentId);
if (!$parent) {
return;
}
$data['url'] = "{$this->getSiteUrl($user)}/#$parentType/view/$parentId";
$data['parentName'] = $parent->get(Field::NAME);
$data['parentType'] = $parentType;
$data['parentId'] = $parentId;
$data['name'] = $data['parentName'];
$data['entityType'] = $this->language->translateLabel($parentType, 'scopeNames');
$data['entityTypeLowerFirst'] = Util::mbLowerCaseFirst($data['entityType']);
$subjectTpl = $this->templateFileManager->getTemplate('notePost', 'subject', $parentType);
$bodyTpl = $this->templateFileManager->getTemplate('notePost', 'body', $parentType);
$subjectTpl = str_replace(["\n", "\r"], '', $subjectTpl);
$subject = $this->getHtmlizer()->render(
$note,
$subjectTpl,
'note-post-email-subject-' . $parentType,
$data,
true
);
$body = $this->getHtmlizer()->render(
$note,
$bodyTpl,
'note-post-email-body-' . $parentType,
$data,
true
);
} else {
$data['url'] = "{$this->getSiteUrl($user)}/#Notification";
$subjectTpl = $this->templateFileManager->getTemplate('notePostNoParent', 'subject');
$bodyTpl = $this->templateFileManager->getTemplate('notePostNoParent', 'body');
$subjectTpl = str_replace(["\n", "\r"], '', $subjectTpl);
$subject = $this->getHtmlizer()->render($note, $subjectTpl, 'note-post-email-subject', $data, true);
$body = $this->getHtmlizer()->render($note, $bodyTpl, 'note-post-email-body', $data, true);
}
/** @var Email $email */
$email = $this->entityManager->getNewEntity(Email::ENTITY_TYPE);
$email
->setSubject($subject)
->setBody($body)
->setIsHtml()
->addToAddress($emailAddress);
$email->set('isSystem', true);
if ($parentId && $parentType) {
$email->setParent(LinkParent::create($parentType, $parentId));
}
$senderParams = SenderParams::create();
if ($parent) {
$handler = $this->getHandler('notePost', $parent->getEntityType());
if ($handler) {
$handler->prepareEmail($email, $parent, $user);
$senderParams = $handler->getSenderParams($parent, $user) ?? $senderParams;
}
}
try {
$this->emailSender
->withParams($senderParams)
->send($email);
} catch (Exception $e) {
$this->log->error("Email notification: {$e->getMessage()}", ['exception' => $e]);
}
}
private function getSiteUrl(User $user): string
{
$portal = null;
if (!$user->isPortal()) {
return $this->applicationConfig->getSiteUrl();
}
if (!array_key_exists($user->getId(), $this->userIdPortalCacheMap)) {
$this->userIdPortalCacheMap[$user->getId()] = null;
$portalIdList = $user->getLinkMultipleIdList('portals');
$defaultPortalId = $this->config->get('defaultPortalId');
$portalId = null;
if (in_array($defaultPortalId, $portalIdList)) {
$portalId = $defaultPortalId;
} else if (count($portalIdList)) {
$portalId = $portalIdList[0];
}
if ($portalId) {
/** @var ?Portal $portal */
$portal = $this->entityManager->getEntityById(Portal::ENTITY_TYPE, $portalId);
}
if ($portal) {
$this->getPortalRepository()->loadUrlField($portal);
$this->userIdPortalCacheMap[$user->getId()] = $portal;
}
} else {
$portal = $this->userIdPortalCacheMap[$user->getId()];
}
if ($portal) {
return rtrim($portal->get('url'), '/');
}
return $this->applicationConfig->getSiteUrl();
}
protected function processNotificationNoteStatus(Note $note, User $user): void
{
$this->noteAccessControl->apply($note, $user);
$parentId = $note->getParentId();
$parentType = $note->getParentType();
$emailAddress = $user->getEmailAddress();
if (!$emailAddress) {
return;
}
$data = [];
if (!$parentId || !$parentType) {
return;
}
$parent = $this->entityManager->getEntityById($parentType, $parentId);
if (!$parent) {
return;
}
$note->loadParentNameField('superParent');
$data['url'] = "{$this->getSiteUrl($user)}/#$parentType/view/$parentId";
$data['parentName'] = $parent->get(Field::NAME);
$data['parentType'] = $parentType;
$data['parentId'] = $parentId;
$data['superParentName'] = $note->get('superParentName');
$data['superParentType'] = $note->getSuperParentType();
$data['superParentId'] = $note->getSuperParentId();
$data['name'] = $data['parentName'];
$data['entityType'] = $this->language->translateLabel($parentType, 'scopeNames');
$data['entityTypeLowerFirst'] = Util::mbLowerCaseFirst($data['entityType']);
$noteData = $note->getData();
$value = $noteData->value ?? null;
$field = $this->metadata->get("scopes.$parentType.statusField");
if ($value === null || !$field || !is_string($field)) {
return;
}
$data['value'] = $value;
$data['field'] = $field;
$data['valueTranslated'] = $this->language->translateOption($value, $field, $parentType);
$data['fieldTranslated'] = $this->language->translateLabel($field, 'fields', $parentType);
$data['fieldTranslatedLowerCase'] = Util::mbLowerCaseFirst($data['fieldTranslated']);
$data['userName'] = $note->get('createdByName');
$subjectTpl = $this->templateFileManager->getTemplate('noteStatus', 'subject', $parentType);
$bodyTpl = $this->templateFileManager->getTemplate('noteStatus', 'body', $parentType);
$subjectTpl = str_replace(["\n", "\r"], '', $subjectTpl);
$subject = $this->getHtmlizer()->render(
entity: $note,
template: $subjectTpl,
cacheId: 'note-status-email-subject',
additionalData: $data,
skipLinks: true,
);
$body = $this->getHtmlizer()->render(
entity: $note,
template: $bodyTpl,
cacheId: 'note-status-email-body',
additionalData: $data,
skipLinks: true,
);
$email = $this->entityManager->getRDBRepositoryByClass(Email::class)->getNew();
$email
->setSubject($subject)
->setBody($body)
->setIsHtml()
->addToAddress($emailAddress)
->setParent(LinkParent::create($parentType, $parentId));
$email->set('isSystem', true);
$senderParams = SenderParams::create();
$handler = $this->getHandler('status', $parentType);
if ($handler) {
$handler->prepareEmail($email, $parent, $user);
$senderParams = $handler->getSenderParams($parent, $user) ?? $senderParams;
}
$sender = $this->emailSender->withParams($senderParams);
try {
$sender->send($email);
} catch (Exception $e) {
$this->log->error("Email notification: {$e->getMessage()}", ['exception' => $e]);
}
}
protected function processNotificationNoteEmailReceived(Note $note, User $user): void
{
$parentId = $note->get('parentId');
$parentType = $note->getParentType();
$allowedEntityTypeList = $this->config->get('streamEmailNotificationsEmailReceivedEntityTypeList');
if (
is_array($allowedEntityTypeList) &&
!in_array($parentType, $allowedEntityTypeList)
) {
return;
}
$emailAddress = $user->getEmailAddress();
if (!$emailAddress) {
return;
}
$noteData = $note->getData();
if (!isset($noteData->emailId)) {
return;
}
$emailSubject = $this->entityManager->getEntityById(Email::ENTITY_TYPE, $noteData->emailId);
if (!$emailSubject) {
return;
}
$emailAddresses = $this->entityManager
->getRelation($user, Link::EMAIL_ADDRESSES)
->find();
foreach ($emailAddresses as $ea) {
if (
$this->entityManager->getRelation($emailSubject, 'toEmailAddresses')->isRelated($ea) ||
$this->entityManager->getRelation($emailSubject, 'ccEmailAddresses')->isRelated($ea)
) {
return;
}
}
$data = [];
$data['fromName'] = '';
if (isset($noteData->personEntityName)) {
$data['fromName'] = $noteData->personEntityName;
} else if (isset($noteData->fromString)) {
$data['fromName'] = $noteData->fromString;
}
$data['subject'] = '';
if (isset($noteData->emailName)) {
$data['subject'] = $noteData->emailName;
}
$data['post'] = nl2br($note->get('post'));
if (!$parentId || !$parentType) {
return;
}
$parent = $this->entityManager->getEntityById($parentType, $parentId);
if (!$parent) {
return;
}
$data['url'] = "{$this->getSiteUrl($user)}/#$parentType/view/$parentId";
$data['parentName'] = $parent->get(Field::NAME);
$data['parentType'] = $parentType;
$data['parentId'] = $parentId;
$data['name'] = $data['parentName'];
$data['entityType'] = $this->language->translateLabel($parentType, 'scopeNames');
$data['entityTypeLowerFirst'] = Util::mbLowerCaseFirst($data['entityType']);
$subjectTpl = $this->templateFileManager->getTemplate('noteEmailReceived', 'subject', $parentType);
$bodyTpl = $this->templateFileManager->getTemplate('noteEmailReceived', 'body', $parentType);
$subjectTpl = str_replace(["\n", "\r"], '', $subjectTpl);
$subject = $this->getHtmlizer()->render(
$note,
$subjectTpl,
'note-email-received-email-subject-' . $parentType,
$data,
true
);
$body = $this->getHtmlizer()->render(
$note,
$bodyTpl,
'note-email-received-email-body-' . $parentType,
$data,
true
);
/** @var Email $email */
$email = $this->entityManager->getNewEntity(Email::ENTITY_TYPE);
$email
->setSubject($subject)
->setBody($body)
->setIsHtml()
->addToAddress($emailAddress)
->setParent(LinkParent::create($parentType, $parentId));
$email->set('isSystem', true);
$senderParams = SenderParams::create();
$handler = $this->getHandler('emailReceived', $parentType);
if ($handler) {
$handler->prepareEmail($email, $parent, $user);
$senderParams = $handler->getSenderParams($parent, $user) ?? $senderParams;
}
try {
$this->emailSender
->withParams($senderParams)
->send($email);
} catch (Exception $e) {
$this->log->error("Email notification: {$e->getMessage()}", ['exception' => $e]);
}
}
private function getHtmlizer(): Htmlizer
{
if (!$this->htmlizer) {
$this->htmlizer = $this->htmlizerFactory->create(true);
}
return $this->htmlizer;
}
private function getPortalRepository(): PortalRepository
{
/** @var PortalRepository */
return $this->entityManager->getRepository(Portal::ENTITY_TYPE);
}
/**
* @return string[]
*/
private function getNoteNotificationTypeList(): array
{
/** @var string[] $output */
$output = $this->config->get('streamEmailNotificationsTypeList', []);
if (in_array(self::TYPE_STATUS, $output)) {
$output[] = Note::TYPE_UPDATE;
$output = array_values(array_filter($output, fn ($v) => $v !== self::TYPE_STATUS));
}
return $output;
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

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