1# -- 2# Copyright (C) 2001-2020 OTRS AG, https://otrs.com/ 3# -- 4# This software comes with ABSOLUTELY NO WARRANTY. For details, see 5# the enclosed file COPYING for license information (GPL). If you 6# did not receive this file, see https://www.gnu.org/licenses/gpl-3.0.txt. 7# -- 8 9package Kernel::System::Ticket::Article::Backend::MIMEBase; 10 11use strict; 12use warnings; 13 14use parent 'Kernel::System::Ticket::Article::Backend::Base'; 15 16use Kernel::System::EmailParser; 17use Kernel::System::VariableCheck qw(:all); 18 19our @ObjectDependencies = ( 20 'Kernel::Config', 21 'Kernel::System::Cache', 22 'Kernel::System::DB', 23 'Kernel::System::Email', 24 'Kernel::System::HTMLUtils', 25 'Kernel::System::Log', 26 'Kernel::System::Main', 27 'Kernel::System::Ticket', 28 'Kernel::System::Ticket::Article', 29 'Kernel::System::Ticket::Article::Backend::Email', 30 'Kernel::System::User', 31); 32 33=head1 NAME 34 35Kernel::System::Ticket::Article::Backend::MIMEBase - base class for all C<MIME> based article backends 36 37=head1 DESCRIPTION 38 39This is a base class for article data in C<MIME> format and should not be instantiated directly. 40Always get real backend instead, i.e. C<Email>, C<Phone> or C<Internal>. 41 42Basic article data is always stored in a database table, but extended data uses configured article 43storage backend. For plain text representation of the message, use C<Body> field. For original 44message with email headers, use L</ArticlePlain()> method to retrieve it from storage backend. 45Attachments are handled by the storage backends, and can be retrieved via L</ArticleAttachment()>. 46 47Inherits from L<Kernel::System::Ticket::Article::Backend::Base>. 48 49See also L<Kernel::System::Ticket::Article::Backend::MIMEBase::Base> and 50L<Kernel::System::Ticket::Article::Backend::Email>. 51 52=head1 PUBLIC INTERFACE 53 54=head2 new() 55 56Don't instantiate this class directly, get instances of the real backends instead: 57 58 my $ArticleBackendObject = $Kernel::OM->Get('Kernel::System::Ticket::Article')->BackendForChannel(ChannelName => 'Email'); 59 60=cut 61 62sub new { 63 my ( $Type, %Param ) = @_; 64 65 # Call constructor of the base class. 66 my $Self = $Type->SUPER::new(%Param); 67 68 # 0=off; 1=on; 69 $Self->{Debug} = $Param{Debug} || 0; 70 71 # Persistent for this object's lifetime so that we can have article objects with different storage modules. 72 $Self->{ArticleStorageModule} = $Param{ArticleStorageModule} 73 || $Kernel::OM->Get('Kernel::Config')->Get('Ticket::Article::Backend::MIMEBase::ArticleStorage') 74 || 'Kernel::System::Ticket::Article::Backend::MIMEBase::ArticleStorageDB'; 75 76 return $Self; 77} 78 79=head2 ArticleCreate() 80 81Create a MIME article. 82 83 my $ArticleID = $ArticleBackendObject->ArticleCreate( 84 TicketID => 123, # (required) 85 SenderTypeID => 1, # (required) 86 # or 87 SenderType => 'agent', # (required) agent|system|customer 88 IsVisibleForCustomer => 1, # (required) Is article visible for customer? 89 UserID => 123, # (required) 90 91 From => 'Some Agent <email@example.com>', # not required but useful 92 To => 'Some Customer A <customer-a@example.com>', # not required but useful 93 Cc => 'Some Customer B <customer-b@example.com>', # not required but useful 94 Bcc => 'Some Customer C <customer-c@example.com>', # not required but useful 95 ReplyTo => 'Some Customer B <customer-b@example.com>', # not required 96 Subject => 'some short description', # not required but useful 97 Body => 'the message text', # not required but useful 98 MessageID => '<asdasdasd.123@example.com>', # not required but useful 99 InReplyTo => '<asdasdasd.12@example.com>', # not required but useful 100 References => '<asdasdasd.1@example.com> <asdasdasd.12@example.com>', # not required but useful 101 ContentType => 'text/plain; charset=ISO-8859-15', # or optional Charset & MimeType 102 HistoryType => 'OwnerUpdate', # EmailCustomer|Move|AddNote|PriorityUpdate|WebRequestCustomer|... 103 HistoryComment => 'Some free text!', 104 Attachment => [ 105 { 106 Content => $Content, 107 ContentType => $ContentType, 108 Filename => 'lala.txt', 109 }, 110 { 111 Content => $Content, 112 ContentType => $ContentType, 113 Filename => 'lala1.txt', 114 }, 115 ], 116 NoAgentNotify => 0, # if you don't want to send agent notifications 117 AutoResponseType => 'auto reply' # auto reject|auto follow up|auto reply/new ticket|auto remove 118 119 ForceNotificationToUserID => [ 1, 43, 56 ], # if you want to force somebody 120 ExcludeNotificationToUserID => [ 43,56 ], # if you want full exclude somebody from notfications, 121 # will also be removed in To: line of article, 122 # higher prio as ForceNotificationToUserID 123 ExcludeMuteNotificationToUserID => [ 43,56 ], # the same as ExcludeNotificationToUserID but only the 124 # sending gets muted, agent will still shown in To: 125 # line of article 126 ); 127 128Example with "Charset & MimeType" and no "ContentType". 129 130 my $ArticleID = $ArticleBackendObject->ArticleCreate( 131 TicketID => 123, # (required) 132 SenderType => 'agent', # (required) agent|system|customer 133 IsVisibleForCustomer => 1, # (required) Is article visible for customer? 134 135 From => 'Some Agent <email@example.com>', # not required but useful 136 To => 'Some Customer A <customer-a@example.com>', # not required but useful 137 Subject => 'some short description', # required 138 Body => 'the message text', # required 139 Charset => 'ISO-8859-15', 140 MimeType => 'text/plain', 141 HistoryType => 'OwnerUpdate', # EmailCustomer|Move|AddNote|PriorityUpdate|WebRequestCustomer|... 142 HistoryComment => 'Some free text!', 143 UserID => 123, 144 UnlockOnAway => 1, # Unlock ticket if owner is away 145 ); 146 147Events: 148 ArticleCreate 149 150=cut 151 152sub ArticleCreate { 153 my ( $Self, %Param ) = @_; 154 155 my $IncomingTime = $Kernel::OM->Create('Kernel::System::DateTime')->ToEpoch(); 156 157 my $ArticleContentPath = $Kernel::OM->Get( $Self->{ArticleStorageModule} )->BuildArticleContentPath(); 158 159 # create ArticleContentPath 160 if ( !$ArticleContentPath ) { 161 $Kernel::OM->Get('Kernel::System::Log')->Log( 162 Priority => 'error', 163 Message => 'Need ArticleContentPath!' 164 ); 165 return; 166 } 167 168 my $ArticleObject = $Kernel::OM->Get('Kernel::System::Ticket::Article'); 169 170 # Lookup if no ID is passed. 171 if ( $Param{SenderType} && !$Param{SenderTypeID} ) { 172 $Param{SenderTypeID} = $ArticleObject->ArticleSenderTypeLookup( SenderType => $Param{SenderType} ); 173 } 174 175 # check needed stuff 176 for my $Needed (qw(TicketID UserID SenderTypeID HistoryType HistoryComment)) { 177 if ( !$Param{$Needed} ) { 178 $Kernel::OM->Get('Kernel::System::Log')->Log( 179 Priority => 'error', 180 Message => "Need $Needed!" 181 ); 182 return; 183 } 184 } 185 186 if ( !defined $Param{IsVisibleForCustomer} ) { 187 $Kernel::OM->Get('Kernel::System::Log')->Log( 188 Priority => 'error', 189 Message => "Need IsVisibleForCustomer!" 190 ); 191 return; 192 } 193 194 # check ContentType vs. Charset & MimeType 195 if ( !$Param{ContentType} ) { 196 for my $Item (qw(Charset MimeType)) { 197 if ( !$Param{$Item} ) { 198 $Kernel::OM->Get('Kernel::System::Log')->Log( 199 Priority => 'error', 200 Message => "Need $Item!" 201 ); 202 return; 203 } 204 } 205 $Param{ContentType} = "$Param{MimeType}; charset=$Param{Charset}"; 206 } 207 else { 208 for my $Item (qw(ContentType)) { 209 if ( !$Param{$Item} ) { 210 $Kernel::OM->Get('Kernel::System::Log')->Log( 211 Priority => 'error', 212 Message => "Need $Item!" 213 ); 214 return; 215 } 216 } 217 $Param{Charset} = ''; 218 if ( $Param{ContentType} =~ /charset=/i ) { 219 $Param{Charset} = $Param{ContentType}; 220 $Param{Charset} =~ s/.+?charset=("|'|)(\w+)/$2/gi; 221 $Param{Charset} =~ s/"|'//g; 222 $Param{Charset} =~ s/(.+?);.*/$1/g; 223 224 } 225 $Param{MimeType} = ''; 226 if ( $Param{ContentType} =~ /^(\w+\/\w+)/i ) { 227 $Param{MimeType} = $1; 228 $Param{MimeType} =~ s/"|'//g; 229 } 230 } 231 232 my $TicketObject = $Kernel::OM->Get('Kernel::System::Ticket'); 233 234 # for the event handler, before any actions have taken place 235 my %OldTicketData = $TicketObject->TicketGet( 236 TicketID => $Param{TicketID}, 237 DynamicFields => 1, 238 ); 239 240 # get html utils object 241 my $HTMLUtilsObject = $Kernel::OM->Get('Kernel::System::HTMLUtils'); 242 243 # add 'no body' if there is no body there! 244 my @AttachmentConvert; 245 if ( !defined $Param{Body} ) { # allow '0' as body 246 $Param{Body} = ''; 247 } 248 249 # process html article 250 elsif ( $Param{MimeType} =~ /text\/html/i ) { 251 252 # add html article as attachment 253 my $Attach = { 254 Content => $Param{Body}, 255 ContentType => "text/html; charset=\"$Param{Charset}\"", 256 Filename => 'file-2', 257 }; 258 push @AttachmentConvert, $Attach; 259 260 # get ASCII body 261 $Param{MimeType} = 'text/plain'; 262 $Param{ContentType} =~ s/html/plain/i; 263 $Param{Body} = $HTMLUtilsObject->ToAscii( 264 String => $Param{Body}, 265 ); 266 } 267 elsif ( $Param{MimeType} && $Param{MimeType} eq "application/json" ) { 268 269 # Keep JSON body unchanged 270 } 271 272 # if body isn't text, attach body as attachment (mostly done by OE) :-/ 273 elsif ( $Param{MimeType} && $Param{MimeType} !~ /\btext\b/i ) { 274 275 # Add non-text as an attachment. Try to deduce the filename from ContentType or ContentDisposition headers. 276 # Please see bug#13644 for more information. 277 my $FileName = 'unknown'; 278 if ( 279 $Param{ContentType} =~ /name="(.+?)"/i 280 || ( 281 defined $Param{ContentDisposition} 282 && $Param{ContentDisposition} =~ /name="(.+?)"/i 283 ) 284 ) 285 { 286 $FileName = $1; 287 } 288 my $Attach = { 289 Content => $Param{Body}, 290 ContentType => $Param{ContentType}, 291 Filename => $FileName, 292 }; 293 push @{ $Param{Attachment} }, $Attach; 294 295 # set ASCII body 296 $Param{MimeType} = 'text/plain'; 297 $Param{ContentType} = 'text/plain'; 298 $Param{Body} = '- no text message => see attachment -'; 299 $Param{OrigHeader}->{Body} = $Param{Body}; 300 } 301 302 # fix some bad stuff from some browsers (Opera)! 303 else { 304 $Param{Body} =~ s/(\n\r|\r\r\n|\r\n)/\n/g; 305 } 306 307 # strip not wanted stuff 308 for my $Attribute (qw(From To Cc Bcc Subject MessageID InReplyTo References ReplyTo)) { 309 if ( defined $Param{$Attribute} ) { 310 $Param{$Attribute} =~ s/\n|\r//g; 311 } 312 else { 313 $Param{$Attribute} = ''; 314 } 315 } 316 ATTRIBUTE: 317 for my $Attribute (qw(MessageID)) { 318 next ATTRIBUTE if !$Param{$Attribute}; 319 $Param{$Attribute} = substr( $Param{$Attribute}, 0, 3800 ); 320 } 321 322 # Check if this is the first article (for notifications). 323 my @Articles = $ArticleObject->ArticleList( TicketID => $Param{TicketID} ); 324 my $FirstArticle = scalar @Articles ? 0 : 1; 325 326 my $MainObject = $Kernel::OM->Get('Kernel::System::Main'); 327 328 # calculate MD5 of Message ID 329 if ( $Param{MessageID} ) { 330 $Param{MD5} = $MainObject->MD5sum( String => $Param{MessageID} ); 331 } 332 333 # Generate unique fingerprint for searching created article in database to prevent race conditions 334 # (see https://bugs.otrs.org/show_bug.cgi?id=12438). 335 my $RandomString = $MainObject->GenerateRandomString( 336 Length => 32, 337 ); 338 my $ArticleInsertFingerprint = $$ . '-' . $RandomString . '-' . ( $Param{MessageID} // '' ); 339 340 # get database object 341 my $DBObject = $Kernel::OM->Get('Kernel::System::DB'); 342 343 # Create meta article. 344 my $ArticleID = $Self->_MetaArticleCreate( 345 TicketID => $Param{TicketID}, 346 SenderTypeID => $Param{SenderTypeID}, 347 IsVisibleForCustomer => $Param{IsVisibleForCustomer}, 348 CommunicationChannelID => $Self->ChannelIDGet(), 349 UserID => $Param{UserID}, 350 ); 351 if ( !$ArticleID ) { 352 $Kernel::OM->Get('Kernel::System::Log')->Log( 353 Priority => 'error', 354 Message => "Can't create meta article (TicketID=$Param{TicketID})!", 355 ); 356 return; 357 } 358 359 my $UserObject = $Kernel::OM->Get('Kernel::System::User'); 360 361 # Check if there are additional To's from InvolvedAgent and InformAgent. 362 # See bug#13422 (https://bugs.otrs.org/show_bug.cgi?id=13422). 363 if ( $Param{ForceNotificationToUserID} && ref $Param{ForceNotificationToUserID} eq 'ARRAY' ) { 364 my $NewTo = ''; 365 USER: 366 for my $UserID ( @{ $Param{ForceNotificationToUserID} } ) { 367 368 next USER if $UserID == 1; 369 next USER if grep { $UserID eq $_ } @{ $Param{ExcludeNotificationToUserID} }; 370 371 my %UserData = $UserObject->GetUserData( 372 UserID => $UserID, 373 Valid => 1, 374 ); 375 376 next USER if !%UserData; 377 378 if ( $Param{To} || $NewTo ) { 379 $NewTo .= ', '; 380 } 381 $NewTo .= "\"$UserData{UserFirstname} $UserData{UserLastname}\" <$UserData{UserEmail}>"; 382 } 383 $Param{To} .= $NewTo; 384 } 385 386 return if !$DBObject->Do( 387 SQL => ' 388 INSERT INTO article_data_mime ( 389 article_id, a_from, a_reply_to, a_to, a_cc, a_bcc, a_subject, a_message_id, 390 a_message_id_md5, a_in_reply_to, a_references, a_content_type, a_body, 391 incoming_time, content_path, create_time, create_by, change_time, change_by) 392 VALUES ( 393 ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, current_timestamp, ?, current_timestamp, ? 394 ) 395 ', 396 Bind => [ 397 \$ArticleID, \$Param{From}, \$Param{ReplyTo}, \$Param{To}, \$Param{Cc}, \$Param{Bcc}, 398 \$Param{Subject}, 399 \$ArticleInsertFingerprint, # just for next search; will be updated with correct MessageID 400 \$Param{MD5}, \$Param{InReplyTo}, \$Param{References}, \$Param{ContentType}, 401 \$Param{Body}, \$IncomingTime, \$ArticleContentPath, \$Param{UserID}, \$Param{UserID}, 402 ], 403 ); 404 405 # Get article data ID. 406 return if !$DBObject->Prepare( 407 SQL => ' 408 SELECT id FROM article_data_mime 409 WHERE article_id = ? 410 AND a_message_id = ? 411 AND incoming_time = ? 412 ORDER BY id DESC 413 ', 414 Bind => [ \$ArticleID, \$ArticleInsertFingerprint, \$IncomingTime, ], 415 Limit => 1, 416 ); 417 418 my $ArticleDataID; 419 while ( my @Row = $DBObject->FetchrowArray() ) { 420 $ArticleDataID = $Row[0]; 421 } 422 423 # Return if article data record was not created. 424 if ( !$ArticleDataID ) { 425 $Kernel::OM->Get('Kernel::System::Log')->Log( 426 Priority => 'error', 427 Message => 428 "Can't store article data (TicketID=$Param{TicketID}, ArticleID=$ArticleID, MessageID=$Param{MessageID})!", 429 ); 430 return; 431 } 432 433 # Save correct Message-ID now. 434 return if !$DBObject->Do( 435 SQL => 'UPDATE article_data_mime SET a_message_id = ? WHERE id = ?', 436 Bind => [ \$Param{MessageID}, \$ArticleDataID, ], 437 ); 438 439 # check for base64 encoded images in html body and upload them 440 for my $Attachment (@AttachmentConvert) { 441 442 if ( 443 $Attachment->{ContentType} eq "text/html; charset=\"$Param{Charset}\"" 444 && $Attachment->{Filename} eq 'file-2' 445 ) 446 { 447 $HTMLUtilsObject->EmbeddedImagesExtract( 448 DocumentRef => \$Attachment->{Content}, 449 AttachmentsRef => \@AttachmentConvert, 450 ); 451 } 452 } 453 454 # add converted attachments 455 for my $Attachment (@AttachmentConvert) { 456 $Kernel::OM->Get( $Self->{ArticleStorageModule} )->ArticleWriteAttachment( 457 %{$Attachment}, 458 ArticleID => $ArticleID, 459 UserID => $Param{UserID}, 460 ); 461 } 462 463 # add attachments 464 if ( $Param{Attachment} ) { 465 for my $Attachment ( @{ $Param{Attachment} } ) { 466 $Kernel::OM->Get( $Self->{ArticleStorageModule} )->ArticleWriteAttachment( 467 %{$Attachment}, 468 ArticleID => $ArticleID, 469 UserID => $Param{UserID}, 470 ); 471 } 472 } 473 474 $ArticleObject->_ArticleCacheClear( 475 TicketID => $Param{TicketID}, 476 ); 477 478 # add history row 479 $TicketObject->HistoryAdd( 480 ArticleID => $ArticleID, 481 TicketID => $Param{TicketID}, 482 CreateUserID => $Param{UserID}, 483 HistoryType => $Param{HistoryType}, 484 Name => $Param{HistoryComment}, 485 ); 486 487 my $ConfigObject = $Kernel::OM->Get('Kernel::Config'); 488 489 # unlock ticket if the owner is away (and the feature is enabled) 490 if ( 491 $Param{UnlockOnAway} 492 && $OldTicketData{Lock} eq 'lock' 493 && $ConfigObject->Get('Ticket::UnlockOnAway') 494 ) 495 { 496 my %OwnerInfo = $UserObject->GetUserData( 497 UserID => $OldTicketData{OwnerID}, 498 ); 499 500 if ( $OwnerInfo{OutOfOfficeMessage} ) { 501 $TicketObject->TicketLockSet( 502 TicketID => $Param{TicketID}, 503 Lock => 'unlock', 504 UserID => $Param{UserID}, 505 ); 506 $Kernel::OM->Get('Kernel::System::Log')->Log( 507 Priority => 'notice', 508 Message => 509 "Ticket [$OldTicketData{TicketNumber}] unlocked, current owner is out of office!", 510 ); 511 } 512 } 513 514 $ArticleObject->ArticleSearchIndexBuild( 515 TicketID => $Param{TicketID}, 516 ArticleID => $ArticleID, 517 UserID => 1, 518 ); 519 520 # event 521 $Self->EventHandler( 522 Event => 'ArticleCreate', 523 Data => { 524 ArticleID => $ArticleID, 525 TicketID => $Param{TicketID}, 526 OldTicketData => \%OldTicketData, 527 }, 528 UserID => $Param{UserID}, 529 ); 530 531 # reset unlock if needed 532 if ( !$Param{SenderType} ) { 533 $Param{SenderType} = $ArticleObject->ArticleSenderTypeLookup( SenderTypeID => $Param{SenderTypeID} ); 534 } 535 536 # reset unlock time if customer sent an update 537 if ( $Param{SenderType} eq 'customer' ) { 538 539 # Check if previous sender was an agent. 540 my $AgentSenderTypeID = $ArticleObject->ArticleSenderTypeLookup( SenderType => 'agent' ); 541 my $SystemSenderTypeID = $ArticleObject->ArticleSenderTypeLookup( SenderType => 'system' ); 542 my @Articles = $ArticleObject->ArticleList( 543 TicketID => $Param{TicketID}, 544 ); 545 546 my $LastSenderTypeID; 547 ARTICLE: 548 for my $Article ( reverse @Articles ) { 549 next ARTICLE if $Article->{ArticleID} eq $ArticleID; 550 next ARTICLE if $Article->{SenderTypeID} eq $SystemSenderTypeID; 551 $LastSenderTypeID = $Article->{SenderTypeID}; 552 last ARTICLE; 553 } 554 555 if ( $LastSenderTypeID && $LastSenderTypeID == $AgentSenderTypeID ) { 556 $TicketObject->TicketUnlockTimeoutUpdate( 557 UnlockTimeout => $IncomingTime, 558 TicketID => $Param{TicketID}, 559 UserID => $Param{UserID}, 560 ); 561 } 562 } 563 564 # check if latest article is sent to customer 565 elsif ( $Param{SenderType} eq 'agent' ) { 566 $TicketObject->TicketUnlockTimeoutUpdate( 567 UnlockTimeout => $IncomingTime, 568 TicketID => $Param{TicketID}, 569 UserID => $Param{UserID}, 570 ); 571 } 572 573 # send auto response 574 if ( $Param{AutoResponseType} ) { 575 576 # Check if SendAutoResponse() method exists, before calling it. If it doesn't, get an instance of an additional 577 # email backend first. 578 if ( $Self->can('SendAutoResponse') ) { 579 $Self->SendAutoResponse( 580 OrigHeader => $Param{OrigHeader}, 581 TicketID => $Param{TicketID}, 582 UserID => $Param{UserID}, 583 AutoResponseType => $Param{AutoResponseType}, 584 SenderTypeID => $Param{SenderTypeID}, 585 IsVisibleForCustomer => $Param{IsVisibleForCustomer}, 586 ); 587 } 588 else { 589 $Kernel::OM->Get('Kernel::System::Ticket::Article::Backend::Email')->SendAutoResponse( 590 OrigHeader => $Param{OrigHeader}, 591 TicketID => $Param{TicketID}, 592 UserID => $Param{UserID}, 593 AutoResponseType => $Param{AutoResponseType}, 594 SenderTypeID => $Param{SenderTypeID}, 595 IsVisibleForCustomer => $Param{IsVisibleForCustomer}, 596 ); 597 } 598 } 599 600 # send no agent notification!? 601 return $ArticleID if $Param{NoAgentNotify}; 602 603 my %Ticket = $TicketObject->TicketGet( 604 TicketID => $Param{TicketID}, 605 DynamicFields => 0, 606 ); 607 608 # remember agent to exclude notifications 609 my @SkipRecipients; 610 if ( $Param{ExcludeNotificationToUserID} && ref $Param{ExcludeNotificationToUserID} eq 'ARRAY' ) 611 { 612 for my $UserID ( @{ $Param{ExcludeNotificationToUserID} } ) { 613 push @SkipRecipients, $UserID; 614 } 615 } 616 617 # remember agent to exclude notifications / already sent 618 my %DoNotSendMute; 619 if ( 620 $Param{ExcludeMuteNotificationToUserID} 621 && ref $Param{ExcludeMuteNotificationToUserID} eq 'ARRAY' 622 ) 623 { 624 for my $UserID ( @{ $Param{ExcludeMuteNotificationToUserID} } ) { 625 push @SkipRecipients, $UserID; 626 } 627 } 628 629 my $ExtraRecipients; 630 if ( $Param{ForceNotificationToUserID} && ref $Param{ForceNotificationToUserID} eq 'ARRAY' ) { 631 $ExtraRecipients = $Param{ForceNotificationToUserID}; 632 } 633 634 # send agent notification on ticket create 635 if ( 636 $FirstArticle && 637 $Param{HistoryType} 638 =~ /^(EmailAgent|EmailCustomer|PhoneCallCustomer|WebRequestCustomer|SystemRequest)$/i 639 ) 640 { 641 # trigger notification event 642 $Self->EventHandler( 643 Event => 'NotificationNewTicket', 644 Data => { 645 TicketID => $Param{TicketID}, 646 ArticleID => $ArticleID, 647 SenderTypeID => $Param{SenderTypeID}, 648 IsVisibleForCustomer => $Param{IsVisibleForCustomer}, 649 Queue => $Param{Queue}, 650 Recipients => $ExtraRecipients, 651 SkipRecipients => \@SkipRecipients, 652 CustomerMessageParams => {%Param}, 653 }, 654 UserID => $Param{UserID}, 655 ); 656 } 657 658 # send agent notification on adding a note 659 elsif ( $Param{HistoryType} =~ /^AddNote$/i ) { 660 661 # trigger notification event 662 $Self->EventHandler( 663 Event => 'NotificationAddNote', 664 Data => { 665 TicketID => $Param{TicketID}, 666 ArticleID => $ArticleID, 667 SenderTypeID => $Param{SenderTypeID}, 668 IsVisibleForCustomer => $Param{IsVisibleForCustomer}, 669 Queue => $Param{Queue}, 670 Recipients => $ExtraRecipients, 671 SkipRecipients => \@SkipRecipients, 672 CustomerMessageParams => {}, 673 }, 674 UserID => $Param{UserID}, 675 ); 676 } 677 678 # send agent notification on follow up 679 elsif ( $Param{HistoryType} =~ /^FollowUp$/i ) { 680 681 # trigger notification event 682 $Self->EventHandler( 683 Event => 'NotificationFollowUp', 684 Data => { 685 TicketID => $Param{TicketID}, 686 ArticleID => $ArticleID, 687 SenderTypeID => $Param{SenderTypeID}, 688 IsVisibleForCustomer => $Param{IsVisibleForCustomer}, 689 Queue => $Param{Queue}, 690 Recipients => $ExtraRecipients, 691 SkipRecipients => \@SkipRecipients, 692 CustomerMessageParams => {%Param}, 693 }, 694 UserID => $Param{UserID}, 695 ); 696 } 697 698 # return ArticleID 699 return $ArticleID; 700} 701 702=head2 ArticleGet() 703 704Returns single article data. 705 706 my %Article = $ArticleBackendObject->ArticleGet( 707 TicketID => 123, # (required) 708 ArticleID => 123, # (required) 709 DynamicFields => 1, # (optional) To include the dynamic field values for this article on the return structure. 710 RealNames => 1, # (optional) To include the From/To/Cc/Bcc fields with real names. 711 ); 712 713Returns: 714 715 %Article = ( 716 TicketID => 123, 717 ArticleID => 123, 718 From => 'Some Agent <email@example.com>', 719 To => 'Some Customer A <customer-a@example.com>', 720 Cc => 'Some Customer B <customer-b@example.com>', 721 Bcc => 'Some Customer C <customer-c@example.com>', 722 ReplyTo => 'Some Customer B <customer-b@example.com>', 723 Subject => 'some short description', 724 MessageID => '<asdasdasd.123@example.com>', 725 InReplyTo => '<asdasdasd.12@example.com>', 726 References => '<asdasdasd.1@example.com> <asdasdasd.12@example.com>', 727 ContentType => 'text/plain; charset=ISO-8859-15', 728 Body => 'the message text', 729 SenderTypeID => 1, 730 SenderType => 'agent', 731 IsVisibleForCustomer => 1, 732 IncomingTime => 1490690026, 733 CreateBy => 1, 734 CreateTime => '2017-03-28 08:33:47', 735 Charset => 'ISO-8859-15', 736 MimeType => 'text/plain', 737 738 # If DynamicFields => 1 was passed, you'll get an entry like this for each dynamic field: 739 DynamicField_X => 'value_x', 740 741 # If RealNames => 1 was passed, you'll get fields with contact real names too: 742 FromRealname => 'Some Agent', 743 ToRealname => 'Some Customer A', 744 CcRealname => 'Some Customer B', 745 BccRealname => 'Some Customer C', 746 ); 747 748=cut 749 750sub ArticleGet { 751 my ( $Self, %Param ) = @_; 752 753 for my $Item (qw(TicketID ArticleID)) { 754 if ( !$Param{$Item} ) { 755 $Kernel::OM->Get('Kernel::System::Log')->Log( 756 Priority => 'error', 757 Message => "Need $Item!" 758 ); 759 return; 760 } 761 } 762 763 # Get meta article. 764 my %Article = $Self->_MetaArticleGet( 765 ArticleID => $Param{ArticleID}, 766 TicketID => $Param{TicketID}, 767 ); 768 return if !%Article; 769 770 my %ArticleSenderTypeList = $Kernel::OM->Get('Kernel::System::Ticket::Article')->ArticleSenderTypeList(); 771 772 # Email parser object might be used below for its field cleanup methods only. 773 my $EmailParser; 774 if ( $Param{RealNames} ) { 775 $EmailParser = Kernel::System::EmailParser->new( 776 Mode => 'Standalone', 777 ); 778 } 779 780 my $DBObject = $Kernel::OM->Get('Kernel::System::DB'); 781 782 my $SQL = ' 783 SELECT sadm.a_from, sadm.a_reply_to, sadm.a_to, sadm.a_cc, sadm.a_bcc, sadm.a_subject, 784 sadm.a_message_id, sadm.a_in_reply_to, sadm.a_references, sadm.a_content_type, 785 sadm.a_body, sadm.incoming_time 786 FROM article_data_mime sadm 787 WHERE sadm.article_id = ? 788 '; 789 my @Bind = ( \$Param{ArticleID} ); 790 791 return if !$DBObject->Prepare( 792 SQL => $SQL, 793 Bind => \@Bind, 794 Limit => 1, 795 ); 796 797 my %Data; 798 while ( my @Row = $DBObject->FetchrowArray() ) { 799 %Data = ( 800 %Article, 801 From => $Row[0], 802 ReplyTo => $Row[1], 803 To => $Row[2], 804 Cc => $Row[3], 805 Bcc => $Row[4], 806 Subject => $Row[5], 807 MessageID => $Row[6], 808 InReplyTo => $Row[7], 809 References => $Row[8], 810 ContentType => $Row[9], 811 Body => $Row[10], 812 IncomingTime => $Row[11], 813 SenderType => $ArticleSenderTypeList{ $Article{SenderTypeID} }, 814 ); 815 816 # Determine charset. 817 if ( $Data{ContentType} && $Data{ContentType} =~ /charset=/i ) { 818 $Data{Charset} = $Data{ContentType}; 819 $Data{Charset} =~ s/.+?charset=("|'|)(\w+)/$2/gi; 820 $Data{Charset} =~ s/"|'//g; 821 $Data{Charset} =~ s/(.+?);.*/$1/g; 822 } 823 else { 824 $Data{Charset} = ''; 825 } 826 $Data{ContentCharset} = $Data{Charset}; # compatibility 827 828 # Determine MIME type. 829 if ( $Data{ContentType} && $Data{ContentType} =~ /^(\w+\/\w+)/i ) { 830 $Data{MimeType} = $1; 831 $Data{MimeType} =~ s/"|'//g; 832 } 833 else { 834 $Data{MimeType} = ''; 835 } 836 837 RECIPIENT: 838 for my $Key (qw(From To Cc Bcc Subject)) { 839 next RECIPIENT if !$Data{$Key}; 840 841 # Strip unwanted stuff from some fields. 842 $Data{$Key} =~ s/\n|\r//g; 843 844 # Skip further processing for subject field. 845 next RECIPIENT if $Key eq 'Subject'; 846 847 if ( $Param{RealNames} ) { 848 849 # Check if it's a queue. 850 if ( $Data{$Key} !~ /@/ ) { 851 $Data{ $Key . 'Realname' } = $Data{$Key}; 852 next RECIPIENT; 853 } 854 855 # Strip out real names. 856 my $Realname = ''; 857 EMAILADDRESS: 858 for my $EmailSplit ( $EmailParser->SplitAddressLine( Line => $Data{$Key} ) ) { 859 my $Name = $EmailParser->GetRealname( Email => $EmailSplit ); 860 if ( !$Name ) { 861 $Name = $EmailParser->GetEmailAddress( Email => $EmailSplit ); 862 } 863 next EMAILADDRESS if !$Name; 864 if ($Realname) { 865 $Realname .= ', '; 866 } 867 $Realname .= $Name; 868 } 869 870 # Add real name lines. 871 $Data{ $Key . 'Realname' } = $Realname; 872 } 873 } 874 } 875 876 # Check if we also need to return dynamic field data. 877 if ( $Param{DynamicFields} ) { 878 my %DataWithDynamicFields = $Self->_MetaArticleDynamicFieldsGet( 879 Data => \%Data, 880 ); 881 %Data = %DataWithDynamicFields; 882 } 883 884 # Return if content is empty. 885 if ( !%Data ) { 886 $Kernel::OM->Get('Kernel::System::Log')->Log( 887 Priority => 'error', 888 Message => "No such article (TicketID=$Param{TicketID}, ArticleID=$Param{ArticleID})!", 889 ); 890 return; 891 } 892 893 return %Data; 894} 895 896=head2 ArticleUpdate() 897 898Update article data. 899 900Note: Keys C<Body>, C<Subject>, C<From>, C<To>, C<Cc>, C<Bcc>, C<ReplyTo>, C<SenderType>, C<SenderTypeID> 901and C<IsVisibleForCustomer> are implemented. 902 903 my $Success = $ArticleBackendObject->ArticleUpdate( 904 TicketID => 123, 905 ArticleID => 123, 906 Key => 'Body', 907 Value => 'New Body', 908 UserID => 123, 909 ); 910 911 my $Success = $ArticleBackendObject->ArticleUpdate( 912 TicketID => 123, 913 ArticleID => 123, 914 Key => 'SenderType', 915 Value => 'agent', 916 UserID => 123, 917 ); 918 919Events: 920 ArticleUpdate 921 922=cut 923 924sub ArticleUpdate { 925 my ( $Self, %Param ) = @_; 926 927 for my $Item (qw(TicketID ArticleID UserID Key)) { 928 if ( !$Param{$Item} ) { 929 $Kernel::OM->Get('Kernel::System::Log')->Log( 930 Priority => 'error', 931 Message => "Need $Item!", 932 ); 933 return; 934 } 935 } 936 937 if ( !defined $Param{Value} ) { 938 $Kernel::OM->Get('Kernel::System::Log')->Log( 939 Priority => 'error', 940 Message => 'Need Value!', 941 ); 942 return; 943 } 944 945 my $ArticleObject = $Kernel::OM->Get('Kernel::System::Ticket::Article'); 946 947 # Lookup for sender type ID. 948 if ( $Param{Key} eq 'SenderType' ) { 949 $Param{Key} = 'SenderTypeID'; 950 $Param{Value} = $ArticleObject->ArticleSenderTypeLookup( 951 SenderType => $Param{Value}, 952 ); 953 } 954 955 my %Map = ( 956 Body => 'a_body', 957 Subject => 'a_subject', 958 From => 'a_from', 959 To => 'a_to', 960 Cc => 'a_cc', 961 Bcc => 'a_bcc', 962 ReplyTo => 'a_reply_to', 963 ); 964 965 if ( $Map{ $Param{Key} } ) { 966 return if !$Kernel::OM->Get('Kernel::System::DB')->Do( 967 SQL => " 968 UPDATE article_data_mime 969 SET $Map{ $Param{Key} } = ?, change_time = current_timestamp, change_by = ? 970 WHERE article_id = ? 971 ", 972 Bind => [ \$Param{Value}, \$Param{UserID}, \$Param{ArticleID} ], 973 ); 974 } 975 else { 976 return if !$Self->_MetaArticleUpdate( 977 %Param, 978 ); 979 } 980 981 $ArticleObject->_ArticleCacheClear( 982 TicketID => $Param{TicketID}, 983 ); 984 985 $ArticleObject->ArticleSearchIndexBuild( 986 TicketID => $Param{TicketID}, 987 ArticleID => $Param{ArticleID}, 988 UserID => $Param{UserID}, 989 ); 990 991 $Self->EventHandler( 992 Event => 'ArticleUpdate', 993 Data => { 994 TicketID => $Param{TicketID}, 995 ArticleID => $Param{ArticleID}, 996 }, 997 UserID => $Param{UserID}, 998 ); 999 1000 return 1; 1001} 1002 1003=head2 ArticleDelete() 1004 1005Delete article data, its plain message, and all attachments. 1006 1007 my $Success = $ArticleBackendObject->ArticleDelete( 1008 TicketID => 123, 1009 ArticleID => 123, 1010 UserID => 123, 1011 ); 1012 1013=cut 1014 1015sub ArticleDelete { ## no critic; 1016 my ( $Self, %Param ) = @_; 1017 1018 for my $Needed (qw(ArticleID TicketID UserID)) { 1019 if ( !$Param{$Needed} ) { 1020 $Kernel::OM->Get('Kernel::System::Log')->Log( 1021 Priority => 'error', 1022 Message => "Need $Needed!", 1023 ); 1024 return; 1025 } 1026 } 1027 1028 # Delete from article storage. 1029 return if !$Kernel::OM->Get( $Self->{ArticleStorageModule} )->ArticleDelete(%Param); 1030 1031 my $DBObject = $Kernel::OM->Get('Kernel::System::DB'); 1032 1033 # Delete article data. 1034 return if !$DBObject->Do( 1035 SQL => 'DELETE FROM article_data_mime WHERE article_id = ?', 1036 Bind => [ \$Param{ArticleID} ], 1037 ); 1038 1039 # Delete related transmission error entries. 1040 return if !$DBObject->Do( 1041 SQL => 'DELETE FROM article_data_mime_send_error WHERE article_id = ?', 1042 Bind => [ \$Param{ArticleID} ], 1043 ); 1044 1045 # Delete meta article and associated data, and clear cache. 1046 return $Self->_MetaArticleDelete( 1047 %Param, 1048 ); 1049} 1050 1051=head1 STORAGE BACKEND DELEGATE METHODS 1052 1053=head2 ArticleWritePlain() 1054 1055Write a plain email to storage. This is a delegate method from active backend. 1056 1057 my $Success = $ArticleBackendObject->ArticleWritePlain( 1058 ArticleID => 123, 1059 Email => $EmailAsString, 1060 UserID => 123, 1061 ); 1062 1063=cut 1064 1065sub ArticleWritePlain { ## no critic; 1066 my $Self = shift; 1067 return $Kernel::OM->Get( $Self->{ArticleStorageModule} )->ArticleWritePlain(@_); 1068} 1069 1070=head2 ArticlePlain() 1071 1072Get plain article/email from storage. This is a delegate method from active backend. 1073 1074 my $PlainMessage = $ArticleBackendObject->ArticlePlain( 1075 ArticleID => 123, 1076 UserID => 123, 1077 ); 1078 1079Returns: 1080 1081 $PlainMessage = ' 1082 From: OTRS Feedback <marketing@otrs.com> 1083 To: Your OTRS System <otrs@localhost> 1084 Subject: Welcome to OTRS! 1085 Content-Type: text/plain; charset=utf-8 1086 Content-Transfer-Encoding: 8bit 1087 1088 Welcome to OTRS! 1089 ... 1090 '; 1091 1092=cut 1093 1094sub ArticlePlain { ## no critic; 1095 my $Self = shift; 1096 return $Kernel::OM->Get( $Self->{ArticleStorageModule} )->ArticlePlain(@_); 1097} 1098 1099=head2 ArticleDeletePlain() 1100 1101Delete a plain article from storage. This is a delegate method from active backend. 1102 1103 my $Success = $ArticleBackendObject->ArticleDeletePlain( 1104 ArticleID => 123, 1105 UserID => 123, 1106 ); 1107 1108=cut 1109 1110sub ArticleDeletePlain { ## no critic; 1111 my $Self = shift; 1112 return $Kernel::OM->Get( $Self->{ArticleStorageModule} )->ArticleDeletePlain(@_); 1113} 1114 1115=head2 ArticleWriteAttachment() 1116 1117Write an article attachment to storage. This is a delegate method from active backend. 1118 1119 my $Success = $ArticleBackendObject->ArticleWriteAttachment( 1120 Content => $ContentAsString, 1121 ContentType => 'text/html; charset="iso-8859-15"', 1122 Filename => 'lala.html', 1123 ContentID => 'cid-1234', # optional 1124 ContentAlternative => 0, # optional, alternative content to shown as body 1125 Disposition => 'attachment', # or 'inline' 1126 ArticleID => 123, 1127 UserID => 123, 1128 ); 1129 1130=cut 1131 1132sub ArticleWriteAttachment { ## no critic; 1133 my $Self = shift; 1134 return $Kernel::OM->Get( $Self->{ArticleStorageModule} )->ArticleWriteAttachment(@_); 1135} 1136 1137=head2 ArticleAttachment() 1138 1139Get article attachment from storage. This is a delegate method from active backend. 1140 1141 my %Attachment = $ArticleBackendObject->ArticleAttachment( 1142 ArticleID => 123, 1143 FileID => 1, # as returned by ArticleAttachmentIndex 1144 ); 1145 1146Returns: 1147 1148 %Attachment = ( 1149 Content => 'xxxx', # actual attachment contents 1150 ContentAlternative => '', 1151 ContentID => '', 1152 ContentType => 'application/pdf', 1153 Filename => 'StdAttachment-Test1.pdf', 1154 FilesizeRaw => 4722, 1155 Disposition => 'attachment', 1156 ); 1157 1158=cut 1159 1160sub ArticleAttachment { ## no critic; 1161 my $Self = shift; 1162 return $Kernel::OM->Get( $Self->{ArticleStorageModule} )->ArticleAttachment(@_); 1163} 1164 1165=head2 ArticleDeleteAttachment() 1166 1167Delete all attachments of an article from storage. This is a delegate method from active backend. 1168 1169 my $Success = $ArticleBackendObject->ArticleDeleteAttachment( 1170 ArticleID => 123, 1171 UserID => 123, 1172 ); 1173 1174=cut 1175 1176sub ArticleDeleteAttachment { ## no critic; 1177 my $Self = shift; 1178 return $Kernel::OM->Get( $Self->{ArticleStorageModule} )->ArticleDeleteAttachment(@_); 1179} 1180 1181=head2 ArticleAttachmentIndex() 1182 1183Get article attachment index as hash. 1184 1185 my %Index = $ArticleBackendObject->ArticleAttachmentIndex( 1186 ArticleID => 123, 1187 ExcludePlainText => 1, # (optional) Exclude plain text attachment 1188 ExcludeHTMLBody => 1, # (optional) Exclude HTML body attachment 1189 ExcludeInline => 1, # (optional) Exclude inline attachments 1190 ); 1191 1192Returns: 1193 1194 my %Index = { 1195 '1' => { # Attachment ID 1196 ContentAlternative => '', # (optional) 1197 ContentID => '', # (optional) 1198 Filesize => '4.6 KB', 1199 ContentType => 'application/pdf', 1200 FilesizeRaw => 4722, 1201 Disposition => 'attachment', 1202 }, 1203 '2' => { 1204 ContentAlternative => '', 1205 ContentID => '', 1206 Filesize => '183 B', 1207 ContentType => 'text/html; charset="utf-8"', 1208 FilesizeRaw => 183, 1209 Disposition => 'attachment', 1210 }, 1211 ... 1212 }; 1213 1214=cut 1215 1216sub ArticleAttachmentIndex { ## no critic 1217 my $Self = shift; 1218 return $Kernel::OM->Get( $Self->{ArticleStorageModule} )->ArticleAttachmentIndex(@_); 1219} 1220 1221=head2 BackendSearchableFieldsGet() 1222 1223Get the definition of the searchable fields as a hash. 1224 1225 my %SearchableFields = $ArticleBackendObject->BackendSearchableFieldsGet(); 1226 1227Returns: 1228 1229 my %SearchableFields = ( 1230 'MIMEBase_From' => { 1231 Label => 'From', 1232 Key => 'MIMEBase_From', 1233 Type => 'Text', 1234 Filterable => 0, 1235 }, 1236 'MIMEBase_To' => { 1237 Label => 'To', 1238 Key => 'MIMEBase_To', 1239 Type => 'Text', 1240 Filterable => 0, 1241 }, 1242 'MIMEBase_Cc' => { 1243 Label => 'Cc', 1244 Key => 'MIMEBase_Cc', 1245 Type => 'Text', 1246 Filterable => 0, 1247 }, 1248 'MIMEBase_Bcc' => { 1249 Label => 'Bcc', 1250 Key => 'MIMEBase_Bcc', 1251 Type => 'Text', 1252 Filterable => 0, 1253 }, 1254 'MIMEBase_Subject' => { 1255 Label => 'Subject', 1256 Key => 'MIMEBase_Subject', 1257 Type => 'Text', 1258 Filterable => 1, 1259 }, 1260 'MIMEBase_Body' => { 1261 Label => 'Body', 1262 Key => 'MIMEBase_Body', 1263 Type => 'Text', 1264 Filterable => 1, 1265 }, 1266 'MIMEBase_AttachmentName' => { 1267 Label => 'Attachment Name', 1268 Key => 'MIMEBase_AttachmentName', 1269 Type => 'Text', 1270 Filterable => 0, 1271 }, 1272 ); 1273 1274=cut 1275 1276sub BackendSearchableFieldsGet { 1277 my ( $Self, %Param ) = @_; 1278 1279 my %SearchableFields = ( 1280 'MIMEBase_From' => { 1281 Label => 'From', 1282 Key => 'MIMEBase_From', 1283 Type => 'Text', 1284 Filterable => 0, 1285 }, 1286 'MIMEBase_To' => { 1287 Label => 'To', 1288 Key => 'MIMEBase_To', 1289 Type => 'Text', 1290 Filterable => 0, 1291 }, 1292 'MIMEBase_Cc' => { 1293 Label => 'Cc', 1294 Key => 'MIMEBase_Cc', 1295 Type => 'Text', 1296 Filterable => 0, 1297 }, 1298 'MIMEBase_Bcc' => { 1299 Label => 'Bcc', 1300 Key => 'MIMEBase_Bcc', 1301 Type => 'Text', 1302 Filterable => 0, 1303 HideInCustomerInterface => 1, 1304 }, 1305 'MIMEBase_Subject' => { 1306 Label => 'Subject', 1307 Key => 'MIMEBase_Subject', 1308 Type => 'Text', 1309 Filterable => 1, 1310 }, 1311 'MIMEBase_Body' => { 1312 Label => 'Body', 1313 Key => 'MIMEBase_Body', 1314 Type => 'Text', 1315 Filterable => 1, 1316 }, 1317 ); 1318 1319 if ( $Kernel::OM->Get('Kernel::Config')->Get('Ticket::Article::Backend::MIMEBase::IndexAttachmentNames') ) { 1320 $SearchableFields{'MIMEBase_AttachmentName'} = { 1321 Label => 'Attachment Name', 1322 Key => 'MIMEBase_AttachmentName', 1323 Type => 'Text', 1324 Filterable => 0, 1325 }; 1326 } 1327 1328 return %SearchableFields; 1329} 1330 1331=head2 ArticleSearchableContentGet() 1332 1333Get article attachment index as hash. 1334 1335 my %Index = $ArticleBackendObject->ArticleSearchableContentGet( 1336 TicketID => 123, # (required) 1337 ArticleID => 123, # (required) 1338 DynamicFields => 1, # (optional) To include the dynamic field values for this article on the return structure. 1339 RealNames => 1, # (optional) To include the From/To/Cc/Bcc fields with real names. 1340 UserID => 123, # (required) 1341 ); 1342 1343Returns: 1344 1345 my %ArticleSearchData = { 1346 'From' => { 1347 String => 'Test User1 <testuser1@example.com>', 1348 Key => 'From', 1349 Type => 'Text', 1350 Filterable => 0, 1351 }, 1352 'To' => { 1353 String => 'Test User2 <testuser2@example.com>', 1354 Key => 'To', 1355 Type => 'Text', 1356 Filterable => 0, 1357 }, 1358 'Cc' => { 1359 String => 'Test User3 <testuser3@example.com>', 1360 Key => 'Cc', 1361 Type => 'Text', 1362 Filterable => 0, 1363 }, 1364 'Bcc' => { 1365 String => 'Test User4 <testuser4@example.com>', 1366 Key => 'Bcc', 1367 Type => 'Text', 1368 Filterable => 0, 1369 }, 1370 'Subject' => { 1371 String => 'This is a test subject!', 1372 Key => 'Subject', 1373 Type => 'Text', 1374 Filterable => 1, 1375 }, 1376 'Body' => { 1377 String => 'This is a body text!', 1378 Key => 'Body', 1379 Type => 'Text', 1380 Filterable => 1, 1381 } 1382 }; 1383 1384=cut 1385 1386sub ArticleSearchableContentGet { 1387 my ( $Self, %Param ) = @_; 1388 1389 for my $Item (qw(TicketID ArticleID UserID)) { 1390 if ( !$Param{$Item} ) { 1391 $Kernel::OM->Get('Kernel::System::Log')->Log( 1392 Priority => 'error', 1393 Message => "Need $Item!" 1394 ); 1395 return; 1396 } 1397 } 1398 1399 my %DataKeyMap = ( 1400 'MIMEBase_From' => 'From', 1401 'MIMEBase_To' => 'To', 1402 'MIMEBase_Cc' => 'Cc', 1403 'MIMEBase_Bcc' => 'Bcc', 1404 'MIMEBase_Subject' => 'Subject', 1405 'MIMEBase_Body' => 'Body', 1406 ); 1407 1408 my %ArticleData = $Self->ArticleGet( 1409 TicketID => $Param{TicketID}, 1410 ArticleID => $Param{ArticleID}, 1411 UserID => $Param{UserID}, 1412 DynamicFields => 0, 1413 ); 1414 1415 my %BackendSearchableFields = $Self->BackendSearchableFieldsGet(); 1416 1417 my %ArticleSearchData; 1418 1419 FIELDKEY: 1420 for my $FieldKey ( sort keys %BackendSearchableFields ) { 1421 1422 my $IndexString; 1423 1424 # scan available attachment names and append the information 1425 if ( $FieldKey eq 'MIMEBase_AttachmentName' ) { 1426 1427 my %AttachmentIndex = $Self->ArticleAttachmentIndex( 1428 ArticleID => $Param{ArticleID}, 1429 UserID => $Param{UserID}, 1430 ExcludePlainText => 1, 1431 ExcludeHTMLBody => 1, 1432 ExcludeInline => 1, 1433 ); 1434 1435 next FIELDKEY if !%AttachmentIndex; 1436 1437 my @AttachmentNames; 1438 1439 for my $AttachmentKey ( sort keys %AttachmentIndex ) { 1440 push @AttachmentNames, $AttachmentIndex{$AttachmentKey}->{Filename}; 1441 } 1442 1443 $IndexString = join ' ', @AttachmentNames; 1444 } 1445 1446 $IndexString //= $ArticleData{ $DataKeyMap{$FieldKey} }; 1447 1448 next FIELDKEY if !IsStringWithData($IndexString); 1449 1450 $ArticleSearchData{$FieldKey} = { 1451 String => $IndexString, 1452 Key => $BackendSearchableFields{$FieldKey}->{Key}, 1453 Type => $BackendSearchableFields{$FieldKey}->{Type} // 'Text', 1454 Filterable => $BackendSearchableFields{$FieldKey}->{Filterable} // 0, 1455 }; 1456 } 1457 1458 return %ArticleSearchData; 1459} 1460 1461=head2 ArticleHasHTMLContent() 1462 1463Returns 1 if article has HTML content. 1464 1465 my $ArticleHasHTMLContent = $ArticleBackendObject->ArticleHasHTMLContent( 1466 TicketID => 1, 1467 ArticleID => 2, 1468 UserID => 1, 1469 ); 1470 1471Result: 1472 1473 $ArticleHasHTMLContent = 1; # or 0 1474 1475=cut 1476 1477sub ArticleHasHTMLContent { 1478 my ( $Self, %Param ) = @_; 1479 1480 # Check needed stuff. 1481 for my $Needed (qw(TicketID ArticleID UserID)) { 1482 if ( !$Param{$Needed} ) { 1483 $Kernel::OM->Get('Kernel::System::Log')->Log( 1484 Priority => 'error', 1485 Message => "Need $Needed!", 1486 ); 1487 return; 1488 } 1489 } 1490 1491 # Check if there is HTML body attachment. 1492 my %AttachmentIndexHTMLBody = $Self->ArticleAttachmentIndex( 1493 %Param, 1494 OnlyHTMLBody => 1, 1495 ); 1496 1497 my ($HTMLBodyAttachmentID) = sort keys %AttachmentIndexHTMLBody; 1498 1499 return $HTMLBodyAttachmentID ? 1 : 0; 1500} 1501 15021; 1503 1504=head1 TERMS AND CONDITIONS 1505 1506This software is part of the OTRS project (L<https://otrs.org/>). 1507 1508This software comes with ABSOLUTELY NO WARRANTY. For details, see 1509the enclosed file COPYING for license information (GPL). If you 1510did not receive this file, see L<https://www.gnu.org/licenses/gpl-3.0.txt>. 1511 1512=cut 1513