# -- # Copyright (C) 2001-2019 OTRS AG, https://otrs.com/ # -- # This software comes with ABSOLUTELY NO WARRANTY. For details, see # the enclosed file COPYING for license information (GPL). If you # did not receive this file, see https://www.gnu.org/licenses/gpl-3.0.txt. # -- package Kernel::System::Ticket::Article::Backend::MIMEBase; use strict; use warnings; use parent 'Kernel::System::Ticket::Article::Backend::Base'; use Kernel::System::EmailParser; use Kernel::System::VariableCheck qw(:all); our @ObjectDependencies = ( 'Kernel::Config', 'Kernel::System::Cache', 'Kernel::System::DB', 'Kernel::System::Email', 'Kernel::System::HTMLUtils', 'Kernel::System::Log', 'Kernel::System::Main', 'Kernel::System::Ticket', 'Kernel::System::Ticket::Article', 'Kernel::System::Ticket::Article::Backend::Email', 'Kernel::System::User', ); =head1 NAME Kernel::System::Ticket::Article::Backend::MIMEBase - base class for all C based article backends =head1 DESCRIPTION This is a base class for article data in C format and should not be instantiated directly. Always get real backend instead, i.e. C, C or C. Basic article data is always stored in a database table, but extended data uses configured article storage backend. For plain text representation of the message, use C field. For original message with email headers, use L method to retrieve it from storage backend. Attachments are handled by the storage backends, and can be retrieved via L. Inherits from L. See also L and L. =head1 PUBLIC INTERFACE =head2 new() Don't instantiate this class directly, get instances of the real backends instead: my $ArticleBackendObject = $Kernel::OM->Get('Kernel::System::Ticket::Article')->BackendForChannel(ChannelName => 'Email'); =cut sub new { my ( $Type, %Param ) = @_; # Call constructor of the base class. my $Self = $Type->SUPER::new(%Param); # 0=off; 1=on; $Self->{Debug} = $Param{Debug} || 0; # Persistent for this object's lifetime so that we can have article objects with different storage modules. $Self->{ArticleStorageModule} = $Param{ArticleStorageModule} || $Kernel::OM->Get('Kernel::Config')->Get('Ticket::Article::Backend::MIMEBase::ArticleStorage') || 'Kernel::System::Ticket::Article::Backend::MIMEBase::ArticleStorageDB'; return $Self; } =head2 ArticleCreate() Create a MIME article. my $ArticleID = $ArticleBackendObject->ArticleCreate( TicketID => 123, # (required) SenderTypeID => 1, # (required) # or SenderType => 'agent', # (required) agent|system|customer IsVisibleForCustomer => 1, # (required) Is article visible for customer? UserID => 123, # (required) From => 'Some Agent ', # not required but useful To => 'Some Customer A ', # not required but useful Cc => 'Some Customer B ', # not required but useful Bcc => 'Some Customer C ', # not required but useful ReplyTo => 'Some Customer B ', # not required Subject => 'some short description', # not required but useful Body => 'the message text', # not required but useful MessageID => '', # not required but useful InReplyTo => '', # not required but useful References => ' ', # not required but useful ContentType => 'text/plain; charset=ISO-8859-15', # or optional Charset & MimeType HistoryType => 'OwnerUpdate', # EmailCustomer|Move|AddNote|PriorityUpdate|WebRequestCustomer|... HistoryComment => 'Some free text!', Attachment => [ { Content => $Content, ContentType => $ContentType, Filename => 'lala.txt', }, { Content => $Content, ContentType => $ContentType, Filename => 'lala1.txt', }, ], NoAgentNotify => 0, # if you don't want to send agent notifications AutoResponseType => 'auto reply' # auto reject|auto follow up|auto reply/new ticket|auto remove ForceNotificationToUserID => [ 1, 43, 56 ], # if you want to force somebody ExcludeNotificationToUserID => [ 43,56 ], # if you want full exclude somebody from notfications, # will also be removed in To: line of article, # higher prio as ForceNotificationToUserID ExcludeMuteNotificationToUserID => [ 43,56 ], # the same as ExcludeNotificationToUserID but only the # sending gets muted, agent will still shown in To: # line of article ); Example with "Charset & MimeType" and no "ContentType". my $ArticleID = $ArticleBackendObject->ArticleCreate( TicketID => 123, # (required) SenderType => 'agent', # (required) agent|system|customer IsVisibleForCustomer => 1, # (required) Is article visible for customer? From => 'Some Agent ', # not required but useful To => 'Some Customer A ', # not required but useful Subject => 'some short description', # required Body => 'the message text', # required Charset => 'ISO-8859-15', MimeType => 'text/plain', HistoryType => 'OwnerUpdate', # EmailCustomer|Move|AddNote|PriorityUpdate|WebRequestCustomer|... HistoryComment => 'Some free text!', UserID => 123, UnlockOnAway => 1, # Unlock ticket if owner is away ); Events: ArticleCreate =cut sub ArticleCreate { my ( $Self, %Param ) = @_; my $IncomingTime = $Kernel::OM->Create('Kernel::System::DateTime')->ToEpoch(); my $ArticleContentPath = $Kernel::OM->Get( $Self->{ArticleStorageModule} )->BuildArticleContentPath(); # create ArticleContentPath if ( !$ArticleContentPath ) { $Kernel::OM->Get('Kernel::System::Log')->Log( Priority => 'error', Message => 'Need ArticleContentPath!' ); return; } my $ArticleObject = $Kernel::OM->Get('Kernel::System::Ticket::Article'); # Lookup if no ID is passed. if ( $Param{SenderType} && !$Param{SenderTypeID} ) { $Param{SenderTypeID} = $ArticleObject->ArticleSenderTypeLookup( SenderType => $Param{SenderType} ); } # check needed stuff for my $Needed (qw(TicketID UserID SenderTypeID HistoryType HistoryComment)) { if ( !$Param{$Needed} ) { $Kernel::OM->Get('Kernel::System::Log')->Log( Priority => 'error', Message => "Need $Needed!" ); return; } } if ( !defined $Param{IsVisibleForCustomer} ) { $Kernel::OM->Get('Kernel::System::Log')->Log( Priority => 'error', Message => "Need IsVisibleForCustomer!" ); return; } # check ContentType vs. Charset & MimeType if ( !$Param{ContentType} ) { for my $Item (qw(Charset MimeType)) { if ( !$Param{$Item} ) { $Kernel::OM->Get('Kernel::System::Log')->Log( Priority => 'error', Message => "Need $Item!" ); return; } } $Param{ContentType} = "$Param{MimeType}; charset=$Param{Charset}"; } else { for my $Item (qw(ContentType)) { if ( !$Param{$Item} ) { $Kernel::OM->Get('Kernel::System::Log')->Log( Priority => 'error', Message => "Need $Item!" ); return; } } $Param{Charset} = ''; if ( $Param{ContentType} =~ /charset=/i ) { $Param{Charset} = $Param{ContentType}; $Param{Charset} =~ s/.+?charset=("|'|)(\w+)/$2/gi; $Param{Charset} =~ s/"|'//g; $Param{Charset} =~ s/(.+?);.*/$1/g; } $Param{MimeType} = ''; if ( $Param{ContentType} =~ /^(\w+\/\w+)/i ) { $Param{MimeType} = $1; $Param{MimeType} =~ s/"|'//g; } } my $TicketObject = $Kernel::OM->Get('Kernel::System::Ticket'); # for the event handler, before any actions have taken place my %OldTicketData = $TicketObject->TicketGet( TicketID => $Param{TicketID}, DynamicFields => 1, ); # get html utils object my $HTMLUtilsObject = $Kernel::OM->Get('Kernel::System::HTMLUtils'); # add 'no body' if there is no body there! my @AttachmentConvert; if ( !defined $Param{Body} ) { # allow '0' as body $Param{Body} = ''; } # process html article elsif ( $Param{MimeType} =~ /text\/html/i ) { # add html article as attachment my $Attach = { Content => $Param{Body}, ContentType => "text/html; charset=\"$Param{Charset}\"", Filename => 'file-2', }; push @AttachmentConvert, $Attach; # get ASCII body $Param{MimeType} = 'text/plain'; $Param{ContentType} =~ s/html/plain/i; $Param{Body} = $HTMLUtilsObject->ToAscii( String => $Param{Body}, ); } elsif ( $Param{MimeType} && $Param{MimeType} eq "application/json" ) { # Keep JSON body unchanged } # if body isn't text, attach body as attachment (mostly done by OE) :-/ elsif ( $Param{MimeType} && $Param{MimeType} !~ /\btext\b/i ) { # Add non-text as an attachment. Try to deduce the filename from ContentType or ContentDisposition headers. # Please see bug#13644 for more information. my $FileName = 'unknown'; if ( $Param{ContentType} =~ /name="(.+?)"/i || ( defined $Param{ContentDisposition} && $Param{ContentDisposition} =~ /name="(.+?)"/i ) ) { $FileName = $1; } my $Attach = { Content => $Param{Body}, ContentType => $Param{ContentType}, Filename => $FileName, }; push @{ $Param{Attachment} }, $Attach; # set ASCII body $Param{MimeType} = 'text/plain'; $Param{ContentType} = 'text/plain'; $Param{Body} = '- no text message => see attachment -'; $Param{OrigHeader}->{Body} = $Param{Body}; } # fix some bad stuff from some browsers (Opera)! else { $Param{Body} =~ s/(\n\r|\r\r\n|\r\n)/\n/g; } # strip not wanted stuff for my $Attribute (qw(From To Cc Bcc Subject MessageID InReplyTo References ReplyTo)) { if ( defined $Param{$Attribute} ) { $Param{$Attribute} =~ s/\n|\r//g; } else { $Param{$Attribute} = ''; } } ATTRIBUTE: for my $Attribute (qw(MessageID)) { next ATTRIBUTE if !$Param{$Attribute}; $Param{$Attribute} = substr( $Param{$Attribute}, 0, 3800 ); } # Check if this is the first article (for notifications). my @Articles = $ArticleObject->ArticleList( TicketID => $Param{TicketID} ); my $FirstArticle = scalar @Articles ? 0 : 1; my $MainObject = $Kernel::OM->Get('Kernel::System::Main'); # calculate MD5 of Message ID if ( $Param{MessageID} ) { $Param{MD5} = $MainObject->MD5sum( String => $Param{MessageID} ); } # Generate unique fingerprint for searching created article in database to prevent race conditions # (see https://bugs.otrs.org/show_bug.cgi?id=12438). my $RandomString = $MainObject->GenerateRandomString( Length => 32, ); my $ArticleInsertFingerprint = $$ . '-' . $RandomString . '-' . ( $Param{MessageID} // '' ); # get database object my $DBObject = $Kernel::OM->Get('Kernel::System::DB'); # Create meta article. my $ArticleID = $Self->_MetaArticleCreate( TicketID => $Param{TicketID}, SenderTypeID => $Param{SenderTypeID}, IsVisibleForCustomer => $Param{IsVisibleForCustomer}, CommunicationChannelID => $Self->ChannelIDGet(), UserID => $Param{UserID}, ); if ( !$ArticleID ) { $Kernel::OM->Get('Kernel::System::Log')->Log( Priority => 'error', Message => "Can't create meta article (TicketID=$Param{TicketID})!", ); return; } my $UserObject = $Kernel::OM->Get('Kernel::System::User'); # Check if there are additional To's from InvolvedAgent and InformAgent. # See bug#13422 (https://bugs.otrs.org/show_bug.cgi?id=13422). if ( $Param{ForceNotificationToUserID} && ref $Param{ForceNotificationToUserID} eq 'ARRAY' ) { my $NewTo = ''; USER: for my $UserID ( @{ $Param{ForceNotificationToUserID} } ) { next USER if $UserID == 1; next USER if grep { $UserID eq $_ } @{ $Param{ExcludeNotificationToUserID} }; my %UserData = $UserObject->GetUserData( UserID => $UserID, Valid => 1, ); next USER if !%UserData; if ( $Param{To} || $NewTo ) { $NewTo .= ', '; } $NewTo .= "\"$UserData{UserFirstname} $UserData{UserLastname}\" <$UserData{UserEmail}>"; } $Param{To} .= $NewTo; } return if !$DBObject->Do( SQL => ' INSERT INTO article_data_mime ( article_id, a_from, a_reply_to, a_to, a_cc, a_bcc, a_subject, a_message_id, a_message_id_md5, a_in_reply_to, a_references, a_content_type, a_body, incoming_time, content_path, create_time, create_by, change_time, change_by) VALUES ( ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, current_timestamp, ?, current_timestamp, ? ) ', Bind => [ \$ArticleID, \$Param{From}, \$Param{ReplyTo}, \$Param{To}, \$Param{Cc}, \$Param{Bcc}, \$Param{Subject}, \$ArticleInsertFingerprint, # just for next search; will be updated with correct MessageID \$Param{MD5}, \$Param{InReplyTo}, \$Param{References}, \$Param{ContentType}, \$Param{Body}, \$IncomingTime, \$ArticleContentPath, \$Param{UserID}, \$Param{UserID}, ], ); # Get article data ID. return if !$DBObject->Prepare( SQL => ' SELECT id FROM article_data_mime WHERE article_id = ? AND a_message_id = ? AND incoming_time = ? ORDER BY id DESC ', Bind => [ \$ArticleID, \$ArticleInsertFingerprint, \$IncomingTime, ], Limit => 1, ); my $ArticleDataID; while ( my @Row = $DBObject->FetchrowArray() ) { $ArticleDataID = $Row[0]; } # Return if article data record was not created. if ( !$ArticleDataID ) { $Kernel::OM->Get('Kernel::System::Log')->Log( Priority => 'error', Message => "Can't store article data (TicketID=$Param{TicketID}, ArticleID=$ArticleID, MessageID=$Param{MessageID})!", ); return; } # Save correct Message-ID now. return if !$DBObject->Do( SQL => 'UPDATE article_data_mime SET a_message_id = ? WHERE id = ?', Bind => [ \$Param{MessageID}, \$ArticleDataID, ], ); # check for base64 encoded images in html body and upload them for my $Attachment (@AttachmentConvert) { if ( $Attachment->{ContentType} eq "text/html; charset=\"$Param{Charset}\"" && $Attachment->{Filename} eq 'file-2' ) { $HTMLUtilsObject->EmbeddedImagesExtract( DocumentRef => \$Attachment->{Content}, AttachmentsRef => \@AttachmentConvert, ); } } # add converted attachments for my $Attachment (@AttachmentConvert) { $Kernel::OM->Get( $Self->{ArticleStorageModule} )->ArticleWriteAttachment( %{$Attachment}, ArticleID => $ArticleID, UserID => $Param{UserID}, ); } # add attachments if ( $Param{Attachment} ) { for my $Attachment ( @{ $Param{Attachment} } ) { $Kernel::OM->Get( $Self->{ArticleStorageModule} )->ArticleWriteAttachment( %{$Attachment}, ArticleID => $ArticleID, UserID => $Param{UserID}, ); } } $ArticleObject->_ArticleCacheClear( TicketID => $Param{TicketID}, ); # add history row $TicketObject->HistoryAdd( ArticleID => $ArticleID, TicketID => $Param{TicketID}, CreateUserID => $Param{UserID}, HistoryType => $Param{HistoryType}, Name => $Param{HistoryComment}, ); my $ConfigObject = $Kernel::OM->Get('Kernel::Config'); # unlock ticket if the owner is away (and the feature is enabled) if ( $Param{UnlockOnAway} && $OldTicketData{Lock} eq 'lock' && $ConfigObject->Get('Ticket::UnlockOnAway') ) { my %OwnerInfo = $UserObject->GetUserData( UserID => $OldTicketData{OwnerID}, ); if ( $OwnerInfo{OutOfOfficeMessage} ) { $TicketObject->TicketLockSet( TicketID => $Param{TicketID}, Lock => 'unlock', UserID => $Param{UserID}, ); $Kernel::OM->Get('Kernel::System::Log')->Log( Priority => 'notice', Message => "Ticket [$OldTicketData{TicketNumber}] unlocked, current owner is out of office!", ); } } $ArticleObject->ArticleSearchIndexBuild( TicketID => $Param{TicketID}, ArticleID => $ArticleID, UserID => 1, ); # event $Self->EventHandler( Event => 'ArticleCreate', Data => { ArticleID => $ArticleID, TicketID => $Param{TicketID}, OldTicketData => \%OldTicketData, }, UserID => $Param{UserID}, ); # reset unlock if needed if ( !$Param{SenderType} ) { $Param{SenderType} = $ArticleObject->ArticleSenderTypeLookup( SenderTypeID => $Param{SenderTypeID} ); } # reset unlock time if customer sent an update if ( $Param{SenderType} eq 'customer' ) { # Check if previous sender was an agent. my $AgentSenderTypeID = $ArticleObject->ArticleSenderTypeLookup( SenderType => 'agent' ); my $SystemSenderTypeID = $ArticleObject->ArticleSenderTypeLookup( SenderType => 'system' ); my @Articles = $ArticleObject->ArticleList( TicketID => $Param{TicketID}, ); my $LastSenderTypeID; ARTICLE: for my $Article ( reverse @Articles ) { next ARTICLE if $Article->{ArticleID} eq $ArticleID; next ARTICLE if $Article->{SenderTypeID} eq $SystemSenderTypeID; $LastSenderTypeID = $Article->{SenderTypeID}; last ARTICLE; } if ( $LastSenderTypeID && $LastSenderTypeID == $AgentSenderTypeID ) { $TicketObject->TicketUnlockTimeoutUpdate( UnlockTimeout => $IncomingTime, TicketID => $Param{TicketID}, UserID => $Param{UserID}, ); } } # check if latest article is sent to customer elsif ( $Param{SenderType} eq 'agent' ) { $TicketObject->TicketUnlockTimeoutUpdate( UnlockTimeout => $IncomingTime, TicketID => $Param{TicketID}, UserID => $Param{UserID}, ); } # send auto response if ( $Param{AutoResponseType} ) { # Check if SendAutoResponse() method exists, before calling it. If it doesn't, get an instance of an additional # email backend first. if ( $Self->can('SendAutoResponse') ) { $Self->SendAutoResponse( OrigHeader => $Param{OrigHeader}, TicketID => $Param{TicketID}, UserID => $Param{UserID}, AutoResponseType => $Param{AutoResponseType}, SenderTypeID => $Param{SenderTypeID}, IsVisibleForCustomer => $Param{IsVisibleForCustomer}, ); } else { $Kernel::OM->Get('Kernel::System::Ticket::Article::Backend::Email')->SendAutoResponse( OrigHeader => $Param{OrigHeader}, TicketID => $Param{TicketID}, UserID => $Param{UserID}, AutoResponseType => $Param{AutoResponseType}, SenderTypeID => $Param{SenderTypeID}, IsVisibleForCustomer => $Param{IsVisibleForCustomer}, ); } } # send no agent notification!? return $ArticleID if $Param{NoAgentNotify}; my %Ticket = $TicketObject->TicketGet( TicketID => $Param{TicketID}, DynamicFields => 0, ); # remember agent to exclude notifications my @SkipRecipients; if ( $Param{ExcludeNotificationToUserID} && ref $Param{ExcludeNotificationToUserID} eq 'ARRAY' ) { for my $UserID ( @{ $Param{ExcludeNotificationToUserID} } ) { push @SkipRecipients, $UserID; } } # remember agent to exclude notifications / already sent my %DoNotSendMute; if ( $Param{ExcludeMuteNotificationToUserID} && ref $Param{ExcludeMuteNotificationToUserID} eq 'ARRAY' ) { for my $UserID ( @{ $Param{ExcludeMuteNotificationToUserID} } ) { push @SkipRecipients, $UserID; } } my $ExtraRecipients; if ( $Param{ForceNotificationToUserID} && ref $Param{ForceNotificationToUserID} eq 'ARRAY' ) { $ExtraRecipients = $Param{ForceNotificationToUserID}; } # send agent notification on ticket create if ( $FirstArticle && $Param{HistoryType} =~ /^(EmailAgent|EmailCustomer|PhoneCallCustomer|WebRequestCustomer|SystemRequest)$/i ) { # trigger notification event $Self->EventHandler( Event => 'NotificationNewTicket', Data => { TicketID => $Param{TicketID}, ArticleID => $ArticleID, SenderTypeID => $Param{SenderTypeID}, IsVisibleForCustomer => $Param{IsVisibleForCustomer}, Queue => $Param{Queue}, Recipients => $ExtraRecipients, SkipRecipients => \@SkipRecipients, CustomerMessageParams => {%Param}, }, UserID => $Param{UserID}, ); } # send agent notification on adding a note elsif ( $Param{HistoryType} =~ /^AddNote$/i ) { # trigger notification event $Self->EventHandler( Event => 'NotificationAddNote', Data => { TicketID => $Param{TicketID}, ArticleID => $ArticleID, SenderTypeID => $Param{SenderTypeID}, IsVisibleForCustomer => $Param{IsVisibleForCustomer}, Queue => $Param{Queue}, Recipients => $ExtraRecipients, SkipRecipients => \@SkipRecipients, CustomerMessageParams => {}, }, UserID => $Param{UserID}, ); } # send agent notification on follow up elsif ( $Param{HistoryType} =~ /^FollowUp$/i ) { # trigger notification event $Self->EventHandler( Event => 'NotificationFollowUp', Data => { TicketID => $Param{TicketID}, ArticleID => $ArticleID, SenderTypeID => $Param{SenderTypeID}, IsVisibleForCustomer => $Param{IsVisibleForCustomer}, Queue => $Param{Queue}, Recipients => $ExtraRecipients, SkipRecipients => \@SkipRecipients, CustomerMessageParams => {%Param}, }, UserID => $Param{UserID}, ); } # return ArticleID return $ArticleID; } =head2 ArticleGet() Returns single article data. my %Article = $ArticleBackendObject->ArticleGet( TicketID => 123, # (required) ArticleID => 123, # (required) DynamicFields => 1, # (optional) To include the dynamic field values for this article on the return structure. RealNames => 1, # (optional) To include the From/To/Cc/Bcc fields with real names. ); Returns: %Article = ( TicketID => 123, ArticleID => 123, From => 'Some Agent ', To => 'Some Customer A ', Cc => 'Some Customer B ', Bcc => 'Some Customer C ', ReplyTo => 'Some Customer B ', Subject => 'some short description', MessageID => '', InReplyTo => '', References => ' ', ContentType => 'text/plain; charset=ISO-8859-15', Body => 'the message text', SenderTypeID => 1, SenderType => 'agent', IsVisibleForCustomer => 1, IncomingTime => 1490690026, CreateBy => 1, CreateTime => '2017-03-28 08:33:47', Charset => 'ISO-8859-15', MimeType => 'text/plain', # If DynamicFields => 1 was passed, you'll get an entry like this for each dynamic field: DynamicField_X => 'value_x', # If RealNames => 1 was passed, you'll get fields with contact real names too: FromRealname => 'Some Agent', ToRealname => 'Some Customer A', CcRealname => 'Some Customer B', BccRealname => 'Some Customer C', ); =cut sub ArticleGet { my ( $Self, %Param ) = @_; for my $Item (qw(TicketID ArticleID)) { if ( !$Param{$Item} ) { $Kernel::OM->Get('Kernel::System::Log')->Log( Priority => 'error', Message => "Need $Item!" ); return; } } # Get meta article. my %Article = $Self->_MetaArticleGet( ArticleID => $Param{ArticleID}, TicketID => $Param{TicketID}, ); return if !%Article; my %ArticleSenderTypeList = $Kernel::OM->Get('Kernel::System::Ticket::Article')->ArticleSenderTypeList(); # Email parser object might be used below for its field cleanup methods only. my $EmailParser; if ( $Param{RealNames} ) { $EmailParser = Kernel::System::EmailParser->new( Mode => 'Standalone', ); } my $DBObject = $Kernel::OM->Get('Kernel::System::DB'); my $SQL = ' SELECT sadm.a_from, sadm.a_reply_to, sadm.a_to, sadm.a_cc, sadm.a_bcc, sadm.a_subject, sadm.a_message_id, sadm.a_in_reply_to, sadm.a_references, sadm.a_content_type, sadm.a_body, sadm.incoming_time FROM article_data_mime sadm WHERE sadm.article_id = ? '; my @Bind = ( \$Param{ArticleID} ); return if !$DBObject->Prepare( SQL => $SQL, Bind => \@Bind, Limit => 1, ); my %Data; while ( my @Row = $DBObject->FetchrowArray() ) { %Data = ( %Article, From => $Row[0], ReplyTo => $Row[1], To => $Row[2], Cc => $Row[3], Bcc => $Row[4], Subject => $Row[5], MessageID => $Row[6], InReplyTo => $Row[7], References => $Row[8], ContentType => $Row[9], Body => $Row[10], IncomingTime => $Row[11], SenderType => $ArticleSenderTypeList{ $Article{SenderTypeID} }, ); # Determine charset. if ( $Data{ContentType} && $Data{ContentType} =~ /charset=/i ) { $Data{Charset} = $Data{ContentType}; $Data{Charset} =~ s/.+?charset=("|'|)(\w+)/$2/gi; $Data{Charset} =~ s/"|'//g; $Data{Charset} =~ s/(.+?);.*/$1/g; } else { $Data{Charset} = ''; } $Data{ContentCharset} = $Data{Charset}; # compatibility # Determine MIME type. if ( $Data{ContentType} && $Data{ContentType} =~ /^(\w+\/\w+)/i ) { $Data{MimeType} = $1; $Data{MimeType} =~ s/"|'//g; } else { $Data{MimeType} = ''; } RECIPIENT: for my $Key (qw(From To Cc Bcc Subject)) { next RECIPIENT if !$Data{$Key}; # Strip unwanted stuff from some fields. $Data{$Key} =~ s/\n|\r//g; # Skip further processing for subject field. next RECIPIENT if $Key eq 'Subject'; if ( $Param{RealNames} ) { # Check if it's a queue. if ( $Data{$Key} !~ /@/ ) { $Data{ $Key . 'Realname' } = $Data{$Key}; next RECIPIENT; } # Strip out real names. my $Realname = ''; EMAILADDRESS: for my $EmailSplit ( $EmailParser->SplitAddressLine( Line => $Data{$Key} ) ) { my $Name = $EmailParser->GetRealname( Email => $EmailSplit ); if ( !$Name ) { $Name = $EmailParser->GetEmailAddress( Email => $EmailSplit ); } next EMAILADDRESS if !$Name; if ($Realname) { $Realname .= ', '; } $Realname .= $Name; } # Add real name lines. $Data{ $Key . 'Realname' } = $Realname; } } } # Check if we also need to return dynamic field data. if ( $Param{DynamicFields} ) { my %DataWithDynamicFields = $Self->_MetaArticleDynamicFieldsGet( Data => \%Data, ); %Data = %DataWithDynamicFields; } # Return if content is empty. if ( !%Data ) { $Kernel::OM->Get('Kernel::System::Log')->Log( Priority => 'error', Message => "No such article (TicketID=$Param{TicketID}, ArticleID=$Param{ArticleID})!", ); return; } return %Data; } =head2 ArticleUpdate() Update article data. Note: Keys C, C, C, C, C, C, C, C, C and C are implemented. my $Success = $ArticleBackendObject->ArticleUpdate( TicketID => 123, ArticleID => 123, Key => 'Body', Value => 'New Body', UserID => 123, ); my $Success = $ArticleBackendObject->ArticleUpdate( TicketID => 123, ArticleID => 123, Key => 'SenderType', Value => 'agent', UserID => 123, ); Events: ArticleUpdate =cut sub ArticleUpdate { my ( $Self, %Param ) = @_; for my $Item (qw(TicketID ArticleID UserID Key)) { if ( !$Param{$Item} ) { $Kernel::OM->Get('Kernel::System::Log')->Log( Priority => 'error', Message => "Need $Item!", ); return; } } if ( !defined $Param{Value} ) { $Kernel::OM->Get('Kernel::System::Log')->Log( Priority => 'error', Message => 'Need Value!', ); return; } my $ArticleObject = $Kernel::OM->Get('Kernel::System::Ticket::Article'); # Lookup for sender type ID. if ( $Param{Key} eq 'SenderType' ) { $Param{Key} = 'SenderTypeID'; $Param{Value} = $ArticleObject->ArticleSenderTypeLookup( SenderType => $Param{Value}, ); } my %Map = ( Body => 'a_body', Subject => 'a_subject', From => 'a_from', To => 'a_to', Cc => 'a_cc', Bcc => 'a_bcc', ReplyTo => 'a_reply_to', ); if ( $Map{ $Param{Key} } ) { return if !$Kernel::OM->Get('Kernel::System::DB')->Do( SQL => " UPDATE article_data_mime SET $Map{ $Param{Key} } = ?, change_time = current_timestamp, change_by = ? WHERE article_id = ? ", Bind => [ \$Param{Value}, \$Param{UserID}, \$Param{ArticleID} ], ); } else { return if !$Self->_MetaArticleUpdate( %Param, ); } $ArticleObject->_ArticleCacheClear( TicketID => $Param{TicketID}, ); $ArticleObject->ArticleSearchIndexBuild( TicketID => $Param{TicketID}, ArticleID => $Param{ArticleID}, UserID => $Param{UserID}, ); $Self->EventHandler( Event => 'ArticleUpdate', Data => { TicketID => $Param{TicketID}, ArticleID => $Param{ArticleID}, }, UserID => $Param{UserID}, ); return 1; } =head2 ArticleDelete() Delete article data, its plain message, and all attachments. my $Success = $ArticleBackendObject->ArticleDelete( TicketID => 123, ArticleID => 123, UserID => 123, ); =cut sub ArticleDelete { ## no critic; my ( $Self, %Param ) = @_; for my $Needed (qw(ArticleID TicketID UserID)) { if ( !$Param{$Needed} ) { $Kernel::OM->Get('Kernel::System::Log')->Log( Priority => 'error', Message => "Need $Needed!", ); return; } } # Delete from article storage. return if !$Kernel::OM->Get( $Self->{ArticleStorageModule} )->ArticleDelete(%Param); my $DBObject = $Kernel::OM->Get('Kernel::System::DB'); # Delete article data. return if !$DBObject->Do( SQL => 'DELETE FROM article_data_mime WHERE article_id = ?', Bind => [ \$Param{ArticleID} ], ); # Delete related transmission error entries. return if !$DBObject->Do( SQL => 'DELETE FROM article_data_mime_send_error WHERE article_id = ?', Bind => [ \$Param{ArticleID} ], ); # Delete meta article and associated data, and clear cache. return $Self->_MetaArticleDelete( %Param, ); } =head1 STORAGE BACKEND DELEGATE METHODS =head2 ArticleWritePlain() Write a plain email to storage. This is a delegate method from active backend. my $Success = $ArticleBackendObject->ArticleWritePlain( ArticleID => 123, Email => $EmailAsString, UserID => 123, ); =cut sub ArticleWritePlain { ## no critic; my $Self = shift; return $Kernel::OM->Get( $Self->{ArticleStorageModule} )->ArticleWritePlain(@_); } =head2 ArticlePlain() Get plain article/email from storage. This is a delegate method from active backend. my $PlainMessage = $ArticleBackendObject->ArticlePlain( ArticleID => 123, UserID => 123, ); Returns: $PlainMessage = ' From: OTRS Feedback To: Your OTRS System Subject: Welcome to OTRS! Content-Type: text/plain; charset=utf-8 Content-Transfer-Encoding: 8bit Welcome to OTRS! ... '; =cut sub ArticlePlain { ## no critic; my $Self = shift; return $Kernel::OM->Get( $Self->{ArticleStorageModule} )->ArticlePlain(@_); } =head2 ArticleDeletePlain() Delete a plain article from storage. This is a delegate method from active backend. my $Success = $ArticleBackendObject->ArticleDeletePlain( ArticleID => 123, UserID => 123, ); =cut sub ArticleDeletePlain { ## no critic; my $Self = shift; return $Kernel::OM->Get( $Self->{ArticleStorageModule} )->ArticleDeletePlain(@_); } =head2 ArticleWriteAttachment() Write an article attachment to storage. This is a delegate method from active backend. my $Success = $ArticleBackendObject->ArticleWriteAttachment( Content => $ContentAsString, ContentType => 'text/html; charset="iso-8859-15"', Filename => 'lala.html', ContentID => 'cid-1234', # optional ContentAlternative => 0, # optional, alternative content to shown as body Disposition => 'attachment', # or 'inline' ArticleID => 123, UserID => 123, ); =cut sub ArticleWriteAttachment { ## no critic; my $Self = shift; return $Kernel::OM->Get( $Self->{ArticleStorageModule} )->ArticleWriteAttachment(@_); } =head2 ArticleAttachment() Get article attachment from storage. This is a delegate method from active backend. my %Attachment = $ArticleBackendObject->ArticleAttachment( ArticleID => 123, FileID => 1, # as returned by ArticleAttachmentIndex ); Returns: %Attachment = ( Content => 'xxxx', # actual attachment contents ContentAlternative => '', ContentID => '', ContentType => 'application/pdf', Filename => 'StdAttachment-Test1.pdf', FilesizeRaw => 4722, Disposition => 'attachment', ); =cut sub ArticleAttachment { ## no critic; my $Self = shift; return $Kernel::OM->Get( $Self->{ArticleStorageModule} )->ArticleAttachment(@_); } =head2 ArticleDeleteAttachment() Delete all attachments of an article from storage. This is a delegate method from active backend. my $Success = $ArticleBackendObject->ArticleDeleteAttachment( ArticleID => 123, UserID => 123, ); =cut sub ArticleDeleteAttachment { ## no critic; my $Self = shift; return $Kernel::OM->Get( $Self->{ArticleStorageModule} )->ArticleDeleteAttachment(@_); } =head2 ArticleAttachmentIndex() Get article attachment index as hash. my %Index = $ArticleBackendObject->ArticleAttachmentIndex( ArticleID => 123, ExcludePlainText => 1, # (optional) Exclude plain text attachment ExcludeHTMLBody => 1, # (optional) Exclude HTML body attachment ExcludeInline => 1, # (optional) Exclude inline attachments ); Returns: my %Index = { '1' => { # Attachment ID ContentAlternative => '', # (optional) ContentID => '', # (optional) Filesize => '4.6 KB', ContentType => 'application/pdf', FilesizeRaw => 4722, Disposition => 'attachment', }, '2' => { ContentAlternative => '', ContentID => '', Filesize => '183 B', ContentType => 'text/html; charset="utf-8"', FilesizeRaw => 183, Disposition => 'attachment', }, ... }; =cut sub ArticleAttachmentIndex { ## no critic my $Self = shift; return $Kernel::OM->Get( $Self->{ArticleStorageModule} )->ArticleAttachmentIndex(@_); } =head2 BackendSearchableFieldsGet() Get the definition of the searchable fields as a hash. my %SearchableFields = $ArticleBackendObject->BackendSearchableFieldsGet(); Returns: my %SearchableFields = ( 'MIMEBase_From' => { Label => 'From', Key => 'MIMEBase_From', Type => 'Text', Filterable => 0, }, 'MIMEBase_To' => { Label => 'To', Key => 'MIMEBase_To', Type => 'Text', Filterable => 0, }, 'MIMEBase_Cc' => { Label => 'Cc', Key => 'MIMEBase_Cc', Type => 'Text', Filterable => 0, }, 'MIMEBase_Bcc' => { Label => 'Bcc', Key => 'MIMEBase_Bcc', Type => 'Text', Filterable => 0, }, 'MIMEBase_Subject' => { Label => 'Subject', Key => 'MIMEBase_Subject', Type => 'Text', Filterable => 1, }, 'MIMEBase_Body' => { Label => 'Body', Key => 'MIMEBase_Body', Type => 'Text', Filterable => 1, }, 'MIMEBase_AttachmentName' => { Label => 'Attachment Name', Key => 'MIMEBase_AttachmentName', Type => 'Text', Filterable => 0, }, ); =cut sub BackendSearchableFieldsGet { my ( $Self, %Param ) = @_; my %SearchableFields = ( 'MIMEBase_From' => { Label => 'From', Key => 'MIMEBase_From', Type => 'Text', Filterable => 0, }, 'MIMEBase_To' => { Label => 'To', Key => 'MIMEBase_To', Type => 'Text', Filterable => 0, }, 'MIMEBase_Cc' => { Label => 'Cc', Key => 'MIMEBase_Cc', Type => 'Text', Filterable => 0, }, 'MIMEBase_Bcc' => { Label => 'Bcc', Key => 'MIMEBase_Bcc', Type => 'Text', Filterable => 0, HideInCustomerInterface => 1, }, 'MIMEBase_Subject' => { Label => 'Subject', Key => 'MIMEBase_Subject', Type => 'Text', Filterable => 1, }, 'MIMEBase_Body' => { Label => 'Body', Key => 'MIMEBase_Body', Type => 'Text', Filterable => 1, }, ); if ( $Kernel::OM->Get('Kernel::Config')->Get('Ticket::Article::Backend::MIMEBase::IndexAttachmentNames') ) { $SearchableFields{'MIMEBase_AttachmentName'} = { Label => 'Attachment Name', Key => 'MIMEBase_AttachmentName', Type => 'Text', Filterable => 0, }; } return %SearchableFields; } =head2 ArticleSearchableContentGet() Get article attachment index as hash. my %Index = $ArticleBackendObject->ArticleSearchableContentGet( TicketID => 123, # (required) ArticleID => 123, # (required) DynamicFields => 1, # (optional) To include the dynamic field values for this article on the return structure. RealNames => 1, # (optional) To include the From/To/Cc/Bcc fields with real names. UserID => 123, # (required) ); Returns: my %ArticleSearchData = { 'From' => { String => 'Test User1 ', Key => 'From', Type => 'Text', Filterable => 0, }, 'To' => { String => 'Test User2 ', Key => 'To', Type => 'Text', Filterable => 0, }, 'Cc' => { String => 'Test User3 ', Key => 'Cc', Type => 'Text', Filterable => 0, }, 'Bcc' => { String => 'Test User4 ', Key => 'Bcc', Type => 'Text', Filterable => 0, }, 'Subject' => { String => 'This is a test subject!', Key => 'Subject', Type => 'Text', Filterable => 1, }, 'Body' => { String => 'This is a body text!', Key => 'Body', Type => 'Text', Filterable => 1, } }; =cut sub ArticleSearchableContentGet { my ( $Self, %Param ) = @_; for my $Item (qw(TicketID ArticleID UserID)) { if ( !$Param{$Item} ) { $Kernel::OM->Get('Kernel::System::Log')->Log( Priority => 'error', Message => "Need $Item!" ); return; } } my %DataKeyMap = ( 'MIMEBase_From' => 'From', 'MIMEBase_To' => 'To', 'MIMEBase_Cc' => 'Cc', 'MIMEBase_Bcc' => 'Bcc', 'MIMEBase_Subject' => 'Subject', 'MIMEBase_Body' => 'Body', ); my %ArticleData = $Self->ArticleGet( TicketID => $Param{TicketID}, ArticleID => $Param{ArticleID}, UserID => $Param{UserID}, DynamicFields => 0, ); my %BackendSearchableFields = $Self->BackendSearchableFieldsGet(); my %ArticleSearchData; FIELDKEY: for my $FieldKey ( sort keys %BackendSearchableFields ) { my $IndexString; # scan available attachment names and append the information if ( $FieldKey eq 'MIMEBase_AttachmentName' ) { my %AttachmentIndex = $Self->ArticleAttachmentIndex( ArticleID => $Param{ArticleID}, UserID => $Param{UserID}, ExcludePlainText => 1, ExcludeHTMLBody => 1, ExcludeInline => 1, ); next FIELDKEY if !%AttachmentIndex; my @AttachmentNames; for my $AttachmentKey ( sort keys %AttachmentIndex ) { push @AttachmentNames, $AttachmentIndex{$AttachmentKey}->{Filename}; } $IndexString = join ' ', @AttachmentNames; } $IndexString //= $ArticleData{ $DataKeyMap{$FieldKey} }; next FIELDKEY if !IsStringWithData($IndexString); $ArticleSearchData{$FieldKey} = { String => $IndexString, Key => $BackendSearchableFields{$FieldKey}->{Key}, Type => $BackendSearchableFields{$FieldKey}->{Type} // 'Text', Filterable => $BackendSearchableFields{$FieldKey}->{Filterable} // 0, }; } return %ArticleSearchData; } =head2 ArticleHasHTMLContent() Returns 1 if article has HTML content. my $ArticleHasHTMLContent = $ArticleBackendObject->ArticleHasHTMLContent( TicketID => 1, ArticleID => 2, UserID => 1, ); Result: $ArticleHasHTMLContent = 1; # or 0 =cut sub ArticleHasHTMLContent { my ( $Self, %Param ) = @_; # Check needed stuff. for my $Needed (qw(TicketID ArticleID UserID)) { if ( !$Param{$Needed} ) { $Kernel::OM->Get('Kernel::System::Log')->Log( Priority => 'error', Message => "Need $Needed!", ); return; } } # Check if there is HTML body attachment. my %AttachmentIndexHTMLBody = $Self->ArticleAttachmentIndex( %Param, OnlyHTMLBody => 1, ); my ($HTMLBodyAttachmentID) = sort keys %AttachmentIndexHTMLBody; return $HTMLBodyAttachmentID ? 1 : 0; } 1; =head1 TERMS AND CONDITIONS This software is part of the OTRS project (L). This software comes with ABSOLUTELY NO WARRANTY. For details, see the enclosed file COPYING for license information (GPL). If you did not receive this file, see L. =cut