This commit is contained in:
2024-10-14 00:08:40 +02:00
parent dbfba56f66
commit 1462d52e13
4572 changed files with 2658864 additions and 0 deletions

View File

@@ -0,0 +1,695 @@
# --
# 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::Base;
use strict;
use warnings;
use parent qw(Kernel::System::EventHandler);
use Kernel::System::VariableCheck qw(:all);
our @ObjectDependencies = (
'Kernel::System::Cache',
'Kernel::System::CommunicationChannel',
'Kernel::System::DB',
'Kernel::System::DynamicField',
'Kernel::System::DynamicField::Backend',
'Kernel::System::DynamicFieldValue',
'Kernel::System::Log',
'Kernel::System::Main',
'Kernel::System::Ticket::Article',
);
=head1 NAME
Kernel::System::Ticket::Article::Backend::Base - base class for article backends
=head1 DESCRIPTION
This is a base class for article backends and should not be instantiated directly.
package Kernel::System::Ticket::Article::Backend::MyBackend;
use strict;
use warnings;
use parent qw(Kernel::System::Ticket::Article::Backend::Base);
# methods go here
=cut
=head1 PUBLIC INTERFACE
=head2 new()
Do not instantiate this class, instead use the real article backend classes.
Also, don't use the constructor directly, use the ObjectManager instead:
my $ArticleBackendObject = $Kernel::OM->Get('Kernel::System::Ticket::Article::Backend::MyBackend');
=cut
sub new {
my ( $Type, %Param ) = @_;
# Die if someone tries to instantiate the base class.
if ( $Type eq __PACKAGE__ ) {
die 'Virtual method in base class must not be called.';
}
my $Self = {};
bless( $Self, $Type );
# init of event handler
$Self->EventHandlerInit(
Config => 'Ticket::EventModulePost',
);
return $Self;
}
=head2 ChannelNameGet()
Returns name of the communication channel used by the article backend. Used internally. Override this method in your
backend class.
my $ChannelName = $ArticleBackendObject->ChannelNameGet();
$ChannelName = 'MyBackend';
=cut
sub ChannelNameGet {
die 'Virtual method in base class must not be called.';
}
=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 {
die 'Virtual method in base class must not be called.';
}
=head2 ChannelIDGet()
Returns registered communication channel ID. Same for all article backends, don't override this
particular method. In case of invalid article backend, this method will return false value.
my $ChannelID = $ArticleBackendObject->ChannelIDGet();
$ChannelID = 1;
=cut
sub ChannelIDGet {
my ( $Self, %Param ) = @_;
return $Self->{CommunicationChannelID} if defined $Self->{CommunicationChannelID};
return if $Self->ChannelNameGet() eq 'Invalid';
# Get registered communication channel ID.
my %CommunicationChannel = $Kernel::OM->Get('Kernel::System::CommunicationChannel')->ChannelGet(
ChannelName => $Self->ChannelNameGet(),
);
$Self->{CommunicationChannelID} = $CommunicationChannel{ChannelID};
if ( !$Self->{CommunicationChannelID} ) {
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'error',
Message => "Can't get channel ID for '"
. $Self->ChannelNameGet()
. "'. Communication channel not registered correctly!",
);
return;
}
return $Self->{CommunicationChannelID};
}
=head2 ArticleCreate()
Create an article. Override this method in your class.
my $ArticleID = $ArticleBackendObject->ArticleCreate(
TicketID => 123,
SenderType => 'agent', # agent|system|customer
IsVisibleForCustomer => 1,
UserID => 123,
# Backend specific parameters:
# From => 'Some Agent <email@example.com>',
# To => 'Some Customer A <customer-a@example.com>',
# Subject => 'some short description',
# ...
);
Events:
ArticleCreate
=cut
sub ArticleCreate {
die 'Virtual method in base class must not be called.';
}
=head2 ArticleUpdate()
Update an article. Override this method in your class.
my $Success = $ArticleBackendObject->ArticleUpdate(
TicketID => 123,
ArticleID => 123,
Key => 'Body',
Value => 'New Body',
UserID => 123,
);
Events:
ArticleUpdate
=cut
sub ArticleUpdate {
die 'Virtual method in base class must not be called.';
}
=head2 ArticleGet()
Returns article data. Override this method in your class.
my %Article = $ArticleBackendObject->ArticleGet(
TicketID => 123,
ArticleID => 123,
DynamicFields => 1,
# Backend specific parameters:
# RealNames => 1,
);
=cut
sub ArticleGet {
die 'Virtual method in base class must not be called.';
}
=head2 ArticleDelete()
Delete an article. Override this method in your class.
my $Success = $ArticleBackendObject->ArticleDelete(
TicketID => 123,
ArticleID => 123,
UserID => 123,
);
=cut
sub ArticleDelete {
die 'Virtual method in base class must not be called.';
}
=head2 BackendSearchableFieldsGet()
Get article attachment index as hash.
my %Index = $ArticleBackendObject->BackendSearchableFieldsGet();
Returns:
my %BackendSearchableFieldsGet = {
From => 'from',
To => 'to',
Cc => 'cc',
Subject => 'subject',
Body => 'body',
};
=cut
sub BackendSearchableFieldsGet {
die 'Virtual method in base class must not be called.';
}
=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 fields with real names.
UserID => 123, # (required)
);
Returns:
my %ArticleSearchData = [
{
'From' => 'Test User1 <testuser1@example.com>',
'To' => 'Test User2 <testuser2@example.com>',
'Cc' => 'Test User3 <testuser3@example.com>',
'Subject' => 'This is a test subject!',
'Body' => 'This is a body text!',
...
},
];
=cut
sub ArticleSearchableContentGet {
die 'Virtual method in base class must not be called.';
}
=head1 PRIVATE FUNCTIONS
Use following functions from backends only.
=head2 _MetaArticleCreate()
Create a new article.
my $ArticleID = $Self->_MetaArticleCreate(
TicketID => 123,
SenderType => 'agent', # agent|system|customer
IsVisibleForCustomer => 0,
CommunicationChannel => 'Email',
UserID => 1,
);
Alternatively, you can pass in IDs too:
my $ArticleID = $Self->_MetaArticleCreate(
TicketID => 123,
SenderTypeID => 1,
IsVisibleForCustomer => 0,
CommunicationChannelID => 2,
UserID => 1,
);
=cut
sub _MetaArticleCreate {
my ( $Self, %Param ) = @_;
if ( $Param{SenderType} && !$Param{SenderTypeID} ) {
$Param{SenderTypeID} = $Kernel::OM->Get('Kernel::System::Ticket::Article')->ArticleSenderTypeLookup(
SenderType => $Param{SenderType},
);
}
my %Channel = $Kernel::OM->Get('Kernel::System::CommunicationChannel')->ChannelGet(
ChannelID => scalar $Param{CommunicationChannelID},
ChannelName => scalar $Param{CommunicationChannel},
);
if ( !%Channel ) {
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'error',
Message => "Need CommunicationChannelID!"
);
return;
}
for my $Needed (qw(TicketID SenderTypeID CommunicationChannelID UserID )) {
if ( !$Param{$Needed} ) {
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'error',
Message => "Need $Needed!"
);
return;
}
}
for my $Needed (qw(IsVisibleForCustomer)) {
if ( !defined $Param{$Needed} ) {
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'error',
Message => "Need $Needed!"
);
return;
}
}
my $DBObject = $Kernel::OM->Get('Kernel::System::DB');
my $RandomString = $Kernel::OM->Get('Kernel::System::Main')->GenerateRandomString(
Length => 32,
);
my $InsertFingerprint = $$ . '-' . $RandomString;
return if !$DBObject->Do(
SQL => 'INSERT INTO article
(ticket_id, article_sender_type_id, is_visible_for_customer, communication_channel_id, insert_fingerprint, create_time, create_by, change_time, change_by)
VALUES (?, ?, ?, ?, ?, current_timestamp, ?, current_timestamp, ?)',
Bind => [
\$Param{TicketID}, \$Param{SenderTypeID}, \( $Param{IsVisibleForCustomer} ? 1 : 0 ), \$Channel{ChannelID},
\$InsertFingerprint, \$Param{UserID}, \$Param{UserID},
],
);
# get article id
return if !$DBObject->Prepare(
SQL => 'SELECT id FROM article
WHERE ticket_id = ?
AND insert_fingerprint = ?
ORDER BY id DESC',
Bind => [ \$Param{TicketID}, \$InsertFingerprint ],
Limit => 1,
);
my $ArticleID;
while ( my @Row = $DBObject->FetchrowArray() ) {
$ArticleID = $Row[0];
}
# return if there is not article created
if ( !$ArticleID ) {
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'error',
Message => "Can't get ArticleID from insert (TicketID=$Param{TicketID})!",
);
return;
}
$Kernel::OM->Get('Kernel::System::Ticket::Article')->_ArticleCacheClear(
TicketID => $Param{TicketID},
);
return $ArticleID;
}
=head2 _MetaArticleUpdate()
Update an article.
Note: Keys C<SenderType>, C<SenderTypeID> and C<IsVisibleForCustomer> are implemented.
my $Success = $Self->_MetaArticleUpdate(
TicketID => 123, # (required)
ArticleID => 123, # (required)
Key => 'IsVisibleForCustomer', # (optional) If not provided, only ChangeBy and ChangeTime will be updated.
Value => 1, # (optional)
UserID => 123, # (required)
);
my $Success = $Self->_MetaArticleUpdate(
TicketID => 123,
ArticleID => 123,
Key => 'SenderType',
Value => 'agent',
UserID => 123,
);
Events:
MetaArticleUpdate
=cut
sub _MetaArticleUpdate {
my ( $Self, %Param ) = @_;
for my $Needed (qw(ArticleID UserID TicketID)) {
if ( !$Param{$Needed} ) {
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'error',
Message => "Need $Needed!"
);
return;
}
}
if ( $Param{Key} && $Param{Key} eq 'SenderType' ) {
$Param{Key} = 'SenderTypeID';
$Param{Value} = $Self->ArticleSenderTypeLookup(
SenderType => $Param{Value},
);
}
# map
my %Map = (
SenderTypeID => 'article_sender_type_id',
IsVisibleForCustomer => 'is_visible_for_customer',
);
my @Bind;
my $SQL = '
UPDATE article
SET
';
if ( $Param{Key} && $Map{ $Param{Key} } ) {
if ( !defined $Param{Value} ) {
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'error',
Message => 'Need Value!',
);
return;
}
$SQL .= "$Map{$Param{Key}} = ?, ";
push @Bind, \$Param{Value};
}
$SQL .= '
change_time = current_timestamp, change_by = ?
WHERE id = ?
';
push @Bind, ( \$Param{UserID}, \$Param{ArticleID} );
# db update
return if !$Kernel::OM->Get('Kernel::System::DB')->Do(
SQL => $SQL,
Bind => \@Bind,
);
$Kernel::OM->Get('Kernel::System::Ticket::Article')->_ArticleCacheClear(
TicketID => $Param{TicketID},
);
return 1;
}
=head2 _MetaArticleGet()
Get article meta data.
my %Article = $Self->_MetaArticleGet(
ArticleID => 42,
TicketID => 23,
);
Returns:
%Article = (
ArticleID => 1,
TicketID => 2,
CommunicationChannelID => 1,
SenderTypeID => 1,
IsVisibleForCustomer => 0,
CreateTime => ...,
CreateBy => ...,
ChangeTime => ...,
ChangeBy => ...,
);
=cut
sub _MetaArticleGet {
my ( $Self, %Param ) = @_;
for my $Needed (qw(ArticleID TicketID)) {
if ( !$Param{$Needed} ) {
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'error',
Message => "Need $Needed!"
);
return;
}
}
# Use ArticleList() internally to benefit from its ticket-level cache.
my @MetaArticleIndex = $Kernel::OM->Get('Kernel::System::Ticket::Article')->ArticleList(
TicketID => $Param{TicketID},
ArticleID => $Param{ArticleID},
);
return %{ $MetaArticleIndex[0] // {} };
}
=head2 _MetaArticleDelete()
Delete an article. This must be called B<after> all backend data has been deleted.
my $Success = $Self->_MetaArticleDelete(
ArticleID => 123,
UserID => 123,
TicketID => 123,
);
=cut
sub _MetaArticleDelete {
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;
}
}
return if !$Kernel::OM->Get('Kernel::System::DynamicFieldValue')->ObjectValuesDelete(
ObjectType => 'Article',
ObjectID => $Param{ArticleID},
UserID => $Param{UserID},
);
my $ArticleObject = $Kernel::OM->Get('Kernel::System::Ticket::Article');
# Delete related accounted time entries.
return if !$ArticleObject->ArticleAccountedTimeDelete(
ArticleID => $Param{ArticleID},
UserID => $Param{UserID},
);
# Delete article from article search index.
return if !$ArticleObject->ArticleSearchIndexDelete(
ArticleID => $Param{ArticleID},
UserID => $Param{UserID},
);
my $DBObject = $Kernel::OM->Get('Kernel::System::DB');
# Remove all associated data for the article.
return if !$DBObject->Do(
SQL => 'DELETE FROM article_flag WHERE article_id = ?',
Bind => [ \$Param{ArticleID} ],
);
return if !$DBObject->Do(
SQL => 'DELETE FROM ticket_history WHERE article_id = ?',
Bind => [ \$Param{ArticleID} ],
);
return if !$DBObject->Do(
SQL => 'DELETE FROM mail_queue WHERE article_id = ?',
Bind => [ \$Param{ArticleID} ],
);
# Finally, delete the meta article entry.
return if !$DBObject->Do(
SQL => 'DELETE FROM article WHERE id = ?',
Bind => [ \$Param{ArticleID} ],
);
$Kernel::OM->Get('Kernel::System::Ticket::Article')->_ArticleCacheClear(
TicketID => $Param{TicketID},
);
return 1;
}
=head2 _MetaArticleDynamicFieldsGet()
Returns article content with dynamic fields.
my %Data = $Self->_MetaArticleDynamicFieldsGet(
Data => { # (required) article data
TicketID => 1,
ArticleID => 1,
From => 'agent@mail.org',
To => 'customer@mail.org',
...
},
);
Returns:
%Data = (
TicketID => 1,
ArticleID => 1,
From => 'agent@mail.org',
To => 'customer@mail.org',
...,
DynamicField_A => 'Value A',
...
);
=cut
sub _MetaArticleDynamicFieldsGet {
my ( $Self, %Param ) = @_;
if ( !$Param{Data} || ref $Param{Data} ne 'HASH' ) {
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'error',
Message => 'Need Data!',
);
return;
}
my %Data = %{ $Param{Data} };
my $DynamicFieldObject = $Kernel::OM->Get('Kernel::System::DynamicField');
my $DynamicFieldBackendObject = $Kernel::OM->Get('Kernel::System::DynamicField::Backend');
my $DynamicFieldArticleList = $DynamicFieldObject->DynamicFieldListGet(
ObjectType => 'Article',
);
DYNAMICFIELD:
for my $DynamicFieldConfig ( @{$DynamicFieldArticleList} ) {
# Validate each dynamic field.
next DYNAMICFIELD if !$DynamicFieldConfig;
next DYNAMICFIELD if !IsHashRefWithData($DynamicFieldConfig);
next DYNAMICFIELD if !$DynamicFieldConfig->{Name};
next DYNAMICFIELD if !IsHashRefWithData( $DynamicFieldConfig->{Config} );
# Get the current value for each dynamic field.
my $Value = $DynamicFieldBackendObject->ValueGet(
DynamicFieldConfig => $DynamicFieldConfig,
ObjectID => $Data{ArticleID},
);
# Set the dynamic field name and value into the article hash.
$Data{ 'DynamicField_' . $DynamicFieldConfig->{Name} } = $Value;
}
return %Data;
}
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

View File

@@ -0,0 +1,761 @@
# --
# 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::Chat;
use strict;
use warnings;
use Kernel::System::VariableCheck qw(:all);
use Kernel::Language qw(Translatable);
use parent 'Kernel::System::Ticket::Article::Backend::Base';
our @ObjectDependencies = (
'Kernel::Config',
'Kernel::System::DateTime',
'Kernel::System::DB',
'Kernel::System::Log',
'Kernel::System::Ticket',
'Kernel::System::Ticket::Article',
'Kernel::System::User',
);
=head1 NAME
Kernel::System::Ticket::Article::Backend::Chat - backend class for chat based articles
=head1 DESCRIPTION
This class provides functions to manipulate chat based articles in the database.
Inherits from L<Kernel::System::Ticket::Article::Backend::MIMEBase>, please have a look there for its base API,
and below for the additional functions this backend provides.
=head1 PUBLIC INTERFACE
=cut
sub ChannelNameGet {
return 'Chat';
}
=head2 ArticleCreate()
Create a chat article.
my $ArticleID = $ArticleBackendObject->ArticleCreate(
TicketID => 123, # (required)
SenderTypeID => 1, # (required)
# or
SenderType => 'agent', # (required) agent|system|customer
ChatMessageList => [ # (required) Output from ChatMessageList()
{
ID => 1,
MessageText => 'My chat message',
CreateTime => '2014-04-04 10:10:10',
SystemGenerated => 0,
ChatterID => '123',
ChatterType => 'User',
ChatterName => 'John Doe',
},
...
],
IsVisibleForCustomer => 1, # (required) Is article visible for customer?
UserID => 123, # (required)
HistoryType => 'OwnerUpdate', # EmailCustomer|Move|AddNote|PriorityUpdate|WebRequestCustomer|...
HistoryComment => 'Some free text!',
);
Events:
ArticleCreate
=cut
sub ArticleCreate {
my ( $Self, %Param ) = @_;
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 ChatMessageList HistoryType HistoryComment)) {
if ( !$Param{$Needed} ) {
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'error',
Message => "Need $Needed!",
);
return;
}
}
if ( !IsArrayRefWithData( $Param{ChatMessageList} ) ) {
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'error',
Message => 'ChatMessageList must be an array reference!',
);
return;
}
if ( !defined $Param{IsVisibleForCustomer} ) {
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'error',
Message => 'Need IsVisibleForCustomer!',
);
return;
}
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,
);
# Check if this is the first article (for notifications).
my @Articles = $ArticleObject->ArticleList( TicketID => $Param{TicketID} );
my $FirstArticle = scalar @Articles ? 0 : 1;
# 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 $DBType = $DBObject->GetDatabaseFunction('Type');
CHAT_MESSAGE:
for my $ChatMessage ( @{ $Param{ChatMessageList} } ) {
next CHAT_MESSAGE if !IsHashRefWithData($ChatMessage);
my $Success = $DBObject->Do(
SQL => '
INSERT INTO article_data_otrs_chat
(article_id, chat_participant_id, chat_participant_name, chat_participant_type,
message_text, system_generated, create_time)
VALUES (?, ?, ?, ?, ?, ?, ?)
',
Bind => [
\$ArticleID, \$ChatMessage->{ChatterID}, \$ChatMessage->{ChatterName}, \$ChatMessage->{ChatterType},
\$ChatMessage->{MessageText}, \$ChatMessage->{SystemGenerated}, \$ChatMessage->{CreateTime},
],
);
if ( !$Success ) {
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'error',
Message => "System was unable to store data in article_data_otrs_chat table (ArticleID = $ArticleID)!",
);
return;
}
}
$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');
my $UserObject = $Kernel::OM->Get('Kernel::System::User');
# 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} );
}
my $ObjectDateTime = $Kernel::OM->Create('Kernel::System::DateTime');
# 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 => $ObjectDateTime->ToEpoch(),
TicketID => $Param{TicketID},
UserID => $Param{UserID},
);
}
}
# check if latest article is sent to customer
elsif ( $Param{SenderType} eq 'agent' ) {
$TicketObject->TicketUnlockTimeoutUpdate(
UnlockTimeout => $ObjectDateTime->ToEpoch(),
TicketID => $Param{TicketID},
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.
);
Returns:
%Article = (
TicketID => 123,
ArticleID => 123,
ChatMessageList => [
{
MessageText => 'My chat message',
CreateTime => '2014-04-04 10:10:10',
SystemGenerated => 0,
ChatterID => '123',
ChatterType => 'User',
ChatterName => 'John Doe',
},
...
],
SenderTypeID => 1,
SenderType => 'agent',
IsVisibleForCustomer => 1,
CreateBy => 1,
CreateTime => '2017-03-28 08:33:47',
# If DynamicFields => 1 was passed, you'll get an entry like this for each dynamic field:
DynamicField_X => 'value_x',
);
=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;
if ( !$Article{SenderType} ) {
my %ArticleSenderTypeList = $Kernel::OM->Get('Kernel::System::Ticket::Article')->ArticleSenderTypeList();
$Article{SenderType} = $ArticleSenderTypeList{ $Article{SenderTypeID} };
}
my $DBObject = $Kernel::OM->Get('Kernel::System::DB');
my $SQL = '
SELECT id, chat_participant_id, chat_participant_name, chat_participant_type, message_text, system_generated,
create_time
FROM article_data_otrs_chat
WHERE article_id = ?
ORDER BY id ASC
';
my @Bind = ( \$Param{ArticleID} );
return if !$DBObject->Prepare(
SQL => $SQL,
Bind => \@Bind,
);
my @ChatMessageList;
while ( my @Row = $DBObject->FetchrowArray() ) {
my %Data = (
ID => $Row[0],
ChatterID => $Row[1],
ChatterName => $Row[2],
ChatterType => $Row[3],
MessageText => $Row[4],
SystemGenerated => $Row[5],
CreateTime => $Row[6],
);
push @ChatMessageList, \%Data;
}
$Article{ChatMessageList} = \@ChatMessageList;
# Check if we also need to return dynamic field data.
if ( $Param{DynamicFields} ) {
my %DataWithDynamicFields = $Self->_MetaArticleDynamicFieldsGet(
Data => \%Article,
);
%Article = %DataWithDynamicFields;
}
# Return if content is empty.
if ( !%Article ) {
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'error',
Message => "No such article (TicketID=$Param{TicketID}, ArticleID=$Param{ArticleID})!",
);
return;
}
return %Article;
}
=head2 ArticleUpdate()
Update article data.
Note: Keys C<ChatMessageList>, C<SenderType>, C<SenderTypeID> and C<IsVisibleForCustomer> are implemented.
my $Success = $ArticleBackendObject->ArticleUpdate(
TicketID => 123, # (required)
ArticleID => 123, # (required)
Key => 'ChatMessageList', # (optional)
Value => [ # (optional)
{
MessageText => 'My chat message (edited)',
CreateTime => '2014-04-04 10:10:10',
SystemGenerated => 0,
ChatterID => '123',
ChatterType => 'User',
ChatterName => 'John Doe',
},
...
],
UserID => 123, # (required)
);
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)) {
if ( !$Param{$Item} ) {
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'error',
Message => "Need $Item!",
);
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},
);
}
if ( $Param{Key} eq 'ChatMessageList' ) {
if ( !IsArrayRefWithData( $Param{Value} ) ) {
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'error',
Message => 'Value must be an array reference!',
);
return;
}
my $DBObject = $Kernel::OM->Get('Kernel::System::DB');
# First, remove existing messages from storage.
my $Success = $DBObject->Do(
SQL => '
DELETE FROM article_data_otrs_chat
WHERE article_id = ?
',
Bind => [ \$Param{ArticleID} ],
);
if ( !$Success ) {
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'error',
Message =>
"System was unable to remove data from article_data_otrs_chat table (ArticleID = $Param{ArticleID})!",
);
return;
}
my $DBType = $DBObject->GetDatabaseFunction('Type');
# Add updated messages.
CHAT_MESSAGE:
for my $ChatMessage ( @{ $Param{Value} } ) {
next CHAT_MESSAGE if !IsHashRefWithData($ChatMessage);
my $Success = $DBObject->Do(
SQL => '
INSERT INTO article_data_otrs_chat
(article_id, chat_participant_id, chat_participant_name, chat_participant_type,
message_text, system_generated, create_time)
VALUES (?, ?, ?, ?, ?, ?, ?)
',
Bind => [
\$Param{ArticleID}, \$ChatMessage->{ChatterID}, \$ChatMessage->{ChatterName},
\$ChatMessage->{ChatterType},
\$ChatMessage->{MessageText}, \$ChatMessage->{SystemGenerated}, \$ChatMessage->{CreateTime},
],
);
if ( !$Success ) {
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'error',
Message =>
"System was unable to store data in article_data_otrs_chat table (ArticleID = $Param{ArticleID})!",
);
return;
}
}
}
return if !$Self->_MetaArticleUpdate(
%Param,
);
$ArticleObject->_ArticleCacheClear(
TicketID => $Param{TicketID},
);
$ArticleObject->ArticleSearchIndexBuild(
TicketID => $Param{TicketID},
ArticleID => $Param{ArticleID},
UserID => 1,
);
$Self->EventHandler(
Event => 'ArticleUpdate',
Data => {
TicketID => $Param{TicketID},
ArticleID => $Param{ArticleID},
},
UserID => $Param{UserID},
);
return 1;
}
=head2 ArticleDelete()
Delete article data.
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 all records related to the article.
return if !$Kernel::OM->Get('Kernel::System::DB')->Do(
SQL => '
DELETE FROM article_data_otrs_chat
WHERE article_id = ?
',
Bind => [ \$Param{ArticleID} ],
);
# Delete meta article and associated data, and clear cache.
return $Self->_MetaArticleDelete(
%Param,
);
}
=head2 BackendSearchableFieldsGet()
Get the definition of the searchable fields as a hash.
my %SearchableFields = $ArticleBackendObject->BackendSearchableFieldsGet();
Returns:
my %SearchableFields = (
'Chat_ChatterName' => {
Label => 'Chat Participant',
Key => 'Chat_ChatterName',
Type => 'Text',
Filterable => 0,
},
'Chat_MessageText' => {
Label => 'Message Text',
Key => 'Chat_MessageText',
Type => 'Text',
Filterable => 1,
},
);
=cut
sub BackendSearchableFieldsGet {
my ( $Self, %Param ) = @_;
my %SearchableFields;
return %SearchableFields if !$Kernel::OM->Get('Kernel::Config')->Get('ChatEngine::Active');
%SearchableFields = (
'Chat_ChatterName' => {
Label => Translatable('Chat Participant'),
Key => 'Chat_ChatterName',
Type => 'Text',
Filterable => 0,
},
'Chat_MessageText' => {
Label => Translatable('Chat Message Text'),
Key => 'Chat_MessageText',
Type => 'Text',
Filterable => 1,
},
);
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 fields with real names.
UserID => 123, # (required)
);
Returns:
my %ArticleSearchData = {
'ChatterName' => {
String => 'John Doe Jane Doe Joe Doe',
Key => 'ChatterName',
Type => 'Text',
Filterable => 0,
},
'ChatterType' => {
String => 'User User1 User2 User3',
Key => 'ChatterType',
Type => 'Text',
Filterable => 0,
},
'MessageText' => {
String => 'Chat message Second chat message Third chat message',
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 = (
'Chat_ChatterName' => 'ChatterName',
'Chat_MessageText' => 'MessageText',
);
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 = '';
for my $ChatMessageList ( @{ $ArticleData{ChatMessageList} } ) {
$IndexString .= ' ' . $ChatMessageList->{ $DataKeyMap{ $BackendSearchableFields{$FieldKey}->{Key} } };
}
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;
=cut
sub ArticleHasHTMLContent {
my ( $Self, %Param ) = @_;
return 1;
}
sub ArticleAttachmentIndex {
my ( $Self, %Param ) = @_;
return;
}
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

View File

@@ -0,0 +1,945 @@
# --
# 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::Email;
use strict;
use warnings;
use Mail::Address;
use Kernel::Language qw(Translatable);
use Kernel::System::VariableCheck qw(:all);
use parent 'Kernel::System::Ticket::Article::Backend::MIMEBase';
our @ObjectDependencies = (
'Kernel::Config',
'Kernel::System::CustomerUser',
'Kernel::System::DB',
'Kernel::System::Email',
'Kernel::System::HTMLUtils',
'Kernel::System::Log',
'Kernel::System::Main',
'Kernel::System::PostMaster::LoopProtection',
'Kernel::System::State',
'Kernel::System::TemplateGenerator',
'Kernel::System::Ticket',
'Kernel::System::Ticket::Article',
'Kernel::System::DateTime',
'Kernel::System::MailQueue',
);
=head1 NAME
Kernel::System::Ticket::Article::Backend::Email - backend class for email based articles
=head1 DESCRIPTION
This class provides functions to manipulate email based articles in the database.
Inherits from L<Kernel::System::Ticket::Article::Backend::MIMEBase>, please have a look there for its base API,
and below for the additional functions this backend provides.
=head1 PUBLIC INTERFACE
=cut
sub ChannelNameGet {
return 'Email';
}
=head2 ArticleGetByMessageID()
Return article data by supplied message ID.
my %Article = $ArticleBackendObject->ArticleGetByMessageID(
MessageID => '<13231231.1231231.32131231@example.com>', # (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.
);
=cut
sub ArticleGetByMessageID {
my ( $Self, %Param ) = @_;
if ( !$Param{MessageID} ) {
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'error',
Message => 'Need MessageID!',
);
return;
}
my $MD5 = $Kernel::OM->Get('Kernel::System::Main')->MD5sum( String => $Param{MessageID} );
my $DBObject = $Kernel::OM->Get('Kernel::System::DB');
# Get ticket and article ID from meta article table.
return if !$DBObject->Prepare(
SQL => '
SELECT sa.id, sa.ticket_id FROM article sa
LEFT JOIN article_data_mime sadm ON sa.id = sadm.article_id
WHERE sadm.a_message_id_md5 = ?
',
Bind => [ \$MD5 ],
Limit => 10,
);
my $Count = 0;
while ( my @Row = $DBObject->FetchrowArray() ) {
$Param{ArticleID} = $Row[0];
$Param{TicketID} = $Row[1];
$Count++;
}
# No reference found.
return if $Count == 0;
return if !$Param{TicketID} || !$Param{ArticleID};
# More than one reference found! That should not happen, since 'a message_id' should be unique!
if ( $Count > 1 ) {
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'notice',
Message =>
"The MessageID '$Param{MessageID}' is in your database more than one time! That should not happen, since 'a message_id' should be unique!",
);
return;
}
return $Self->ArticleGet(
%Param,
);
}
=head2 ArticleSend()
Send article via email and create article with attachments.
my $ArticleID = $ArticleBackendObject->ArticleSend(
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>', # required
To => 'Some Customer A <customer-a@example.com>', # required if both Cc and Bcc are not present
Cc => 'Some Customer B <customer-b@example.com>', # required if both To and Bcc are not present
Bcc => 'Some Customer C <customer-c@example.com>', # required if both To and Cc are not present
ReplyTo => 'Some Customer B <customer-b@example.com>', # not required, is possible to use 'Reply-To' instead
Subject => 'some short description', # required
Body => 'the message text', # required
InReplyTo => '<asdasdasd.12@example.com>', # not required but useful
References => '<asdasdasd.1@example.com> <asdasdasd.12@example.com>', # not required but useful
Charset => 'iso-8859-15'
MimeType => 'text/plain',
Loop => 0, # 1|0 used for bulk emails
Attachment => [
{
Content => $Content,
ContentType => $ContentType,
Filename => 'lala.txt',
},
{
Content => $Content,
ContentType => $ContentType,
Filename => 'lala1.txt',
},
],
EmailSecurity => {
Backend => 'PGP', # PGP or SMIME
Method => 'Detached', # Optional Detached or Inline (defaults to Detached)
SignKey => '81877F5E', # Optional
EncryptKeys => [ '81877F5E', '3b630c80' ], # Optional
}
HistoryType => 'OwnerUpdate', # Move|AddNote|PriorityUpdate|WebRequestCustomer|...
HistoryComment => 'Some free text!',
NoAgentNotify => 0, # if you don't want to send agent notifications
);
my $ArticleID = $ArticleBackendObject->ArticleSend( (Backwards compatibility)
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>', # required
To => 'Some Customer A <customer-a@example.com>', # required if both Cc and Bcc are not present
Cc => 'Some Customer B <customer-b@example.com>', # required if both To and Bcc are not present
Bcc => 'Some Customer C <customer-c@example.com>', # required if both To and Cc are not present
ReplyTo => 'Some Customer B <customer-b@example.com>', # not required, is possible to use 'Reply-To' instead
Subject => 'some short description', # required
Body => 'the message text', # required
InReplyTo => '<asdasdasd.12@example.com>', # not required but useful
References => '<asdasdasd.1@example.com> <asdasdasd.12@example.com>', # not required but useful
Charset => 'iso-8859-15'
MimeType => 'text/plain',
Loop => 0, # 1|0 used for bulk emails
Attachment => [
{
Content => $Content,
ContentType => $ContentType,
Filename => 'lala.txt',
},
{
Content => $Content,
ContentType => $ContentType,
Filename => 'lala1.txt',
},
],
Sign => {
Type => 'PGP',
SubType => 'Inline|Detached',
Key => '81877F5E',
Type => 'SMIME',
Key => '3b630c80',
},
Crypt => {
Type => 'PGP',
SubType => 'Inline|Detached',
Key => '81877F5E',
Type => 'SMIME',
Key => '3b630c80',
},
HistoryType => 'OwnerUpdate', # Move|AddNote|PriorityUpdate|WebRequestCustomer|...
HistoryComment => 'Some free text!',
NoAgentNotify => 0, # if you don't want to send agent notifications
);
Events:
ArticleSend
=cut
sub ArticleSend {
my ( $Self, %Param ) = @_;
my $ToOrig = $Param{To} || '';
my $Loop = $Param{Loop} || 0;
my $HistoryType = $Param{HistoryType} || 'SendAnswer';
my $ArticleObject = $Kernel::OM->Get('Kernel::System::Ticket::Article');
my $DateTimeObject = $Kernel::OM->Create('Kernel::System::DateTime');
# Lookup if no ID is passed.
if ( $Param{SenderType} && !$Param{SenderTypeID} ) {
$Param{SenderTypeID} = $ArticleObject->ArticleSenderTypeLookup( SenderType => $Param{SenderType} );
}
for my $Needed (qw(TicketID UserID SenderTypeID From Body Charset MimeType)) {
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;
}
# Map ReplyTo into Reply-To if present.
if ( $Param{ReplyTo} ) {
$Param{'Reply-To'} = $Param{ReplyTo};
}
# Clean up body string.
$Param{Body} =~ s/(\r\n|\n\r)/\n/g;
$Param{Body} =~ s/\r/\n/g;
# initialize parameter for attachments, so that the content pushed into that ref from
# EmbeddedImagesExtract will stay available
if ( !$Param{Attachment} ) {
$Param{Attachment} = [];
}
# check for base64 images in body and process them
$Kernel::OM->Get('Kernel::System::HTMLUtils')->EmbeddedImagesExtract(
DocumentRef => \$Param{Body},
AttachmentsRef => $Param{Attachment},
);
# create article
my $Time = $DateTimeObject->ToEpoch();
my $Random = rand 999999;
my $FQDN = $Kernel::OM->Get('Kernel::Config')->Get('FQDN');
my $MessageID = "<$Time.$Random\@$FQDN>";
my $ArticleID = $Self->ArticleCreate(
%Param,
MessageID => $MessageID,
);
return if !$ArticleID;
# Send the mail
my $Result = $Kernel::OM->Get('Kernel::System::Email')->Send(
%Param,
ArticleID => $ArticleID,
'Message-ID' => $MessageID,
);
# return if mail wasn't sent
if ( !$Result->{Success} ) {
$Kernel::OM->Get('Kernel::System::Log')->Log(
Message => "Impossible to send message to: $Param{'To'} .",
Priority => 'error',
);
return;
}
# write article to file system
my $Plain = $Self->ArticleWritePlain(
ArticleID => $ArticleID,
Email => sprintf( "%s\n%s", $Result->{Data}->{Header}, $Result->{Data}->{Body} ),
UserID => $Param{UserID},
);
return if !$Plain;
# log
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'info',
Message => sprintf(
"Queued email to '%s' from '%s'. HistoryType => %s, Subject => %s;",
$Param{To},
$Param{From},
$HistoryType,
$Param{Subject},
),
);
# event
$Self->EventHandler(
Event => 'ArticleSend',
Data => {
TicketID => $Param{TicketID},
ArticleID => $ArticleID,
},
UserID => $Param{UserID},
);
return $ArticleID;
}
=head2 ArticleBounce()
Bounce an article.
my $Success = $ArticleBackendObject->ArticleBounce(
From => 'some@example.com',
To => 'webmaster@example.com',
TicketID => 123,
ArticleID => 123,
UserID => 123,
);
Events:
ArticleBounce
=cut
sub ArticleBounce {
my ( $Self, %Param ) = @_;
for my $Item (qw(TicketID ArticleID From To UserID)) {
if ( !$Param{$Item} ) {
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'error',
Message => "Need $Item!",
);
return;
}
}
my $DateTimeObject = $Kernel::OM->Create('Kernel::System::DateTime');
# create message id
my $Time = $DateTimeObject->ToEpoch();
my $Random = rand 999999;
my $FQDN = $Kernel::OM->Get('Kernel::Config')->Get('FQDN');
my $NewMessageID = "<$Time.$Random.0\@$FQDN>";
my $Email = $Self->ArticlePlain( ArticleID => $Param{ArticleID} );
# check if plain email exists
if ( !$Email ) {
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'error',
Message => "No such plain article for ArticleID ($Param{ArticleID})!",
);
return;
}
# pipe all into sendmail
my $BounceSent = $Kernel::OM->Get('Kernel::System::Email')->Bounce(
'Message-ID' => $NewMessageID,
From => $Param{From},
To => $Param{To},
Email => $Email,
);
return if !$BounceSent->{Success};
# write history
my $HistoryType = $Param{HistoryType} || 'Bounce';
$Kernel::OM->Get('Kernel::System::Ticket')->HistoryAdd(
TicketID => $Param{TicketID},
ArticleID => $Param{ArticleID},
HistoryType => $HistoryType,
Name => "\%\%$Param{To}",
CreateUserID => $Param{UserID},
);
# event
$Self->EventHandler(
Event => 'ArticleBounce',
Data => {
TicketID => $Param{TicketID},
ArticleID => $Param{ArticleID},
},
UserID => $Param{UserID},
);
return 1;
}
=head2 SendAutoResponse()
Send an auto response to a customer via email.
my $ArticleID = $ArticleBackendObject->SendAutoResponse(
TicketID => 123,
AutoResponseType => 'auto reply',
OrigHeader => {
From => 'some@example.com',
Subject => 'For the message!',
},
UserID => 123,
);
Events:
ArticleAutoResponse
=cut
sub SendAutoResponse {
my ( $Self, %Param ) = @_;
# check needed stuff
for my $Item (qw(TicketID UserID OrigHeader AutoResponseType)) {
if ( !$Param{$Item} ) {
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'error',
Message => "Need $Item!",
);
return;
}
}
# return if no notification is active
return 1 if $Self->{SendNoNotification};
# get orig email header
my %OrigHeader = %{ $Param{OrigHeader} };
my $TicketObject = $Kernel::OM->Get('Kernel::System::Ticket');
# get ticket
my %Ticket = $TicketObject->TicketGet(
TicketID => $Param{TicketID},
DynamicFields => 0, # not needed here, TemplateGenerator will fetch the ticket on its own
);
# get auto default responses
my %AutoResponse = $Kernel::OM->Get('Kernel::System::TemplateGenerator')->AutoResponse(
TicketID => $Param{TicketID},
AutoResponseType => $Param{AutoResponseType},
OrigHeader => $Param{OrigHeader},
UserID => $Param{UserID},
);
# return if no valid auto response exists
return if !$AutoResponse{Text};
return if !$AutoResponse{SenderRealname};
return if !$AutoResponse{SenderAddress};
# send if notification should be sent (not for closed tickets)!?
my %State = $Kernel::OM->Get('Kernel::System::State')->StateGet( ID => $Ticket{StateID} );
if (
$Param{AutoResponseType} eq 'auto reply'
&& ( $State{TypeName} eq 'closed' || $State{TypeName} eq 'removed' )
)
{
# add history row
$TicketObject->HistoryAdd(
TicketID => $Param{TicketID},
HistoryType => 'Misc',
Name => "Sent no auto response or agent notification because ticket is "
. "state-type '$State{TypeName}'!",
CreateUserID => $Param{UserID},
);
# return
return;
}
# log that no auto response was sent!
if ( $OrigHeader{'X-OTRS-Loop'} && $OrigHeader{'X-OTRS-Loop'} !~ /^(false|no)$/i ) {
# add history row
$TicketObject->HistoryAdd(
TicketID => $Param{TicketID},
HistoryType => 'Misc',
Name => "Sent no auto-response because the sender doesn't want "
. "an auto-response (e. g. loop or precedence header)",
CreateUserID => $Param{UserID},
);
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'info',
Message => "Sent no '$Param{AutoResponseType}' for Ticket ["
. "$Ticket{TicketNumber}] ($OrigHeader{From}) because the "
. "sender doesn't want an auto-response (e. g. loop or precedence header)"
);
return;
}
# check reply to for auto response recipient
if ( $OrigHeader{ReplyTo} ) {
$OrigHeader{From} = $OrigHeader{ReplyTo};
}
# get loop protection object
my $LoopProtectionObject = $Kernel::OM->Get('Kernel::System::PostMaster::LoopProtection');
# create email parser object
my $EmailParser = Kernel::System::EmailParser->new(
Mode => 'Standalone',
);
my @AutoReplyAddresses;
my @Addresses = $EmailParser->SplitAddressLine( Line => $OrigHeader{From} );
ADDRESS:
for my $Address (@Addresses) {
my $Email = $EmailParser->GetEmailAddress( Email => $Address );
if ( !$Email ) {
# add it to ticket history
$TicketObject->HistoryAdd(
TicketID => $Param{TicketID},
CreateUserID => $Param{UserID},
HistoryType => 'Misc',
Name => "Sent no auto response to '$Address' - no valid email address.",
);
# log
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'notice',
Message => "Sent no auto response to '$Address' because of invalid address.",
);
next ADDRESS;
}
if ( !$LoopProtectionObject->Check( To => $Email ) ) {
# add history row
$TicketObject->HistoryAdd(
TicketID => $Param{TicketID},
HistoryType => 'LoopProtection',
Name => "\%\%$Email",
CreateUserID => $Param{UserID},
);
# log
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'notice',
Message => "Sent no '$Param{AutoResponseType}' for Ticket ["
. "$Ticket{TicketNumber}] ($Email) because of loop protection."
);
next ADDRESS;
}
else {
# increase loop count
return if !$LoopProtectionObject->SendEmail( To => $Email );
}
# check if sender is e. g. MAILER-DAEMON or Postmaster
my $NoAutoRegExp = $Kernel::OM->Get('Kernel::Config')->Get('SendNoAutoResponseRegExp');
if ( $Email =~ /$NoAutoRegExp/i ) {
# add it to ticket history
$TicketObject->HistoryAdd(
TicketID => $Param{TicketID},
CreateUserID => $Param{UserID},
HistoryType => 'Misc',
Name => "Sent no auto response to '$Email', SendNoAutoResponseRegExp matched.",
);
# log
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'info',
Message => "Sent no auto response to '$Email' because config"
. " option SendNoAutoResponseRegExp (/$NoAutoRegExp/i) matched.",
);
next ADDRESS;
}
push @AutoReplyAddresses, $Address;
}
my $AutoReplyAddresses = join( ', ', @AutoReplyAddresses );
my $Cc;
# also send CC to customer user if customer user id is used and addresses do not match
if ( $Ticket{CustomerUserID} ) {
my %CustomerUser = $Kernel::OM->Get('Kernel::System::CustomerUser')->CustomerUserDataGet(
User => $Ticket{CustomerUserID},
);
if (
$CustomerUser{UserEmail}
&& $OrigHeader{From} !~ /\Q$CustomerUser{UserEmail}\E/i
&& $Param{IsVisibleForCustomer}
)
{
$Cc = $CustomerUser{UserEmail};
}
}
# get history type
my $HistoryType;
if ( $Param{AutoResponseType} =~ /^auto follow up$/i ) {
$HistoryType = 'SendAutoFollowUp';
}
elsif ( $Param{AutoResponseType} =~ /^auto reply$/i ) {
$HistoryType = 'SendAutoReply';
}
elsif ( $Param{AutoResponseType} =~ /^auto reply\/new ticket$/i ) {
$HistoryType = 'SendAutoReply';
}
elsif ( $Param{AutoResponseType} =~ /^auto reject$/i ) {
$HistoryType = 'SendAutoReject';
}
else {
$HistoryType = 'Misc';
}
if ( !@AutoReplyAddresses && !$Cc ) {
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'info',
Message => "No auto response addresses for Ticket [$Ticket{TicketNumber}]"
. " (TicketID=$Param{TicketID})."
);
return;
}
# Format sender realname and address because it maybe contains comma or other special symbols (see bug#13130).
my $From = Mail::Address->new( $AutoResponse{SenderRealname} // '', $AutoResponse{SenderAddress} );
# send email
my $ArticleID = $Self->ArticleSend(
IsVisibleForCustomer => 1,
SenderType => 'system',
TicketID => $Param{TicketID},
HistoryType => $HistoryType,
HistoryComment => "\%\%$AutoReplyAddresses",
From => $From->format(),
To => $AutoReplyAddresses,
Cc => $Cc,
Charset => 'utf-8',
MimeType => $AutoResponse{ContentType},
Subject => $AutoResponse{Subject},
Body => $AutoResponse{Text},
InReplyTo => $OrigHeader{'Message-ID'},
Loop => 1,
UserID => $Param{UserID},
);
# log
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'info',
Message => "Sent auto response ($HistoryType) for Ticket [$Ticket{TicketNumber}]"
. " (TicketID=$Param{TicketID}, ArticleID=$ArticleID) to '$AutoReplyAddresses'."
);
# event
$Self->EventHandler(
Event => 'ArticleAutoResponse',
Data => {
TicketID => $Param{TicketID},
},
UserID => $Param{UserID},
);
return 1;
}
=head2 ArticleTransmissionStatus()
Get the transmission status for one article.
my $TransmissionStatus = $ArticleBackendObject->ArticleTransmissionStatus(
ArticleID => 123, # required
);
This returns something like:
$TransmissionStatus = {
ArticleID => 123,
MessageID => 456,
Message => 'Descriptive message of last communication', # only in case of failed status
CreateTime => '2017-01-01 12:34:56',
Status => [Processing|Failed],
Attempts => 1, # only in case of processing status
DueTime => '2017-01-02 12:34:56', # only in case of processing status
}
=cut
sub ArticleTransmissionStatus {
my ( $Self, %Param ) = @_;
if ( !$Param{ArticleID} ) {
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'error',
Message => 'Need ArticleID',
);
return;
}
my $Result = $Self->ArticleGetTransmissionError( %Param, );
return $Result if $Result && %{$Result};
my $DBObject = $Kernel::OM->Get('Kernel::System::DB');
return if !$DBObject->Prepare(
SQL =>
'SELECT article_id, create_time, attempts, due_time FROM mail_queue WHERE article_id = ?',
Bind => [ \$Param{ArticleID} ],
);
if ( my @Row = $DBObject->FetchrowArray() ) {
return {
ArticleID => $Row[0],
CreateTime => $Row[1],
Attempts => $Row[2],
DueTime => $Row[3],
Status => 'Processing',
};
}
return;
}
=head2 ArticleCreateTransmissionError()
Creates a Transmission Error entry for one article.
my $Success = $ArticleBackendObject->ArticleCreateTransmissionError(
ArticleID => 123, # Required
MessageID => 456, # Optional
Message => '', # Optional
);
=cut
sub ArticleCreateTransmissionError {
my ( $Self, %Param ) = @_;
# check needed stuff
for my $Field (qw{ArticleID}) {
if ( !$Param{$Field} ) {
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'error',
Message => "Need ${Field}!"
);
return;
}
}
my $SQL = 'INSERT INTO article_data_mime_send_error(';
my @Fields;
my @Bind;
my %MapDB = (
ArticleID => 'article_id',
MessageID => 'message_id',
Message => 'log_message',
);
my @PlaceHolder;
for my $Field ( sort keys %MapDB ) {
if ( IsStringWithData( $Param{$Field} ) ) {
push @Fields, $MapDB{$Field};
push @PlaceHolder, '?';
push @Bind, \$Param{$Field};
}
}
push @Fields, 'create_time';
$SQL .= join( ', ', @Fields )
. ') values(';
$SQL .= join ', ', @PlaceHolder;
$SQL .= ', current_timestamp)';
# get database object
my $DBObject = $Kernel::OM->Get('Kernel::System::DB');
return if !$DBObject->Do(
SQL => $SQL,
Bind => \@Bind,
);
return 1;
}
=head2 ArticleGetTransmissionError()
Get the Transmission Error entry for a given article.
my %TransmissionError = $ArticleBackendObject->ArticleGetTransmissionError(
ArticleID => 123, # Required
);
Returns:
{
ArticleID => 123,
MessageID => 456,
Message => 'Descriptive message of last communication',
CreateTime => '2017-01-01 01:02:03',
Status => 'Failed',
}
or undef in case of failure to retrive a record from the database.
=cut
sub ArticleGetTransmissionError {
my ( $Self, %Param ) = @_;
# check needed stuff
if ( !$Param{ArticleID} ) {
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'error',
Message => "Need ArticleID!"
);
return;
}
# prepare/filter ArticleID
$Param{ArticleID} = quotemeta( $Param{ArticleID} );
$Param{ArticleID} =~ s/\0//g;
# get database object
my $DBObject = $Kernel::OM->Get('Kernel::System::DB');
# can't open article, try database
return if !$DBObject->Prepare(
SQL =>
'SELECT article_id, message_id, log_message, create_time FROM article_data_mime_send_error WHERE article_id = ?',
Bind => [ \$Param{ArticleID} ],
);
my @Row = $DBObject->FetchrowArray();
if (@Row) {
return {
'ArticleID' => $Row[0],
'MessageID' => $Row[1],
'Message' => $Row[2],
'CreateTime' => $Row[3],
'Status' => 'Failed',
};
}
return;
}
=head2 ArticleUpdateTransmissionError()
Updates the Transmission Error.
my $Result = $ArticleBackendObject->ArticleUpdateTransmissionError(
ArticleID => 123, # Required
MessageID => 456, # Optional
Message => 'Short descriptive message', # Optional
);
Returns 1 on Success, undef on failure.
=cut
sub ArticleUpdateTransmissionError {
my ( $Self, %Param ) = @_;
# check needed stuff
if ( !$Param{ArticleID} ) {
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'error',
Message => "Need ArticleID!"
);
return;
}
my @FieldsToUpdate;
my @Bind;
if ( IsStringWithData( $Param{MessageID} ) ) {
push @FieldsToUpdate, 'message_id = ?';
push @Bind, \$Param{MessageID};
}
if ( IsStringWithData( $Param{Message} ) ) {
push @FieldsToUpdate, 'log_message = ?';
push @Bind, \$Param{Message};
}
return if !scalar @Bind;
my $SQL = 'UPDATE article_data_mime_send_error SET '
. join( ', ', @FieldsToUpdate )
. ' WHERE article_id = ?';
push @Bind, \$Param{ArticleID};
# get database object
my $DBObject = $Kernel::OM->Get('Kernel::System::DB');
# db update
return if !$Kernel::OM->Get('Kernel::System::DB')->Do(
SQL => $SQL,
Bind => \@Bind,
);
return 1;
}
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

View File

@@ -0,0 +1,44 @@
# --
# 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::Internal;
use strict;
use warnings;
use parent 'Kernel::System::Ticket::Article::Backend::MIMEBase';
our @ObjectDependencies = ();
=head1 NAME
Kernel::System::Ticket::Article::Backend::Internal - backend class for internal articles
=head1 DESCRIPTION
This class provides functions to manipulate internal articles in the database.
Inherits from L<Kernel::System::Ticket::Article::Backend::MIMEBase>, please have a look there for its API.
=cut
sub ChannelNameGet {
return 'Internal';
}
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

View File

@@ -0,0 +1,228 @@
# --
# 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::Invalid;
use strict;
use warnings;
use parent qw(
Kernel::System::EventHandler
Kernel::System::Ticket::Article::Backend::Base
);
our @ObjectDependencies = (
'Kernel::System::CommunicationChannel',
'Kernel::System::DB',
'Kernel::System::Log',
'Kernel::System::Ticket::Article',
);
=head1 NAME
Kernel::System::Ticket::Article::Backend::Invalid - backend for articles that have an unknown communication channel
=head1 DESCRIPTION
This is a fallback backend which exists for two purposes: first, to make sure that you can always chain-call on
C<BackendForArticle>, even if the article has a communication channel that is missing in the system. And second,
to make it possible to delete such articles.
=cut
=head1 PUBLIC INTERFACE
=cut
sub ChannelNameGet {
return 'Invalid';
}
=head2 ArticleCreate()
Dummy function. The invalid backend will not create any articles.
=cut
sub ArticleCreate {
return;
}
=head2 ArticleUpdate()
Dummy function. The invalid backend will not update any articles.
=cut
sub ArticleUpdate {
return;
}
=head2 ArticleGet()
Returns article meta data as also returned by L<Kernel::System::Ticket::Article::ArticleList()>.
my %Article = $ArticleBackendObject->ArticleGet(
TicketID => 123,
ArticleID => 123,
DynamicFields => 1,
);
=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;
}
}
my %MetaArticle = $Self->_MetaArticleGet(%Param);
my %ArticleSenderTypeList = $Kernel::OM->Get('Kernel::System::Ticket::Article')->ArticleSenderTypeList();
# Include sender type lookup.
$MetaArticle{SenderType} = $ArticleSenderTypeList{ $MetaArticle{SenderTypeID} };
return %MetaArticle;
}
=head2 ArticleDelete()
Delete an article. Override this method in your class.
my $Success = $ArticleBackendObject->ArticleDelete(
TicketID => 123,
ArticleID => 123,
UserID => 123,
);
This method uses data stored in the communication channel entry to determine if there are any database tables that
have foreign keys to the C<article> table. Depending data will first be deleted, then the main article entry.
=cut
sub ArticleDelete {
my ( $Self, %Param ) = @_;
for my $Needed (qw(TicketID ArticleID UserID)) {
if ( !defined $Param{$Needed} ) {
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'error',
Message => "Need $Needed!"
);
return;
}
}
# Get CommunicationChannel data of article.
if ( !$Param{CommunicationChannelID} ) {
my %MetaArticle = $Self->_MetaArticleGet(%Param);
$Param{CommunicationChannelID} = $MetaArticle{CommunicationChannelID};
}
if ( !$Param{CommunicationChannelID} ) {
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'error',
Message => "Could not determine CommunicationChannelID for Article $Param{ArticleID}!"
);
return;
}
# Get table dependency information from CommunicationChannel.
my %CommunicationChannel = $Kernel::OM->Get('Kernel::System::CommunicationChannel')->ChannelGet(
ChannelID => $Param{CommunicationChannelID},
);
if ( !%CommunicationChannel ) {
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'error',
Message => "Could not load CommunicationChannel data for Article $Param{ArticleID}!"
);
return;
}
my %ChannelData;
if ( ref $CommunicationChannel{ChannelData} eq 'HASH' ) {
%ChannelData = %{ $CommunicationChannel{ChannelData} };
}
my @ArticleDataTables = @{ $ChannelData{ArticleDataTables} // [] };
my $ArticleDataArticleIDField = $ChannelData{ArticleDataArticleIDField};
# Delete depending Article data.
if ( @ArticleDataTables && $ArticleDataArticleIDField ) {
my $DBObject = $Kernel::OM->Get('Kernel::System::DB');
for my $ArticleDataTable (@ArticleDataTables) {
$DBObject->Do(
SQL => "DELETE FROM $ArticleDataTable WHERE $ArticleDataArticleIDField = ?",
Bind => [ \$Param{ArticleID} ],
);
}
}
# Delete main article data.
return $Self->_MetaArticleDelete(%Param);
}
=head2 ArticleSearchableContentGet()
Dummy function. The invalid backend will not return any searchable data.
=cut
sub ArticleSearchableContentGet {
return;
}
=head2 BackendSearchableFieldsGet()
Dummy function. The invalid backend will not return any searchable fields.
=cut
sub BackendSearchableFieldsGet {
return;
}
=head2 ArticleHasHTMLContent()
Dummy function. The invalid backend will always return 1.
=cut
sub ArticleHasHTMLContent {
return 1;
}
=head2 ArticleAttachmentIndex()
Dummy function. The invalid backend will not return any attachments.
=cut
sub ArticleAttachmentIndex {
return;
}
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

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,511 @@
# --
# 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::ArticleStorageDB;
use strict;
use warnings;
use MIME::Base64;
use MIME::Words qw(:all);
use parent qw(Kernel::System::Ticket::Article::Backend::MIMEBase::Base);
use Kernel::System::VariableCheck qw(:all);
our @ObjectDependencies = (
'Kernel::Config',
'Kernel::System::DB',
'Kernel::System::DynamicFieldValue',
'Kernel::System::Encode',
'Kernel::System::Log',
'Kernel::System::Main',
'Kernel::System::Ticket::Article::Backend::MIMEBase::ArticleStorageFS',
);
=head1 NAME
Kernel::System::Ticket::Article::Backend::MIMEBase::ArticleStorageDB - DB based ticket article storage interface
=head1 DESCRIPTION
This class provides functions to manipulate ticket articles in the database.
The methods are currently documented in L<Kernel::System::Ticket::Article::Backend::MIMEBase>.
Inherits from L<Kernel::System::Ticket::Article::Backend::MIMEBase::Base>.
See also L<Kernel::System::Ticket::Article::Backend::MIMEBase::ArticleStorageFS>.
=cut
sub ArticleDelete {
my ( $Self, %Param ) = @_;
# check needed stuff
for my $Item (qw(ArticleID UserID)) {
if ( !$Param{$Item} ) {
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'error',
Message => "Need $Item!"
);
return;
}
}
# delete attachments
$Self->ArticleDeleteAttachment(
ArticleID => $Param{ArticleID},
UserID => $Param{UserID},
);
# delete plain message
$Self->ArticleDeletePlain(
ArticleID => $Param{ArticleID},
UserID => $Param{UserID},
);
# Delete storage directory in case there are leftovers in the FS.
$Self->_ArticleDeleteDirectory(
ArticleID => $Param{ArticleID},
UserID => $Param{UserID},
);
return 1;
}
sub ArticleDeletePlain {
my ( $Self, %Param ) = @_;
# check needed stuff
for my $Item (qw(ArticleID UserID)) {
if ( !$Param{$Item} ) {
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'error',
Message => "Need $Item!"
);
return;
}
}
# delete attachments
return if !$Kernel::OM->Get('Kernel::System::DB')->Do(
SQL => 'DELETE FROM article_data_mime_plain WHERE article_id = ?',
Bind => [ \$Param{ArticleID} ],
);
# return if we only need to check one backend
return 1 if !$Self->{CheckAllBackends};
# return of only delete in my backend
return 1 if $Param{OnlyMyBackend};
return $Kernel::OM->Get('Kernel::System::Ticket::Article::Backend::MIMEBase::ArticleStorageFS')->ArticleDeletePlain(
%Param,
OnlyMyBackend => 1,
);
}
sub ArticleDeleteAttachment {
my ( $Self, %Param ) = @_;
# check needed stuff
for my $Item (qw(ArticleID UserID)) {
if ( !$Param{$Item} ) {
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'error',
Message => "Need $Item!"
);
return;
}
}
# delete attachments
return if !$Kernel::OM->Get('Kernel::System::DB')->Do(
SQL => 'DELETE FROM article_data_mime_attachment WHERE article_id = ?',
Bind => [ \$Param{ArticleID} ],
);
# return if we only need to check one backend
return 1 if !$Self->{CheckAllBackends};
# return if only delete in my backend
return 1 if $Param{OnlyMyBackend};
return $Kernel::OM->Get('Kernel::System::Ticket::Article::Backend::MIMEBase::ArticleStorageFS')
->ArticleDeleteAttachment(
%Param,
OnlyMyBackend => 1,
);
}
sub ArticleWritePlain {
my ( $Self, %Param ) = @_;
# check needed stuff
for my $Item (qw(ArticleID Email UserID)) {
if ( !$Param{$Item} ) {
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'error',
Message => "Need $Item!"
);
return;
}
}
# get database object
my $DBObject = $Kernel::OM->Get('Kernel::System::DB');
# encode attachment if it's a postgresql backend!!!
if ( !$DBObject->GetDatabaseFunction('DirectBlob') ) {
$Kernel::OM->Get('Kernel::System::Encode')->EncodeOutput( \$Param{Email} );
$Param{Email} = encode_base64( $Param{Email} );
}
# write article to db 1:1
return if !$DBObject->Do(
SQL => 'INSERT INTO article_data_mime_plain '
. ' (article_id, body, create_time, create_by, change_time, change_by) '
. ' VALUES (?, ?, current_timestamp, ?, current_timestamp, ?)',
Bind => [ \$Param{ArticleID}, \$Param{Email}, \$Param{UserID}, \$Param{UserID} ],
);
return 1;
}
sub ArticleWriteAttachment {
my ( $Self, %Param ) = @_;
# check needed stuff
for my $Item (qw(Filename ContentType ArticleID UserID)) {
if ( !IsStringWithData( $Param{$Item} ) ) {
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'error',
Message => "Need $Item!"
);
return;
}
}
$Param{Filename} = $Kernel::OM->Get('Kernel::System::Main')->FilenameCleanUp(
Filename => $Param{Filename},
Type => 'Local',
NoReplace => 1,
);
my $NewFileName = $Param{Filename};
my %UsedFile;
my %Index = $Self->ArticleAttachmentIndex(
ArticleID => $Param{ArticleID},
);
for my $IndexFile ( sort keys %Index ) {
$UsedFile{ $Index{$IndexFile}->{Filename} } = 1;
}
for ( my $i = 1; $i <= 50; $i++ ) {
if ( exists $UsedFile{$NewFileName} ) {
if ( $Param{Filename} =~ /^(.*)\.(.+?)$/ ) {
$NewFileName = "$1-$i.$2";
}
else {
$NewFileName = "$Param{Filename}-$i";
}
}
}
# get file name
$Param{Filename} = $NewFileName;
# get attachment size
$Param{Filesize} = bytes::length( $Param{Content} );
# get database object
my $DBObject = $Kernel::OM->Get('Kernel::System::DB');
# encode attachment if it's a postgresql backend!!!
if ( !$DBObject->GetDatabaseFunction('DirectBlob') ) {
$Kernel::OM->Get('Kernel::System::Encode')->EncodeOutput( \$Param{Content} );
$Param{Content} = encode_base64( $Param{Content} );
}
# set content id in angle brackets
if ( $Param{ContentID} ) {
$Param{ContentID} =~ s/^([^<].*[^>])$/<$1>/;
}
my $Disposition;
my $Filename;
if ( $Param{Disposition} ) {
( $Disposition, $Filename ) = split ';', $Param{Disposition};
}
$Disposition //= '';
# write attachment to db
return if !$DBObject->Do(
SQL => '
INSERT INTO article_data_mime_attachment (article_id, filename, content_type, content_size,
content, content_id, content_alternative, disposition, create_time, create_by,
change_time, change_by)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, current_timestamp, ?, current_timestamp, ?)',
Bind => [
\$Param{ArticleID}, \$Param{Filename}, \$Param{ContentType}, \$Param{Filesize},
\$Param{Content}, \$Param{ContentID}, \$Param{ContentAlternative},
\$Disposition, \$Param{UserID}, \$Param{UserID},
],
);
return 1;
}
sub ArticlePlain {
my ( $Self, %Param ) = @_;
# check needed stuff
if ( !$Param{ArticleID} ) {
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'error',
Message => "Need ArticleID!"
);
return;
}
# prepare/filter ArticleID
$Param{ArticleID} = quotemeta( $Param{ArticleID} );
$Param{ArticleID} =~ s/\0//g;
# get database object
my $DBObject = $Kernel::OM->Get('Kernel::System::DB');
# can't open article, try database
return if !$DBObject->Prepare(
SQL => 'SELECT body FROM article_data_mime_plain WHERE article_id = ?',
Bind => [ \$Param{ArticleID} ],
Encode => [0],
);
my $Data;
while ( my @Row = $DBObject->FetchrowArray() ) {
# decode attachment if it's e. g. a postgresql backend!!!
if ( !$DBObject->GetDatabaseFunction('DirectBlob') && $Row[0] !~ m/ / ) {
$Data = decode_base64( $Row[0] );
}
else {
$Data = $Row[0];
}
}
return $Data if defined $Data;
# return if we only need to check one backend
return if !$Self->{CheckAllBackends};
# return of only delete in my backend
return if $Param{OnlyMyBackend};
return $Kernel::OM->Get('Kernel::System::Ticket::Article::Backend::MIMEBase::ArticleStorageFS')->ArticlePlain(
%Param,
OnlyMyBackend => 1,
);
}
sub ArticleAttachmentIndexRaw {
my ( $Self, %Param ) = @_;
# check needed stuff
if ( !$Param{ArticleID} ) {
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'error',
Message => 'Need ArticleID!'
);
return;
}
my %Index;
my $Counter = 0;
# get database object
my $DBObject = $Kernel::OM->Get('Kernel::System::DB');
# try database
return if !$DBObject->Prepare(
SQL => '
SELECT filename, content_type, content_size, content_id, content_alternative,
disposition
FROM article_data_mime_attachment
WHERE article_id = ?
ORDER BY filename, id',
Bind => [ \$Param{ArticleID} ],
);
while ( my @Row = $DBObject->FetchrowArray() ) {
my $Disposition = $Row[5];
if ( !$Disposition ) {
# if no content disposition is set images with content id should be inline
if ( $Row[3] && $Row[1] =~ m{image}i ) {
$Disposition = 'inline';
}
# converted article body should be inline
elsif ( $Row[0] =~ m{file-[12]} ) {
$Disposition = 'inline';
}
# all others including attachments with content id that are not images
# should NOT be inline
else {
$Disposition = 'attachment';
}
}
# add the info the the hash
$Counter++;
$Index{$Counter} = {
Filename => $Row[0],
FilesizeRaw => $Row[2] || 0,
ContentType => $Row[1],
ContentID => $Row[3] || '',
ContentAlternative => $Row[4] || '',
Disposition => $Disposition,
};
}
# return existing index
return %Index if %Index;
# return if we only need to check one backend
return if !$Self->{CheckAllBackends};
# return if only delete in my backend
return if $Param{OnlyMyBackend};
return $Kernel::OM->Get('Kernel::System::Ticket::Article::Backend::MIMEBase::ArticleStorageFS')
->ArticleAttachmentIndexRaw(
%Param,
OnlyMyBackend => 1,
);
}
sub ArticleAttachment {
my ( $Self, %Param ) = @_;
# check needed stuff
for my $Item (qw(ArticleID FileID)) {
if ( !$Param{$Item} ) {
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'error',
Message => "Need $Item!"
);
return;
}
}
# prepare/filter ArticleID
$Param{ArticleID} = quotemeta( $Param{ArticleID} );
$Param{ArticleID} =~ s/\0//g;
# get attachment index
my %Index = $Self->ArticleAttachmentIndex(
ArticleID => $Param{ArticleID},
);
return if !$Index{ $Param{FileID} };
my %Data = %{ $Index{ $Param{FileID} } };
# get database object
my $DBObject = $Kernel::OM->Get('Kernel::System::DB');
# try database
return if !$DBObject->Prepare(
SQL => '
SELECT id
FROM article_data_mime_attachment
WHERE article_id = ?
ORDER BY filename, id',
Bind => [ \$Param{ArticleID} ],
Limit => $Param{FileID},
);
my $AttachmentID;
while ( my @Row = $DBObject->FetchrowArray() ) {
$AttachmentID = $Row[0];
}
return if !$DBObject->Prepare(
SQL => '
SELECT content_type, content, content_id, content_alternative, disposition, filename
FROM article_data_mime_attachment
WHERE id = ?',
Bind => [ \$AttachmentID ],
Encode => [ 1, 0, 0, 0, 1, 1 ],
);
while ( my @Row = $DBObject->FetchrowArray() ) {
$Data{ContentType} = $Row[0];
# decode attachment if it's e. g. a postgresql backend!!!
if ( !$DBObject->GetDatabaseFunction('DirectBlob') ) {
$Data{Content} = decode_base64( $Row[1] );
}
else {
$Data{Content} = $Row[1];
}
$Data{ContentID} = $Row[2] || '';
$Data{ContentAlternative} = $Row[3] || '';
$Data{Disposition} = $Row[4];
$Data{Filename} = $Row[5];
}
if ( !$Data{Disposition} ) {
# if no content disposition is set images with content id should be inline
if ( $Data{ContentID} && $Data{ContentType} =~ m{image}i ) {
$Data{Disposition} = 'inline';
}
# converted article body should be inline
elsif ( $Data{Filename} =~ m{file-[12]} ) {
$Data{Disposition} = 'inline';
}
# all others including attachments with content id that are not images
# should NOT be inline
else {
$Data{Disposition} = 'attachment';
}
}
return %Data if defined $Data{Content};
# return if we only need to check one backend
return if !$Self->{CheckAllBackends};
# return if only delete in my backend
return if $Param{OnlyMyBackend};
return $Kernel::OM->Get('Kernel::System::Ticket::Article::Backend::MIMEBase::ArticleStorageFS')->ArticleAttachment(
%Param,
OnlyMyBackend => 1,
);
}
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

View File

@@ -0,0 +1,921 @@
# --
# 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::ArticleStorageFS;
use strict;
use warnings;
use File::Path qw();
use MIME::Base64 qw();
use Time::HiRes qw();
use Unicode::Normalize qw();
use parent qw(Kernel::System::Ticket::Article::Backend::MIMEBase::Base);
use Kernel::System::VariableCheck qw(:all);
our @ObjectDependencies = (
'Kernel::Config',
'Kernel::System::Cache',
'Kernel::System::DB',
'Kernel::System::DynamicFieldValue',
'Kernel::System::Encode',
'Kernel::System::Log',
'Kernel::System::Main',
'Kernel::System::Ticket::Article::Backend::MIMEBase::ArticleStorageDB',
);
=head1 NAME
Kernel::System::Ticket::Article::Backend::MIMEBase::ArticleStorageFS - FS based ticket article storage interface
=head1 DESCRIPTION
This class provides functions to manipulate ticket articles on the file system.
The methods are currently documented in L<Kernel::System::Ticket::Article::Backend::MIMEBase>.
Inherits from L<Kernel::System::Ticket::Article::Backend::MIMEBase::Base>.
See also L<Kernel::System::Ticket::Article::Backend::MIMEBase::ArticleStorageDB>.
=cut
sub new {
my ( $Type, %Param ) = @_;
# Call new() on Base.pm to execute the common code.
my $Self = $Type->SUPER::new(%Param);
my $ArticleContentPath = $Self->BuildArticleContentPath();
my $ArticleDir = "$Self->{ArticleDataDir}/$ArticleContentPath/";
File::Path::mkpath( $ArticleDir, 0, 0770 ); ## no critic
# Check write permissions.
if ( !-w $ArticleDir ) {
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'notice',
Message => "Can't write $ArticleDir! try: \$OTRS_HOME/bin/otrs.SetPermissions.pl!",
);
die "Can't write $ArticleDir! try: \$OTRS_HOME/bin/otrs.SetPermissions.pl!";
}
# Get activated cache backend configuration.
my $ConfigObject = $Kernel::OM->Get('Kernel::Config');
return $Self if !$ConfigObject->Get('Cache::ArticleStorageCache');
my $CacheModule = $ConfigObject->Get('Cache::Module') || '';
return $Self if $CacheModule ne 'Kernel::System::Cache::MemcachedFast';
# Turn on special cache used for speeding up article storage methods in huge systems with many
# nodes and slow FS access. It will be used only in environments with configured Memcached
# backend (see config above).
$Self->{ArticleStorageCache} = 1;
$Self->{ArticleStorageCacheTTL} = $ConfigObject->Get('Cache::ArticleStorageCache::TTL') || 60 * 60 * 24;
return $Self;
}
sub ArticleDelete {
my ( $Self, %Param ) = @_;
# check needed stuff
for my $Item (qw(ArticleID UserID)) {
if ( !$Param{$Item} ) {
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'error',
Message => "Need $Item!",
);
return;
}
}
# delete attachments
$Self->ArticleDeleteAttachment(
ArticleID => $Param{ArticleID},
UserID => $Param{UserID},
);
# delete plain message
$Self->ArticleDeletePlain(
ArticleID => $Param{ArticleID},
UserID => $Param{UserID},
);
# delete storage directory
$Self->_ArticleDeleteDirectory(
ArticleID => $Param{ArticleID},
UserID => $Param{UserID},
);
# Delete special article storage cache.
if ( $Self->{ArticleStorageCache} ) {
$Kernel::OM->Get('Kernel::System::Cache')->CleanUp(
Type => 'ArticleStorageFS_' . $Param{ArticleID},
);
}
return 1;
}
sub ArticleDeletePlain {
my ( $Self, %Param ) = @_;
# check needed stuff
for my $Item (qw(ArticleID UserID)) {
if ( !$Param{$Item} ) {
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'error',
Message => "Need $Item!",
);
return;
}
}
# delete from fs
my $ContentPath = $Self->_ArticleContentPathGet(
ArticleID => $Param{ArticleID},
);
my $File = "$Self->{ArticleDataDir}/$ContentPath/$Param{ArticleID}/plain.txt";
if ( -f $File ) {
if ( !unlink $File ) {
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'error',
Message => "Can't remove: $File: $!!",
);
return;
}
}
# Delete special article storage cache.
if ( $Self->{ArticleStorageCache} ) {
$Kernel::OM->Get('Kernel::System::Cache')->Delete(
Type => 'ArticleStorageFS_' . $Param{ArticleID},
Key => 'ArticlePlain',
);
}
# return if only delete in my backend
return 1 if $Param{OnlyMyBackend};
return $Kernel::OM->Get('Kernel::System::Ticket::Article::Backend::MIMEBase::ArticleStorageDB')->ArticleDeletePlain(
%Param,
OnlyMyBackend => 1,
);
}
sub ArticleDeleteAttachment {
my ( $Self, %Param ) = @_;
# check needed stuff
for my $Item (qw(ArticleID UserID)) {
if ( !$Param{$Item} ) {
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'error',
Message => "Need $Item!",
);
return;
}
}
# delete from fs
my $ContentPath = $Self->_ArticleContentPathGet(
ArticleID => $Param{ArticleID},
);
my $Path = "$Self->{ArticleDataDir}/$ContentPath/$Param{ArticleID}";
if ( -e $Path ) {
my @List = $Kernel::OM->Get('Kernel::System::Main')->DirectoryRead(
Directory => $Path,
Filter => "*",
);
for my $File (@List) {
if ( $File !~ /(\/|\\)plain.txt$/ ) {
if ( !unlink "$File" ) {
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'error',
Message => "Can't remove: $File: $!!",
);
}
}
}
}
# Delete special article storage cache.
if ( $Self->{ArticleStorageCache} ) {
$Kernel::OM->Get('Kernel::System::Cache')->CleanUp(
Type => 'ArticleStorageFS_' . $Param{ArticleID},
);
}
# return if only delete in my backend
return 1 if $Param{OnlyMyBackend};
return $Kernel::OM->Get('Kernel::System::Ticket::Article::Backend::MIMEBase::ArticleStorageDB')
->ArticleDeleteAttachment(
%Param,
OnlyMyBackend => 1,
);
}
sub ArticleWritePlain {
my ( $Self, %Param ) = @_;
# check needed stuff
for my $Item (qw(ArticleID Email UserID)) {
if ( !$Param{$Item} ) {
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'error',
Message => "Need $Item!",
);
return;
}
}
# prepare/filter ArticleID
$Param{ArticleID} = quotemeta( $Param{ArticleID} );
$Param{ArticleID} =~ s/\0//g;
# define path
my $ContentPath = $Self->_ArticleContentPathGet(
ArticleID => $Param{ArticleID},
);
my $Path = $Self->{ArticleDataDir} . '/' . $ContentPath . '/' . $Param{ArticleID};
# debug
if ( defined $Self->{Debug} && $Self->{Debug} > 1 ) {
$Kernel::OM->Get('Kernel::System::Log')->Log( Message => "->WriteArticle: $Path" );
}
# write article to fs 1:1
File::Path::mkpath( [$Path], 0, 0770 ); ## no critic
# write article to fs
my $Success = $Kernel::OM->Get('Kernel::System::Main')->FileWrite(
Location => "$Path/plain.txt",
Mode => 'binmode',
Content => \$Param{Email},
Permission => '660',
);
# Write to special article storage cache.
if ( $Self->{ArticleStorageCache} ) {
$Kernel::OM->Get('Kernel::System::Cache')->Set(
Type => 'ArticleStorageFS_' . $Param{ArticleID},
TTL => $Self->{ArticleStorageCacheTTL},
Key => 'ArticlePlain',
Value => $Param{Email},
CacheInMemory => 0,
CacheInBackend => 1,
);
}
return if !$Success;
return 1;
}
sub ArticleWriteAttachment {
my ( $Self, %Param ) = @_;
# check needed stuff
for my $Item (qw(Filename ContentType ArticleID UserID)) {
if ( !IsStringWithData( $Param{$Item} ) ) {
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'error',
Message => "Need $Item!",
);
return;
}
}
# prepare/filter ArticleID
$Param{ArticleID} = quotemeta( $Param{ArticleID} );
$Param{ArticleID} =~ s/\0//g;
my $ContentPath = $Self->_ArticleContentPathGet(
ArticleID => $Param{ArticleID},
);
# define path
$Param{Path} = $Self->{ArticleDataDir} . '/' . $ContentPath . '/' . $Param{ArticleID};
# get main object
my $MainObject = $Kernel::OM->Get('Kernel::System::Main');
# Perform FilenameCleanup here already to check for
# conflicting existing attachment files correctly
$Param{Filename} = $MainObject->FilenameCleanUp(
Filename => $Param{Filename},
Type => 'Local',
);
my $NewFileName = $Param{Filename};
my %UsedFile;
my %Index = $Self->ArticleAttachmentIndex(
ArticleID => $Param{ArticleID},
);
# Normalize filenames to find file names which are identical but in a different unicode form.
# This is needed because Mac OS (HFS+) converts all filenames to NFD internally.
# Without this, the same file might be overwritten because the strings are not equal.
for my $Position ( sort keys %Index ) {
$UsedFile{ Unicode::Normalize::NFC( $Index{$Position}->{Filename} ) } = 1;
}
for ( my $i = 1; $i <= 50; $i++ ) {
if ( exists $UsedFile{ Unicode::Normalize::NFC($NewFileName) } ) {
if ( $Param{Filename} =~ /^(.*)\.(.+?)$/ ) {
$NewFileName = "$1-$i.$2";
}
else {
$NewFileName = "$Param{Filename}-$i";
}
}
}
$Param{Filename} = $NewFileName;
# write attachment to backend
if ( !-d $Param{Path} ) {
if ( !File::Path::mkpath( [ $Param{Path} ], 0, 0770 ) ) { ## no critic
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'error',
Message => "Can't create $Param{Path}: $!",
);
return;
}
}
# write attachment content type to fs
my $SuccessContentType = $MainObject->FileWrite(
Directory => $Param{Path},
Filename => "$Param{Filename}.content_type",
Mode => 'binmode',
Content => \$Param{ContentType},
Permission => 660,
NoFilenameClean => 1,
);
return if !$SuccessContentType;
# set content id in angle brackets
if ( $Param{ContentID} ) {
$Param{ContentID} =~ s/^([^<].*[^>])$/<$1>/;
}
# write attachment content id to fs
if ( $Param{ContentID} ) {
$MainObject->FileWrite(
Directory => $Param{Path},
Filename => "$Param{Filename}.content_id",
Mode => 'binmode',
Content => \$Param{ContentID},
Permission => 660,
NoFilenameClean => 1,
);
}
# write attachment content alternative to fs
if ( $Param{ContentAlternative} ) {
$MainObject->FileWrite(
Directory => $Param{Path},
Filename => "$Param{Filename}.content_alternative",
Mode => 'binmode',
Content => \$Param{ContentAlternative},
Permission => 660,
NoFilenameClean => 1,
);
}
# write attachment disposition to fs
if ( $Param{Disposition} ) {
my ( $Disposition, $FileName ) = split ';', $Param{Disposition};
$MainObject->FileWrite(
Directory => $Param{Path},
Filename => "$Param{Filename}.disposition",
Mode => 'binmode',
Content => \$Disposition || '',
Permission => 660,
NoFilenameClean => 1,
);
}
# write attachment content to fs
my $SuccessContent = $MainObject->FileWrite(
Directory => $Param{Path},
Filename => $Param{Filename},
Mode => 'binmode',
Content => \$Param{Content},
Permission => 660,
);
return if !$SuccessContent;
# Delete special article storage cache.
if ( $Self->{ArticleStorageCache} ) {
$Kernel::OM->Get('Kernel::System::Cache')->CleanUp(
Type => 'ArticleStorageFS_' . $Param{ArticleID},
);
}
return 1;
}
sub ArticlePlain {
my ( $Self, %Param ) = @_;
# check needed stuff
if ( !$Param{ArticleID} ) {
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'error',
Message => 'Need ArticleID!',
);
return;
}
# prepare/filter ArticleID
$Param{ArticleID} = quotemeta( $Param{ArticleID} );
$Param{ArticleID} =~ s/\0//g;
# get cache object
my $CacheObject = $Kernel::OM->Get('Kernel::System::Cache');
# Read from special article storage cache.
if ( $Self->{ArticleStorageCache} ) {
my $Cache = $CacheObject->Get(
Type => 'ArticleStorageFS_' . $Param{ArticleID},
Key => 'ArticlePlain',
CacheInMemory => 0,
CacheInBackend => 1,
);
return $Cache if $Cache;
}
# get content path
my $ContentPath = $Self->_ArticleContentPathGet(
ArticleID => $Param{ArticleID},
);
# open plain article
if ( -f "$Self->{ArticleDataDir}/$ContentPath/$Param{ArticleID}/plain.txt" ) {
# read whole article
my $Data = $Kernel::OM->Get('Kernel::System::Main')->FileRead(
Directory => "$Self->{ArticleDataDir}/$ContentPath/$Param{ArticleID}/",
Filename => 'plain.txt',
Mode => 'binmode',
);
return if !$Data;
# Write to special article storage cache.
if ( $Self->{ArticleStorageCache} ) {
$CacheObject->Set(
Type => 'ArticleStorageFS_' . $Param{ArticleID},
TTL => $Self->{ArticleStorageCacheTTL},
Key => 'ArticlePlain',
Value => ${$Data},
CacheInMemory => 0,
CacheInBackend => 1,
);
}
return ${$Data};
}
# return if we only need to check one backend
return if !$Self->{CheckAllBackends};
# return if only delete in my backend
return if $Param{OnlyMyBackend};
my $Data = $Kernel::OM->Get('Kernel::System::Ticket::Article::Backend::MIMEBase::ArticleStorageDB')->ArticlePlain(
%Param,
OnlyMyBackend => 1,
);
# Write to special article storage cache.
if ( $Self->{ArticleStorageCache} ) {
$CacheObject->Set(
Type => 'ArticleStorageFS_' . $Param{ArticleID},
TTL => $Self->{ArticleStorageCacheTTL},
Key => 'ArticlePlain',
Value => $Data,
CacheInMemory => 0,
CacheInBackend => 1,
);
}
return $Data;
}
sub ArticleAttachmentIndexRaw {
my ( $Self, %Param ) = @_;
# check needed stuff
if ( !$Param{ArticleID} ) {
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'error',
Message => 'Need ArticleID!',
);
return;
}
# get cache object
my $CacheObject = $Kernel::OM->Get('Kernel::System::Cache');
# Read from special article storage cache.
if ( $Self->{ArticleStorageCache} ) {
my $Cache = $CacheObject->Get(
Type => 'ArticleStorageFS_' . $Param{ArticleID},
Key => 'ArticleAttachmentIndexRaw',
CacheInMemory => 0,
CacheInBackend => 1,
);
return %{$Cache} if $Cache;
}
my $ContentPath = $Self->_ArticleContentPathGet(
ArticleID => $Param{ArticleID},
);
my %Index;
my $Counter = 0;
# get main object
my $MainObject = $Kernel::OM->Get('Kernel::System::Main');
# try fs
my @List = $MainObject->DirectoryRead(
Directory => "$Self->{ArticleDataDir}/$ContentPath/$Param{ArticleID}",
Filter => "*",
Silent => 1,
);
FILENAME:
for my $Filename ( sort @List ) {
my $FileSizeRaw = -s $Filename;
# do not use control file
next FILENAME if $Filename =~ /\.content_alternative$/;
next FILENAME if $Filename =~ /\.content_id$/;
next FILENAME if $Filename =~ /\.content_type$/;
next FILENAME if $Filename =~ /\.disposition$/;
next FILENAME if $Filename =~ /\/plain.txt$/;
# read content type
my $ContentType = '';
my $ContentID = '';
my $Alternative = '';
my $Disposition = '';
if ( -e "$Filename.content_type" ) {
my $Content = $MainObject->FileRead(
Location => "$Filename.content_type",
);
return if !$Content;
$ContentType = ${$Content};
# content id (optional)
if ( -e "$Filename.content_id" ) {
my $Content = $MainObject->FileRead(
Location => "$Filename.content_id",
);
if ($Content) {
$ContentID = ${$Content};
}
}
# alternative (optional)
if ( -e "$Filename.content_alternative" ) {
my $Content = $MainObject->FileRead(
Location => "$Filename.content_alternative",
);
if ($Content) {
$Alternative = ${$Content};
}
}
# disposition
if ( -e "$Filename.disposition" ) {
my $Content = $MainObject->FileRead(
Location => "$Filename.disposition",
);
if ($Content) {
$Disposition = ${$Content};
}
}
# if no content disposition is set images with content id should be inline
elsif ( $ContentID && $ContentType =~ m{image}i ) {
$Disposition = 'inline';
}
# converted article body should be inline
elsif ( $Filename =~ m{file-[12]} ) {
$Disposition = 'inline';
}
# all others including attachments with content id that are not images
# should NOT be inline
else {
$Disposition = 'attachment';
}
}
# read content type (old style)
else {
my $Content = $MainObject->FileRead(
Location => $Filename,
Result => 'ARRAY',
);
if ( !$Content ) {
return;
}
$ContentType = $Content->[0];
}
# strip filename
$Filename =~ s!^.*/!!;
# add the info the the hash
$Counter++;
$Index{$Counter} = {
Filename => $Filename,
FilesizeRaw => $FileSizeRaw,
ContentType => $ContentType,
ContentID => $ContentID,
ContentAlternative => $Alternative,
Disposition => $Disposition,
};
}
# Write to special article storage cache.
if ( $Self->{ArticleStorageCache} ) {
$CacheObject->Set(
Type => 'ArticleStorageFS_' . $Param{ArticleID},
TTL => $Self->{ArticleStorageCacheTTL},
Key => 'ArticleAttachmentIndexRaw',
Value => \%Index,
CacheInMemory => 0,
CacheInBackend => 1,
);
}
return %Index if %Index;
# return if we only need to check one backend
return if !$Self->{CheckAllBackends};
# return if only delete in my backend
return %Index if $Param{OnlyMyBackend};
%Index = $Kernel::OM->Get('Kernel::System::Ticket::Article::Backend::MIMEBase::ArticleStorageDB')
->ArticleAttachmentIndexRaw(
%Param,
OnlyMyBackend => 1,
);
# Write to special article storage cache.
if ( $Self->{ArticleStorageCache} ) {
$CacheObject->Set(
Type => 'ArticleStorageFS_' . $Param{ArticleID},
TTL => $Self->{ArticleStorageCacheTTL},
Key => 'ArticleAttachmentIndexRaw',
Value => \%Index,
CacheInMemory => 0,
CacheInBackend => 1,
);
}
return %Index;
}
sub ArticleAttachment {
my ( $Self, %Param ) = @_;
# check needed stuff
for my $Item (qw(ArticleID FileID)) {
if ( !$Param{$Item} ) {
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'error',
Message => "Need $Item!",
);
return;
}
}
# prepare/filter ArticleID
$Param{ArticleID} = quotemeta( $Param{ArticleID} );
$Param{ArticleID} =~ s/\0//g;
# get cache object
my $CacheObject = $Kernel::OM->Get('Kernel::System::Cache');
# Read from special article storage cache.
if ( $Self->{ArticleStorageCache} ) {
my $Cache = $CacheObject->Get(
Type => 'ArticleStorageFS_' . $Param{ArticleID},
Key => 'ArticleAttachment' . $Param{FileID},
CacheInMemory => 0,
CacheInBackend => 1,
);
return %{$Cache} if $Cache;
}
# get attachment index
my %Index = $Self->ArticleAttachmentIndex(
ArticleID => $Param{ArticleID},
);
# get content path
my $ContentPath = $Self->_ArticleContentPathGet(
ArticleID => $Param{ArticleID},
);
my %Data = %{ $Index{ $Param{FileID} } // {} };
my $Counter = 0;
# get main object
my $MainObject = $Kernel::OM->Get('Kernel::System::Main');
my @List = $MainObject->DirectoryRead(
Directory => "$Self->{ArticleDataDir}/$ContentPath/$Param{ArticleID}",
Filter => "*",
Silent => 1,
);
if (@List) {
# get encode object
my $EncodeObject = $Kernel::OM->Get('Kernel::System::Encode');
FILENAME:
for my $Filename (@List) {
next FILENAME if $Filename =~ /\.content_alternative$/;
next FILENAME if $Filename =~ /\.content_id$/;
next FILENAME if $Filename =~ /\.content_type$/;
next FILENAME if $Filename =~ /\/plain.txt$/;
next FILENAME if $Filename =~ /\.disposition$/;
# add the info the the hash
$Counter++;
if ( $Counter == $Param{FileID} ) {
if ( -e "$Filename.content_type" ) {
# read content type
my $Content = $MainObject->FileRead(
Location => "$Filename.content_type",
);
return if !$Content;
$Data{ContentType} = ${$Content};
# read content
$Content = $MainObject->FileRead(
Location => $Filename,
Mode => 'binmode',
);
return if !$Content;
$Data{Content} = ${$Content};
# content id (optional)
if ( -e "$Filename.content_id" ) {
my $Content = $MainObject->FileRead(
Location => "$Filename.content_id",
);
if ($Content) {
$Data{ContentID} = ${$Content};
}
}
# alternative (optional)
if ( -e "$Filename.content_alternative" ) {
my $Content = $MainObject->FileRead(
Location => "$Filename.content_alternative",
);
if ($Content) {
$Data{Alternative} = ${$Content};
}
}
# disposition
if ( -e "$Filename.disposition" ) {
my $Content = $MainObject->FileRead(
Location => "$Filename.disposition",
);
if ($Content) {
$Data{Disposition} = ${$Content};
}
}
# if no content disposition is set images with content id should be inline
elsif ( $Data{ContentID} && $Data{ContentType} =~ m{image}i ) {
$Data{Disposition} = 'inline';
}
# converted article body should be inline
elsif ( $Filename =~ m{file-[12]} ) {
$Data{Disposition} = 'inline';
}
# all others including attachments with content id that are not images
# should NOT be inline
else {
$Data{Disposition} = 'attachment';
}
}
else {
# read content
my $Content = $MainObject->FileRead(
Location => $Filename,
Mode => 'binmode',
Result => 'ARRAY',
);
return if !$Content;
$Data{ContentType} = $Content->[0];
my $Counter = 0;
for my $Line ( @{$Content} ) {
if ($Counter) {
$Data{Content} .= $Line;
}
$Counter++;
}
}
if (
$Data{ContentType} =~ /plain\/text/i
&& $Data{ContentType} =~ /(utf\-8|utf8)/i
)
{
$EncodeObject->EncodeInput( \$Data{Content} );
}
chomp $Data{ContentType};
# Write to special article storage cache.
if ( $Self->{ArticleStorageCache} ) {
$CacheObject->Set(
Type => 'ArticleStorageFS_' . $Param{ArticleID},
TTL => $Self->{ArticleStorageCacheTTL},
Key => 'ArticleAttachment' . $Param{FileID},
Value => \%Data,
CacheInMemory => 0,
CacheInBackend => 1,
);
}
return %Data;
}
}
}
# return if we only need to check one backend
return if !$Self->{CheckAllBackends};
# return if only delete in my backend
return if $Param{OnlyMyBackend};
%Data = $Kernel::OM->Get('Kernel::System::Ticket::Article::Backend::MIMEBase::ArticleStorageDB')->ArticleAttachment(
%Param,
OnlyMyBackend => 1,
);
# Write to special article storage cache.
if ( $Self->{ArticleStorageCache} ) {
$CacheObject->Set(
Type => 'ArticleStorageFS_' . $Param{ArticleID},
TTL => $Self->{ArticleStorageCacheTTL},
Key => 'ArticleAttachment' . $Param{FileID},
Value => \%Data,
CacheInMemory => 0,
CacheInBackend => 1,
);
}
return %Data;
}
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

View File

@@ -0,0 +1,357 @@
# --
# 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::Base;
use strict;
use warnings;
our $ObjectManagerDisabled = 1;
=head1 NAME
Kernel::System::Ticket::Article::Backend::MIMEBase::Base - base class for article storage modules
=head1 DESCRIPTION
This is a base class for article storage backends and should not be instantiated directly.
=head1 PUBLIC INTERFACE
=cut
=head2 new()
Don't instantiate this class directly, get instances of the real storage backends instead:
my $BackendObject = $Kernel::OM->Get('Kernel::System::Article::Backend::MIMEBase::ArticleStorageDB');
=cut
sub new {
my ( $Type, %Param ) = @_;
# allocate new hash for object
my $Self = {};
bless( $Self, $Type );
$Self->{CacheType} = 'ArticleStorageBase';
$Self->{CacheTTL} = 60 * 60 * 24 * 20;
$Self->{ArticleDataDir}
= $Kernel::OM->Get('Kernel::Config')->Get('Ticket::Article::Backend::MIMEBase::ArticleDataDir')
|| die 'Got no ArticleDataDir!';
# do we need to check all backends, or just one?
$Self->{CheckAllBackends}
= $Kernel::OM->Get('Kernel::Config')->Get('Ticket::Article::Backend::MIMEBase::CheckAllStorageBackends')
// 0;
return $Self;
}
=head2 BuildArticleContentPath()
Generate a base article content path for article storage in the file system.
my $ArticleContentPath = $BackendObject->BuildArticleContentPath();
=cut
sub BuildArticleContentPath {
my ( $Self, %Param ) = @_;
return $Self->{ArticleContentPath} if $Self->{ArticleContentPath};
$Self->{ArticleContentPath} = $Kernel::OM->Create('Kernel::System::DateTime')->Format(
Format => '%Y/%m/%d',
);
return $Self->{ArticleContentPath};
}
=head2 ArticleAttachmentIndex()
Get article attachment index as hash.
my %Index = $BackendObject->ArticleAttachmentIndex(
ArticleID => 123,
ExcludePlainText => 1, # (optional) Exclude plain text attachment
ExcludeHTMLBody => 1, # (optional) Exclude HTML body attachment
ExcludeInline => 1, # (optional) Exclude inline attachments
OnlyHTMLBody => 1, # (optional) Return only HTML body attachment, return nothing if not found
);
Returns:
my %Index = {
'1' => { # Attachment ID
ContentAlternative => '', # (optional)
ContentID => '', # (optional)
ContentType => 'application/pdf',
Filename => 'StdAttachment-Test1.pdf',
FilesizeRaw => 4722,
Disposition => 'attachment',
},
'2' => {
ContentAlternative => '',
ContentID => '',
ContentType => 'text/html; charset="utf-8"',
Filename => 'file-2',
FilesizeRaw => 183,
Disposition => 'attachment',
},
...
};
=cut
sub ArticleAttachmentIndex {
my ( $Self, %Param ) = @_;
if ( !$Param{ArticleID} ) {
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'error',
Message => 'Need ArticleID!',
);
return;
}
if ( $Param{ExcludeHTMLBody} && $Param{OnlyHTMLBody} ) {
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'error',
Message => 'ExcludeHTMLBody and OnlyHTMLBody cannot be used together!',
);
return;
}
# Get complete attachment index from backend.
my %Attachments = $Self->ArticleAttachmentIndexRaw(%Param);
# Iterate over attachments only if any of optional parameters is active.
if ( $Param{ExcludePlainText} || $Param{ExcludeHTMLBody} || $Param{ExcludeInline} || $Param{OnlyHTMLBody} ) {
my $AttachmentIDPlain = 0;
my $AttachmentIDHTML = 0;
ATTACHMENT_ID:
for my $AttachmentID ( sort keys %Attachments ) {
my %File = %{ $Attachments{$AttachmentID} };
# Identify plain text attachment.
if (
!$AttachmentIDPlain
&&
$File{Filename} eq 'file-1'
&& $File{ContentType} =~ /text\/plain/i
&& $File{Disposition} eq 'inline'
)
{
$AttachmentIDPlain = $AttachmentID;
next ATTACHMENT_ID;
}
# Identify html body attachment:
# - file-[12] is plain+html attachment
# - file-1.html is html attachment only
if (
!$AttachmentIDHTML
&&
( $File{Filename} =~ /^file-[12]$/ || $File{Filename} eq 'file-1.html' )
&& $File{ContentType} =~ /text\/html/i
&& $File{Disposition} eq 'inline'
)
{
$AttachmentIDHTML = $AttachmentID;
next ATTACHMENT_ID;
}
}
# If neither plain text or html body were found, iterate again to try to identify plain text among regular
# non-inline attachments.
if ( !$AttachmentIDPlain && !$AttachmentIDHTML ) {
ATTACHMENT_ID:
for my $AttachmentID ( sort keys %Attachments ) {
my %File = %{ $Attachments{$AttachmentID} };
# Remember, file-1 got defined by parsing if no filename was given.
if (
$File{Filename} eq 'file-1'
&& $File{ContentType} =~ /text\/plain/i
)
{
$AttachmentIDPlain = $AttachmentID;
last ATTACHMENT_ID;
}
}
}
# Identify inline (image) attachments which are referenced in HTML body. Do not strip attachments based on their
# disposition, since this method of detection is unreliable. Please see bug#13353 for more information.
my @AttachmentIDsInline;
if ($AttachmentIDHTML) {
# Get HTML article body.
my %HTMLBody = $Self->ArticleAttachment(
ArticleID => $Param{ArticleID},
FileID => $AttachmentIDHTML,
);
if ( %HTMLBody && $HTMLBody{Content} ) {
ATTACHMENT_ID:
for my $AttachmentID ( sort keys %Attachments ) {
my %File = %{ $Attachments{$AttachmentID} };
next ATTACHMENT_ID if $File{ContentType} !~ m{image}ixms;
next ATTACHMENT_ID if !$File{ContentID};
my ($ImageID) = ( $File{ContentID} =~ m{^<(.*)>$}ixms );
# Search in the article body if there is any reference to it.
if ( $HTMLBody{Content} =~ m{<img.+src=['|"]cid:\Q$ImageID\E['|"].*>}ixms ) {
push @AttachmentIDsInline, $AttachmentID;
}
}
}
}
if ( $AttachmentIDPlain && $Param{ExcludePlainText} ) {
delete $Attachments{$AttachmentIDPlain};
}
if ( $AttachmentIDHTML && $Param{ExcludeHTMLBody} ) {
delete $Attachments{$AttachmentIDHTML};
}
if ( $Param{ExcludeInline} ) {
for my $AttachmentID (@AttachmentIDsInline) {
delete $Attachments{$AttachmentID};
}
}
if ( $Param{OnlyHTMLBody} ) {
if ($AttachmentIDHTML) {
%Attachments = (
$AttachmentIDHTML => $Attachments{$AttachmentIDHTML}
);
}
else {
%Attachments = ();
}
}
}
return %Attachments;
}
=head1 PRIVATE FUNCTIONS
=cut
sub _ArticleDeleteDirectory {
my ( $Self, %Param ) = @_;
for my $Needed (qw(ArticleID UserID)) {
if ( !$Param{$Needed} ) {
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'error',
Message => "Need $Needed!"
);
return;
}
}
# delete directory from fs
my $ContentPath = $Self->_ArticleContentPathGet(
ArticleID => $Param{ArticleID},
);
my $Path = "$Self->{ArticleDataDir}/$ContentPath/$Param{ArticleID}";
if ( -d $Path ) {
if ( !rmdir $Path ) {
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'error',
Message => "Can't remove '$Path': $!.",
);
return;
}
}
return 1;
}
=head2 _ArticleContentPathGet()
Get the stored content path of an article.
my $Path = $BackendObject->_ArticleContentPatGeth(
ArticleID => 123,
);
=cut
sub _ArticleContentPathGet {
my ( $Self, %Param ) = @_;
if ( !$Param{ArticleID} ) {
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'error',
Message => 'Need ArticleID!',
);
return;
}
# check key
my $CacheKey = '_ArticleContentPathGet::' . $Param{ArticleID};
my $CacheObject = $Kernel::OM->Get('Kernel::System::Cache');
# check cache
my $Cache = $CacheObject->Get(
Type => $Self->{CacheType},
Key => $CacheKey,
);
return $Cache if $Cache;
# get database object
my $DBObject = $Kernel::OM->Get('Kernel::System::DB');
# sql query
return if !$DBObject->Prepare(
SQL => 'SELECT content_path FROM article_data_mime WHERE article_id = ?',
Bind => [ \$Param{ArticleID} ],
);
my $Result;
while ( my @Row = $DBObject->FetchrowArray() ) {
$Result = $Row[0];
}
# set cache
$CacheObject->Set(
Type => $Self->{CacheType},
TTL => $Self->{CacheTTL},
Key => $CacheKey,
Value => $Result,
);
# return
return $Result;
}
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

View File

@@ -0,0 +1,44 @@
# --
# 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::Phone;
use strict;
use warnings;
use parent 'Kernel::System::Ticket::Article::Backend::MIMEBase';
our @ObjectDependencies = ();
=head1 NAME
Kernel::System::Ticket::Article::Backend::Phone - backend class for phone articles
=head1 DESCRIPTION
This class provides functions to manipulate phone articles in the database.
Inherits from L<Kernel::System::Ticket::Article::Backend::MIMEBase>, please have a look there for its API.
=cut
sub ChannelNameGet {
return 'Phone';
}
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