diff --git a/application/Espo/Binding.php b/application/Espo/Binding.php index 3403b46aec..ac4056428a 100644 --- a/application/Espo/Binding.php +++ b/application/Espo/Binding.php @@ -317,6 +317,11 @@ class Binding implements BindingProcessor 'Espo\\Core\\Mail\\Importer', 'Espo\\Core\\Mail\\Importer\\DefaultImporter' ); + + $binder->bindImplementation( + 'Espo\\Core\\Mail\\Importer\\AutoReplyDetector', + 'Espo\\Core\\Mail\\Importer\\DefaultAutoReplyDetector' + ); } private function bindAcl(Binder $binder): void diff --git a/application/Espo/Core/Mail/Account/GroupAccount/Hooks/AfterFetch.php b/application/Espo/Core/Mail/Account/GroupAccount/Hooks/AfterFetch.php index 95f2dce0d7..120d9fa17f 100644 --- a/application/Espo/Core/Mail/Account/GroupAccount/Hooks/AfterFetch.php +++ b/application/Espo/Core/Mail/Account/GroupAccount/Hooks/AfterFetch.php @@ -244,26 +244,22 @@ class AfterFetch implements AfterFetchInterface $subject = $replyData->getSubject(); - if ($case) { - $subject = '[#' . $case->get('number'). '] ' . $subject; + if ($case && $case->getNumber() !== null) { + $subject = "[#{$case->getNumber()}] $subject"; } - /** @var Email $reply */ $reply = $this->entityManager->getRDBRepositoryByClass(Email::class)->getNew(); $reply ->addToAddress($fromAddress) ->setSubject($subject) ->setBody($replyData->getBody()) - ->setIsHtml($replyData->isHtml()); - - if ($email->has('teamsIds')) { - $reply->set('teamsIds', $email->get('teamsIds')); - } + ->setIsHtml($replyData->isHtml()) + ->setIsAutoReply() + ->setTeams($email->getTeams()); if ($email->getParentId() && $email->getParentType()) { - $reply->set('parentId', $email->getParentId()); - $reply->set('parentType', $email->getParentType()); + $reply->setParent($email->getParent()); } $this->entityManager->saveEntity($reply); diff --git a/application/Espo/Core/Mail/Account/GroupAccount/Hooks/BeforeFetch.php b/application/Espo/Core/Mail/Account/GroupAccount/Hooks/BeforeFetch.php index cbe33bf395..61da506be0 100644 --- a/application/Espo/Core/Mail/Account/GroupAccount/Hooks/BeforeFetch.php +++ b/application/Espo/Core/Mail/Account/GroupAccount/Hooks/BeforeFetch.php @@ -32,6 +32,7 @@ namespace Espo\Core\Mail\Account\GroupAccount\Hooks; use Espo\Core\Mail\Account\Hook\BeforeFetch as BeforeFetchInterface; use Espo\Core\Mail\Account\Hook\BeforeFetchResult; use Espo\Core\Mail\Account\Account; +use Espo\Core\Mail\Importer\AutoReplyDetector; use Espo\Core\Mail\Message; use Espo\Core\Mail\Account\GroupAccount\BouncedRecognizer; use Espo\Core\Utils\Log; @@ -50,6 +51,7 @@ class BeforeFetch implements BeforeFetchInterface private EntityManager $entityManager, private BouncedRecognizer $bouncedRecognizer, private CampaignService $campaignService, + private AutoReplyDetector $autoReplyDetector, ) {} public function process(Account $account, Message $message): BeforeFetchResult @@ -125,22 +127,7 @@ class BeforeFetch implements BeforeFetchInterface private function checkMessageIsAutoReply(Message $message): bool { - if ($message->getHeader('X-Autoreply')) { - return true; - } - - if ($message->getHeader('X-Autorespond')) { - return true; - } - - if ( - $message->getHeader('Auto-submitted') && - strtolower($message->getHeader('Auto-submitted')) !== 'no' - ) { - return true; - } - - return false; + return $this->autoReplyDetector->detect($message); } private function checkMessageCannotBeAutoReplied(Message $message): bool diff --git a/application/Espo/Core/Mail/Importer/AutoReplyDetector.php b/application/Espo/Core/Mail/Importer/AutoReplyDetector.php new file mode 100644 index 0000000000..2c6a04764b --- /dev/null +++ b/application/Espo/Core/Mail/Importer/AutoReplyDetector.php @@ -0,0 +1,42 @@ +. + * + * 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\Importer; + +use Espo\Core\Mail\Message; + +/** + * Detects if an email is auto-response. + * + * @since 9.2.0 + */ +interface AutoReplyDetector +{ + public function detect(Message $message): bool; +} diff --git a/application/Espo/Core/Mail/Importer/DefaultAutoReplyDetector.php b/application/Espo/Core/Mail/Importer/DefaultAutoReplyDetector.php new file mode 100644 index 0000000000..eac8e75cfa --- /dev/null +++ b/application/Espo/Core/Mail/Importer/DefaultAutoReplyDetector.php @@ -0,0 +1,55 @@ +. + * + * 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\Importer; + +use Espo\Core\Mail\Message; + +class DefaultAutoReplyDetector implements AutoReplyDetector +{ + public function detect(Message $message): bool + { + if ($message->getHeader('X-Autoreply')) { + return true; + } + + if ($message->getHeader('X-Autorespond')) { + return true; + } + + if ( + $message->getHeader('Auto-submitted') && + strtolower($message->getHeader('Auto-submitted')) !== 'no' + ) { + return true; + } + + return false; + } +} diff --git a/application/Espo/Core/Mail/Importer/DefaultImporter.php b/application/Espo/Core/Mail/Importer/DefaultImporter.php index 1f53fa1f5f..4ec21632ed 100644 --- a/application/Espo/Core/Mail/Importer/DefaultImporter.php +++ b/application/Espo/Core/Mail/Importer/DefaultImporter.php @@ -84,7 +84,8 @@ class DefaultImporter implements Importer private LinkMultipleSaver $linkMultipleSaver, private DuplicateFinder $duplicateFinder, private JobSchedulerFactory $jobSchedulerFactory, - private ParentFinder $parentFinder + private ParentFinder $parentFinder, + private AutoReplyDetector $autoReplyDetector, ) { $this->notificator = $notificatorFactory->createByClass(Email::class); $this->filtersMatcher = new FiltersMatcher(); @@ -146,6 +147,8 @@ class DefaultImporter implements Importer return $duplicate; } + $email->setIsAutoReply($this->autoReplyDetector->detect($message)); + $this->processDeliveryDate($parser, $message, $email); if (!$email->getDateSent()) { diff --git a/application/Espo/Entities/Email.php b/application/Espo/Entities/Email.php index c069bd744e..01cee0acad 100644 --- a/application/Espo/Entities/Email.php +++ b/application/Espo/Entities/Email.php @@ -893,4 +893,14 @@ class Email extends Entity { return $this->set('isReplied', $isReplied); } + + public function isAutoReply(): bool + { + return $this->get('isAutoReply'); + } + + public function setIsAutoReply(bool $isAutoReply = true): self + { + return $this->set('isAutoReply', $isAutoReply); + } } diff --git a/application/Espo/Modules/Crm/Entities/CaseObj.php b/application/Espo/Modules/Crm/Entities/CaseObj.php index d9b1318fe1..b60e43c942 100644 --- a/application/Espo/Modules/Crm/Entities/CaseObj.php +++ b/application/Espo/Modules/Crm/Entities/CaseObj.php @@ -50,6 +50,11 @@ class CaseObj extends Entity protected $entityType = 'Case'; + public function getNumber(): ?int + { + return $this->get('number'); + } + public function setName(?string $name): self { return $this->set(Field::NAME, $name); diff --git a/application/Espo/Resources/i18n/en_US/Email.json b/application/Espo/Resources/i18n/en_US/Email.json index e0e43b4ef9..af56d438e7 100644 --- a/application/Espo/Resources/i18n/en_US/Email.json +++ b/application/Espo/Resources/i18n/en_US/Email.json @@ -61,7 +61,8 @@ "icsEventDateStart": "ICS Event Date Start", "groupFolder": "Group Folder", "groupStatusFolder": "Group Status Folder", - "sendAt": "Send At" + "sendAt": "Send At", + "isAutoReply": "Is Auto-Reply" }, "links": { "replied": "Replied", diff --git a/application/Espo/Resources/metadata/entityDefs/Email.json b/application/Espo/Resources/metadata/entityDefs/Email.json index 21ef4e2b0d..717efca43a 100644 --- a/application/Espo/Resources/metadata/entityDefs/Email.json +++ b/application/Espo/Resources/metadata/entityDefs/Email.json @@ -443,6 +443,11 @@ "Espo\\Classes\\FieldValidators\\Email\\SendAt\\Future" ] }, + "isAutoReply": { + "type": "bool", + "readOnly": true, + "fieldManagerParamList": [] + }, "createdAt": { "type": "datetime", "readOnly": true, diff --git a/application/Espo/Resources/metadata/selectDefs/Email.json b/application/Espo/Resources/metadata/selectDefs/Email.json index 206caf0e92..b829c0ed1d 100644 --- a/application/Espo/Resources/metadata/selectDefs/Email.json +++ b/application/Espo/Resources/metadata/selectDefs/Email.json @@ -31,7 +31,11 @@ "textFilterClassName": "Espo\\Classes\\Select\\Email\\TextFilter", "textFilterUseContainsAttributeList": ["name"], "selectAttributesDependencyMap": { - "subject": ["name"], + "subject": [ + "name", + "isAutoReply", + "hasAttachment" + ], "personStringData": ["fromString", "fromEmailAddressId"], "replyToName": ["replyToString"] } diff --git a/client/res/templates/email/fields/subject/list-link.tpl b/client/res/templates/email/fields/subject/list-link.tpl index a06de148bd..935d12d368 100644 --- a/client/res/templates/email/fields/subject/list-link.tpl +++ b/client/res/templates/email/fields/subject/list-link.tpl @@ -7,17 +7,25 @@ title="{{value}}" >{{value}} - {{#if hasAttachment}} - - - + {{#if hasIcon}} + + {{#if hasAttachment}} + + {{/if}} + {{#if isAutoReply}} + + {{/if}} + {{/if}} diff --git a/client/src/views/email/fields/subject.js b/client/src/views/email/fields/subject.js index 77629552fa..5c3a817797 100644 --- a/client/src/views/email/fields/subject.js +++ b/client/src/views/email/fields/subject.js @@ -39,6 +39,17 @@ class EmailSubjectFieldView extends VarcharFieldView { data.isImportant = this.model.has('isImportant') && this.model.get('isImportant'); data.hasAttachment = this.model.has('hasAttachment') && this.model.get('hasAttachment'); data.isReplied = this.model.has('isReplied') && this.model.get('isReplied'); + data.isAutoReply = this.model.has('isAutoReply') && this.model.attributes.isAutoReply; + + data.hasIcon = data.hasAttachment || data.isAutoReply; + + if (data.hasIcon) { + data.iconCount = 1; + + if (data.hasAttachment && data.isAutoReply) { + data.iconCount = 2; + } + } data.inTrash = this.model.attributes.groupFolderId ? this.model.attributes.groupStatusFolder === 'Trash' : @@ -88,6 +99,7 @@ class EmailSubjectFieldView extends VarcharFieldView { 'hasAttachment', 'inTrash', 'groupStatusFolder', + 'isAutoReply', ]; } diff --git a/frontend/less/espo/custom.less b/frontend/less/espo/custom.less index 0d4af13d80..3c739a5bd6 100644 --- a/frontend/less/espo/custom.less +++ b/frontend/less/espo/custom.less @@ -374,6 +374,18 @@ div.list-expanded > ul > li > div.expanded-row { display: inline-block; } } + + &:has(.list-icon-container[data-icon-count="2"]) { + > span { + > span:first-child { + width: calc(100% - var(--32px)); + } + + > span.list-icon-container { + width: var(--32px); + } + } + } } } }