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