Files
scripts/Perl OTRS/Kernel/System/Ticket/Article/Backend/MIMEBase.pm
2024-10-14 00:08:40 +02:00

1513 lines
49 KiB
Perl

# --
# 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<MIME> based article backends
=head1 DESCRIPTION
This is a base class for article data in C<MIME> format and should not be instantiated directly.
Always get real backend instead, i.e. C<Email>, C<Phone> or C<Internal>.
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<Body> field. For original
message with email headers, use L</ArticlePlain()> method to retrieve it from storage backend.
Attachments are handled by the storage backends, and can be retrieved via L</ArticleAttachment()>.
Inherits from L<Kernel::System::Ticket::Article::Backend::Base>.
See also L<Kernel::System::Ticket::Article::Backend::MIMEBase::Base> and
L<Kernel::System::Ticket::Article::Backend::Email>.
=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 <email@example.com>', # not required but useful
To => 'Some Customer A <customer-a@example.com>', # not required but useful
Cc => 'Some Customer B <customer-b@example.com>', # not required but useful
Bcc => 'Some Customer C <customer-c@example.com>', # not required but useful
ReplyTo => 'Some Customer B <customer-b@example.com>', # not required
Subject => 'some short description', # not required but useful
Body => 'the message text', # not required but useful
MessageID => '<asdasdasd.123@example.com>', # not required but useful
InReplyTo => '<asdasdasd.12@example.com>', # not required but useful
References => '<asdasdasd.1@example.com> <asdasdasd.12@example.com>', # 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 <email@example.com>', # not required but useful
To => 'Some Customer A <customer-a@example.com>', # 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 <email@example.com>',
To => 'Some Customer A <customer-a@example.com>',
Cc => 'Some Customer B <customer-b@example.com>',
Bcc => 'Some Customer C <customer-c@example.com>',
ReplyTo => 'Some Customer B <customer-b@example.com>',
Subject => 'some short description',
MessageID => '<asdasdasd.123@example.com>',
InReplyTo => '<asdasdasd.12@example.com>',
References => '<asdasdasd.1@example.com> <asdasdasd.12@example.com>',
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<Body>, C<Subject>, C<From>, C<To>, C<Cc>, C<Bcc>, C<ReplyTo>, C<SenderType>, C<SenderTypeID>
and C<IsVisibleForCustomer> 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 <marketing@otrs.com>
To: Your OTRS System <otrs@localhost>
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 <testuser1@example.com>',
Key => 'From',
Type => 'Text',
Filterable => 0,
},
'To' => {
String => 'Test User2 <testuser2@example.com>',
Key => 'To',
Type => 'Text',
Filterable => 0,
},
'Cc' => {
String => 'Test User3 <testuser3@example.com>',
Key => 'Cc',
Type => 'Text',
Filterable => 0,
},
'Bcc' => {
String => 'Test User4 <testuser4@example.com>',
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<https://otrs.org/>).
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<https://www.gnu.org/licenses/gpl-3.0.txt>.
=cut