# --
# 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::MailQueue;
use strict;
use warnings;
use MIME::Base64;
use Kernel::System::VariableCheck qw(:all);
use parent qw(Kernel::System::EventHandler);
our @ObjectDependencies = (
'Kernel::Config',
'Kernel::System::Log',
'Kernel::System::DB',
'Kernel::System::DateTime',
'Kernel::System::Storable',
'Kernel::System::Ticket::Article',
'Kernel::System::Email',
'Kernel::System::CheckItem',
'Kernel::System::Main',
'Kernel::System::CommunicationLog',
'Kernel::System::CommunicationLog::DB',
);
my %DBNames = (
ID => 'id',
ArticleID => 'article_id',
CommunicationLogMessageID => 'communication_log_object_id',
Sender => 'sender',
Recipient => 'recipient',
Attempts => 'attempts',
Message => 'raw_message',
DueTime => 'due_time',
LastSMTPCode => 'last_smtp_code',
LastSMTPMessage => 'last_smtp_message',
);
=head1 PUBLIC INTERFACE
=head2 new()
Create a MailQueue object. Do not use it directly, instead use:
my $MailQueueObject = $Kernel::OM->Get('Kernel::System::MailQueue');
=cut
sub new {
my ( $Type, %Param ) = @_;
my $Self = {};
bless( $Self, $Type );
$Self->{CheckEmailAddresses} = $Param{CheckEmailAddresses} // 1;
# init of event handler
$Self->{EventHandlerTransaction} = 1;
$Self->EventHandlerInit(
Config => 'Ticket::EventModulePost',
);
return $Self;
}
=head2 Create()
Create a new queue element.
my $Result = $MailQueue->Create(
ArticleID => '...', # optional
MessageID => '...', # optional (in case article id was passed this should be also)
Sender => '...',
Recipient => '...' || [],
Message => '...',
Attempts => '...', # optional
);
Returns 1 or undef.
=cut
sub Create {
my ( $Self, %Param ) = @_;
my $LogObject = $Kernel::OM->Get('Kernel::System::Log');
$Param{CommunicationLogObject} ||= $Kernel::OM->Create(
'Kernel::System::CommunicationLog',
ObjectParams => {
Transport => 'Email',
Direction => 'Outgoing',
},
);
if ( !$Param{CommunicationLogObject} ) {
$LogObject->Log(
Priority => 'error',
Message => "Communication log object not started!",
);
return;
}
if (
!$Param{CommunicationLogObject}->IsObjectLogOpen(
ObjectLogType => 'Message',
)
)
{
$Param{CommunicationLogObject}->ObjectLogStart(
ObjectLogType => 'Message',
);
}
# Check for required data.
for my $Argument (qw(Recipient Message)) {
if ( !$Param{$Argument} ) {
my $ErrorMessage = "Need $Argument!";
$LogObject->Log(
Priority => 'error',
Message => $ErrorMessage,
);
$Param{CommunicationLogObject}->ObjectLog(
ObjectLogType => 'Message',
Priority => 'Error',
Key => 'Kernel::System::MailQueue',
Value => $ErrorMessage,
);
$Self->_SetArticleTransmissionError(
%Param,
Message => $ErrorMessage,
);
return;
}
}
# Recipient can be a Scalar or ArrayRef
my $Recipient = $Param{Recipient} // [];
my $RecipientRef = ref $Recipient;
if ( !$RecipientRef ) {
$Recipient = [ split( ',', $Recipient, ) ];
}
my $OnErrorSetArticleTransmissionError = sub {
my %LocalParam = @_;
my $Result = $LocalParam{Process}->();
if ( !$Result ) {
$Self->_SetArticleTransmissionError(
%Param,
Message => $LocalParam{ErrorMessage}
);
}
return $Result;
};
# Message should be a Hashref.
return if !$OnErrorSetArticleTransmissionError->(
Process => sub {
return $Self->_CheckValidMessageData(
Data => $Param{Message},
CommunicationLogObject => $Param{CommunicationLogObject},
);
},
ErrorMessage => 'Error while validating Message data.',
);
# Check if sender and recipent are valid email addresses.
return if !$OnErrorSetArticleTransmissionError->(
Process => sub {
return $Self->_CheckValidEmailAddresses(
ParamName => 'Sender',
Addresses => [ $Param{Sender} ? $Param{Sender} : () ],
CommunicationLogObject => $Param{CommunicationLogObject},
);
},
ErrorMessage => 'Error while validating Sender email address.',
);
return if !$OnErrorSetArticleTransmissionError->(
Process => sub {
return $Self->_CheckValidEmailAddresses(
ParamName => 'Recipient',
Addresses => $Recipient,
CommunicationLogObject => $Param{CommunicationLogObject},
);
},
ErrorMessage => 'Error while validating Recipient email address.',
);
# Check if already exists a mail-queue item for the same article id.
return if $Self->_IsArticleAlreadyQueued( %Param, );
my $RecipientStr = join q{,}, @{$Recipient};
$Param{CommunicationLogObject}->ObjectLog(
ObjectLogType => 'Message',
Priority => 'Debug',
Key => 'Kernel::System::MailQueue',
Value => sprintf(
"Serializing and saving message (ArticleID: %s, Sender: $Param{Sender}, "
. "Recipient: $RecipientStr, MessageID: %s)",
$Param{ArticleID} || '',
$Param{MessageID} || '',
),
);
# Serialize Message.
my $Message = $Self->_SerializeMessage(
Message => $Param{Message},
);
my $InsertResult = $Self->_DBInsert(
%Param,
Recipient => $RecipientStr,
Message => $Message,
);
return if !$InsertResult->{Success} && $InsertResult->{Action} eq 'db-insert';
$Self->_SetCommunicationLogLookup(
CommunicationLogObject => $Param{CommunicationLogObject},
ArticleID => $Param{ArticleID},
ID => $InsertResult->{ID},
);
my $LogMessage = 'Successfully stored message for sending.';
# Event Notification
$Self->_SendEventNotification(
ArticleID => $Param{ArticleID},
Status => 'Queued',
Message => $LogMessage,
UserID => $Param{UserID},
);
$Param{CommunicationLogObject}->ObjectLog(
ObjectLogType => 'Message',
Priority => 'Info',
Key => 'Kernel::System::MailQueue',
Value => $LogMessage,
);
return 1;
}
=head2 List()
Get a list of the queue elements.
my $List = $MailQueue->List(
ID => '...' # optional
ArticleID => '...' # optional
Sender => '...' # optional
Recipient => '...' # optional
Attempts => '...' # optional
);
This returns something like:
my $List = [
{
ID => '...',
ArticleID => '...',
Attempts => '...',
Sender => '...',
Recipient => ['...'],
Message => '...',
DueTime => '...',
LastSMTPCode => '...',
LastSMTPMessage => '...',
},
...,
]
=cut
sub List {
my ( $Self, %Param, ) = @_;
my @SQL = (
'SELECT id, article_id, attempts, sender, recipient, raw_message',
', due_time, last_smtp_code, last_smtp_message',
'FROM mail_queue',
);
my ( $FilterSQL, $Binds ) = $Self->_FiltersSQLAndBinds(%Param);
if ($FilterSQL) {
push @SQL, 'WHERE', $FilterSQL;
}
my $DBObject = $Kernel::OM->Get('Kernel::System::DB');
return if !$DBObject->Prepare(
SQL => join( ' ', @SQL ),
Bind => $Binds,
Limit => $Param{Limit},
Encode => [ 1, 1, 1, 1, 1, 0 ],
);
my @List = ();
while ( my @Row = $DBObject->FetchrowArray() ) {
# Deserialize Message.
my $Message = $Self->_DeserializeMessage(
Message => $Row[5],
);
# Convert DueTime to a DateTimeObject
my $DueTime = $Row[6];
if ($DueTime) {
$DueTime = $Kernel::OM->Create(
'Kernel::System::DateTime',
ObjectParams => {
String => $DueTime,
},
);
}
my %Item = (
ID => $Row[0],
ArticleID => $Row[1],
Attempts => $Row[2],
Sender => $Row[3],
Recipient => [ split( ',', $Row[4], ) ],
Message => $Message,
DueTime => $DueTime,
LastSMTPCode => $Row[7],
LastSMTPMessage => $Row[8],
);
# Normalize empty string/undef value to undef.
KEY:
for my $Key ( sort keys %Item ) {
my $Value = $Item{$Key};
next KEY if ref $Value;
next KEY if defined $Value && length $Value;
$Item{$Key} = undef;
}
push @List, \%Item;
}
return \@List;
}
=head2 Get()
Get a queue element.
my $Item = $MailQueue->Get(
ID => '...' # optional
ArticleID => '...' # optional
);
This returns something like:
$Item = {
ID => '...',
ArticleID => '...',
Attempts => '...',
Sender => '...',
Recipient => ['...'],
Message => '...',
DueTime => '...',
LastSMTPCode => '...',
LastSMTPMessage => '...',
};
or and empty hashref if element not found.
=cut
sub Get {
my ( $Self, %Param, ) = @_;
my $LogObject = $Kernel::OM->Get('Kernel::System::Log');
# Check for required data.
if ( !$Param{ID} && !$Param{ArticleID} ) {
$LogObject->Log(
Priority => 'error',
Message => "Need ID or ArticleID!",
);
return;
}
my $List = $Self->List(
ID => $Param{ID},
ArticleID => $Param{ArticleID}
);
return $List->[0] if IsArrayRefWithData($List);
return {};
}
=head2 Update()
Update queue elements.
my $Result = $MailQueue->Update(
Filters => {},
Data => {},
);
Returns 1 or undef.
=cut
sub Update {
my ( $Self, %Param, ) = @_;
my %Filters = %{ $Param{Filters} || {} };
my %Data = %{ $Param{Data} || {} };
my $LogError = sub {
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'error',
Message => shift,
);
return;
};
if ( !%Data ) {
return $LogError->("Need Data!");
}
# If we're updating the ArticleID, need to check if is already queued.
if ( $Data{ArticleID} ) {
my $ArticleID = $Data{ArticleID};
my $ToUpdate = $Self->List(%Filters);
return if $Self->_IsArticleAlreadyQueued(
ArticleID => $ArticleID,
ToUpdate => $ToUpdate,
CommunicationLogObject => $Param{CommunicationLogObject},
);
$Self->_SetCommunicationLogLookup(
CommunicationLogObject => $Param{CommunicationLogObject},
ArticleID => $Data{ArticleID},
ID => $ToUpdate->[0]->{ID},
);
}
my @SQL = ( 'UPDATE mail_queue', );
my @Binds = ();
# Build set clause.
my @SQLSet = ();
for my $Col ( sort keys %Data ) {
my $Value = $Data{$Col};
if ( $Col eq 'Sender' || $Col eq 'Recipient' ) {
if ( $Col eq 'Recipient' && !ref $Value ) {
$Value = [ split( ',', $Value, ) ];
}
return if !$Self->_CheckValidEmailAddresses(
ParamName => $Col,
Addresses => $Value,
CommunicationLogObject => $Param{CommunicationLogObject},
);
if ( ref $Value ) {
$Value = join( ',', @{$Value}, );
}
}
elsif ( $Col eq 'Message' ) {
return if !$Self->_CheckValidMessageData(
Data => $Value,
CommunicationLogObject => $Param{CommunicationLogObject},
);
$Value = $Self->_SerializeMessage(
Message => $Value,
);
}
push @SQLSet, sprintf( '%s = ?', $DBNames{$Col} );
push @Binds, \$Value;
}
push @SQL, 'SET', join( q{, }, @SQLSet );
# Build where clause.
my ( $FilterSQL, $FilterBinds ) = $Self->_FiltersSQLAndBinds( %Filters, );
if ($FilterSQL) {
push @SQL, 'WHERE', $FilterSQL;
push @Binds, @{$FilterBinds};
}
return if !$Kernel::OM->Get('Kernel::System::DB')->Do(
SQL => join( q{ }, @SQL ),
Bind => \@Binds,
);
return 1;
}
=head2 Delete()
Delete queue elements.
my $Result = $MailQueue->Delete(
%Filters, # See _FiltersSQLAndBinds
);
Returns 1 or undef.
=cut
sub Delete {
my ( $Self, %Param, ) = @_;
my $DoSetTransmissionArticleError = delete $Param{SetTransmissionArticleError};
my $List;
if ($DoSetTransmissionArticleError) {
$List = $Self->List(%Param);
}
my @SQL = ( 'DELETE FROM mail_queue', );
my ( $FilterSQL, $Binds ) = $Self->_FiltersSQLAndBinds(%Param);
if ($FilterSQL) {
push @SQL, 'WHERE', $FilterSQL;
}
my $DBObject = $Kernel::OM->Get('Kernel::System::DB');
return if !$Kernel::OM->Get('Kernel::System::DB')->Do(
SQL => join( ' ', @SQL ),
Bind => $Binds,
);
return 1 if !$DoSetTransmissionArticleError;
for my $Item (@$List) {
$Self->_SetArticleTransmissionError(
ArticleID => $Item->{ArticleID},
Message => $DoSetTransmissionArticleError,
);
my $CommunicationLogObject = $Self->_GetCommunicationLog(
ID => $Item->{ID},
);
$CommunicationLogObject->ObjectLog(
ObjectLogType => 'Message',
Priority => 'Error',
Key => ref $Self,
Value => $DoSetTransmissionArticleError,
);
$CommunicationLogObject->ObjectLogStop(
ObjectLogType => 'Message',
Status => 'Failed',
);
$CommunicationLogObject->CommunicationStop( Status => 'Failed' );
}
return 1;
}
=head2 Send()
Send/Process a mail queue element/item.
my $List = $MailQueue->Send(
ID => '...',
Sender => '...',
Recipient => '...',
Message => '...',
Force => '...' # optional, to force the sending even if isn't time
);
This returns something like:
$List = {
Status => '(Failed|Pending|Success)',
Message => '...', # undef if success.
};
=cut
sub Send {
my ( $Self, %Param ) = @_;
my $LogObject = $Kernel::OM->Get('Kernel::System::Log');
# Check for required data.
for my $Argument (qw(ID Recipient Message)) {
if ( !$Param{$Argument} ) {
return {
Status => 'Failed',
Message => "Need $Argument.",
};
}
}
# Message should be a HashRef
if ( !IsHashRefWithData( $Param{Message} ) ) {
return {
Status => 'Failed',
Message => 'Invalid Message, should be a HashRef!',
};
}
# Check for message required data.
for my $Argument (qw(Header Body)) {
if ( !$Param{Message}->{$Argument} ) {
return {
Status => 'Failed',
Message => "Need Message - $Argument!",
};
}
}
# Lookup for the communication id for the processing mail-queue item.
$Param{CommunicationLogObject} = $Self->_GetCommunicationLog( %Param, );
$Param{CommunicationID} = $Param{CommunicationLogObject}->CommunicationIDGet();
# If DueTime is bigger than current time, skip, it is not time to run yet.
my $CurrentSysDTObject = $Kernel::OM->Create('Kernel::System::DateTime');
my $DueTime = $Param{DueTime};
if ( !( delete $Param{Force} ) && $DueTime && $DueTime > $CurrentSysDTObject ) {
my $LogMessage = sprintf(
q{Resending pause still active for message with id "%s", please wait until "%s"!},
$Param{ID},
$DueTime->ToString(),
);
$Param{CommunicationLogObject}->ObjectLog(
ObjectLogType => 'Message',
Priority => 'Debug',
Key => 'Kernel::System::MailQueue',
Value => $LogMessage,
);
return {
Status => 'Pending',
Message => $LogMessage,
};
}
$Param{CommunicationLogObject}->ObjectLog(
ObjectLogType => 'Message',
Priority => 'Info',
Key => 'Kernel::System::MailQueue',
Value => "Sending queued message with id '$Param{ID}'.",
);
my $EmailObject = $Kernel::OM->Get('Kernel::System::Email');
my $Result = $EmailObject->SendExecute(
From => $Param{Sender},
To => $Param{Recipient},
Header => $Param{Message}->{Header},
Body => $Param{Message}->{Body},
CommunicationLogObject => $Param{CommunicationLogObject},
);
if ( $Result->{Success} ) {
$Self->_SendSuccess(
Item => \%Param,
);
return { Status => 'Success' };
}
$Self->_SendError(
Item => \%Param,
SendResult => $Result,
);
return {
Status => 'Failed',
Message => 'Sending has Failed.'
};
}
=head1 PRIVATE INTERFACE
=head2 _SendSuccess()
This method is called after a MailQueue item is successfully sent.
It clears the item from the MailQueue, closes the communication log and
triggers a Event Notification.
my $Result = $Object->_SendSuccess(
Item => {
ID => ...,
ArticleID => ..., # optional
UserID => ...,
CommunicationLogObject => ...,
}
);
Returns 1 or undef.
=cut
sub _SendSuccess {
my ( $Self, %Param, ) = @_;
my $LogError = sub {
my $LogObject = $Kernel::OM->Get('Kernel::System::Log');
$LogObject->Log(
Priority => 'error',
Message => shift,
);
return;
};
my $Result;
my $Item = $Param{Item};
# Delete queue element/item.
$Result = $Self->Delete(
ID => $Item->{ID},
);
if ( !$Result ) {
my $LogMessage = sprintf(
'Error while deleting message with id "%s" from mail queue!',
$Item->{ID},
);
$LogError->($LogMessage);
$Item->{CommunicationLogObject}->ObjectLog(
ObjectLogType => 'Message',
Priority => 'Error',
Key => 'Kernel::System::MailQueue',
Value => $LogMessage,
);
}
$Item->{CommunicationLogObject}->ObjectLog(
ObjectLogType => 'Message',
Priority => 'Info',
Key => 'Kernel::System::MailQueue',
Value => 'Message successfully sent!',
);
$Item->{CommunicationLogObject}->ObjectLogStop(
ObjectLogType => 'Message',
Status => 'Successful',
);
$Result = $Item->{CommunicationLogObject}->CommunicationStop(
Status => 'Successful',
);
if ( !$Result ) {
$LogError->(
sprintf(
'Error while closing the communication "%s"!',
$Item->{CommunicationLogObject}->CommunicationIDGet(),
),
);
}
if ( $Item->{ArticleID} ) {
# Event Notification
$Self->_SendEventNotification(
ArticleID => $Item->{ArticleID},
Status => 'Sent',
Message => 'Mail successfully sent.',
UserID => $Item->{UserID},
);
}
return 1;
}
=head2 _SendError()
Handles Send errors.
Situations where the mail queue item is deleted:
- SMTP 5?? errors codes, considered permanent errors.
- reached maximum attempts
$Object->_SendError(
Item => ...,
SendResult => ...,
);
This always returns undef.
=cut
sub _SendError {
my ( $Self, %Param, ) = @_;
my $LogError = sub {
my $LogObject = $Kernel::OM->Get('Kernel::System::Log');
$LogObject->Log(
Priority => 'error',
Message => shift,
);
return;
};
my $Result;
my $Config = $Kernel::OM->Get('Kernel::Config')->Get('MailQueue');
my $SendResult = $Param{SendResult};
my $Item = $Param{Item};
my $ItemAttempts = $Item->{Attempts} + 1;
my $ItemMaxAttempts = $Config->{ItemMaxAttempts};
$Item->{CommunicationLogObject}->ObjectLog(
ObjectLogType => 'Message',
Priority => 'Error',
Key => 'Kernel::System::MailQueue',
Value => "Message could not be sent! Error message: $SendResult->{ErrorMessage}",
);
# If is a permanent error or reach the maximum attempts, remove the element from the queue.
if (
( $SendResult->{SMTPError} && $SendResult->{Code} && substr( $SendResult->{Code}, 0, 1 ) == 5 ) ||
( $ItemAttempts >= $ItemMaxAttempts )
)
{
$Self->_SetArticleTransmissionError(
ArticleID => $Item->{ArticleID},
Message => $SendResult->{ErrorMessage},
CommunicationLogObject => $Item->{CommunicationLogObject},
);
$Item->{CommunicationLogObject}->ObjectLog(
ObjectLogType => 'Message',
Priority => 'Error',
Key => 'Kernel::System::MailQueue',
Value => "Permanent sending problem or we reached the sending attempt limit. Message will be removed",
);
# Delete mail queue element/item.
$Result = $Self->Delete(
ID => $Item->{ID},
);
if ( !$Result ) {
my $LogMessage = sprintf(
'Error while deleting the mail-queue item "%s"!',
$Item->{ID},
);
$LogError->($LogMessage);
$Item->{CommunicationLogObject}->ObjectLog(
ObjectLogType => 'Message',
Priority => 'Error',
Key => 'Kernel::System::MailQueue',
Value => $LogMessage,
);
}
$Item->{CommunicationLogObject}->ObjectLogStop(
ObjectLogType => 'Message',
Status => 'Failed',
);
# Set Communication as failed.
$Result = $Item->{CommunicationLogObject}->CommunicationStop(
Status => 'Failed',
);
if ( !$Result ) {
$LogError->(
sprintf(
'Error while closing the communication "%s" as failed!',
$Item->{CommunicationLogObject}->CommunicationIDGet(),
),
);
}
return;
}
# Temporary errors, update due time, last-smtp-code and last-smtp-message.
my $MinutesToIncrement = $Config->{IncrementAttemptDelayInMinutes} || 1;
my $CurrentSysDTObject = $Kernel::OM->Create('Kernel::System::DateTime');
# Calcule next attempt datetime.
my $NextAttempt = $CurrentSysDTObject->Clone();
$NextAttempt->Add( Minutes => $ItemAttempts * $MinutesToIncrement );
# Update mail-queue with attempt and smtp code and message.
my %UpdateData = (
Attempts => $ItemAttempts,
DueTime => $NextAttempt->ToString(),
);
if ( $SendResult->{SMTPError} ) {
%UpdateData = (
%UpdateData,
LastSMTPCode => $SendResult->{Code},
LastSMTPMessage => $SendResult->{ErrorMessage},
);
}
$Item->{CommunicationLogObject}->ObjectLog(
ObjectLogType => 'Message',
Priority => 'Error',
Key => 'Kernel::System::MailQueue',
Value => 'Temporary problem returned from server, requeuing message for sending. Message: '
. sprintf(
"SMTPCode: %s, ErrorMessage: $SendResult->{ErrorMessage}",
$SendResult->{Code} ? $SendResult->{Code} : '-'
),
);
$Result = $Self->Update(
Filters => {
ID => $Item->{ID},
},
Data => \%UpdateData,
);
if ( !$Result ) {
my $LogMessage = sprintf(
'Error while updating mail queue element "%s" with "%s"!',
$Item->{ID},
join( ', ', map { $_ . '=' . $UpdateData{$_} } sort keys %UpdateData ),
);
$LogError->($LogMessage);
$Item->{CommunicationLogObject}->ObjectLog(
ObjectLogType => 'Message',
Priority => 'Error',
Key => 'Kernel::System::MailQueue',
Value => $LogMessage,
);
}
return;
}
=head2 _SetArticleTransmissionError()
Creates or Updates the Article Transmission Error record with the error message.
Then, fires a Notification Event.
my $Result = $Object->_SetArticleTransmissionError(
ArticleID => ...,
Message => ...,
MessageID => ...,
UserID => ...,
ForceUpdate => ...,
CommunicationLogObject => ...,
);
Returns 1 or undef.
=cut
sub _SetArticleTransmissionError {
my ( $Self, %Param, ) = @_;
my $ArticleID = $Param{ArticleID};
return 1 if !$ArticleID;
my $ArticleObject = $Kernel::OM->Get('Kernel::System::Ticket::Article');
my $ArticleBackendObject = $ArticleObject->BackendForChannel(
ChannelName => 'Email',
);
my $TicketID = $ArticleObject->TicketIDLookup( ArticleID => $ArticleID );
if ( $Param{ForceUpdate} || $ArticleBackendObject->ArticleGetTransmissionError( ArticleID => $ArticleID ) ) {
my $Result = $ArticleBackendObject->ArticleUpdateTransmissionError(
ArticleID => $ArticleID,
Message => $Param{Message},
);
if ( !$Result ) {
my $ErrorMessage = sprintf(
'Error while updating transmission error for article "%s"!',
$ArticleID,
);
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'error',
Message => $ErrorMessage,
);
$Param{CommunicationLogObject}->ObjectLog(
ObjectLogType => 'Message',
Priority => 'Error',
Key => 'Kernel::System::MailQueue',
Value => $ErrorMessage,
);
# Event Notification
$Self->_SendEventNotification(
ArticleID => $ArticleID,
Status => 'Error',
Message => $ErrorMessage,
UserID => $Param{UserID},
);
return;
}
# Event Notification
$Self->_SendEventNotification(
ArticleID => $ArticleID,
Status => 'Error',
Message => $Param{Message},
UserID => $Param{UserID},
);
return 1;
}
my $Result = $ArticleBackendObject->ArticleCreateTransmissionError(
ArticleID => $ArticleID,
MessageID => $Param{MessageID},
Message => $Param{Message},
);
if ( !$Result ) {
my $ErrorMessage = sprintf(
'Error while creating transmission error for article "%s"!',
$ArticleID,
);
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'error',
Message => $ErrorMessage,
);
$Param{CommunicationLogObject}->ObjectLog(
ObjectLogType => 'Message',
Priority => 'Error',
Key => 'Kernel::System::MailQueue',
Value => $ErrorMessage,
);
# Event Notification
$Self->_SendEventNotification(
ArticleID => $ArticleID,
Status => 'Error',
Message => $ErrorMessage,
UserID => $Param{UserID},
);
return;
}
# Event Notification
$Self->_SendEventNotification(
ArticleID => $ArticleID,
Status => 'Error',
Message => $Param{Message},
UserID => $Param{UserID},
);
return 1;
}
=head2 _SendEventNotification()
Formats a Notification and asks Event Handler to send it.
my $Result = $Object->_SendEventNotification(
ArticleID => ...,
Status => "Queued|Sent|Error",
Message => ...,
UserID => ...,
);
This returns something like:
my $Result = {
Status => 'Failed',
Message => 'Need ArticleID'
};
in case of missing or invalid arguments, or the status of the EventHandler call.
=cut
sub _SendEventNotification {
my ( $Self, %Param ) = @_;
# Check for required data.
for my $Argument (qw(ArticleID Status Message)) {
if ( !$Param{$Argument} ) {
return {
Status => 'Failed',
Message => "Need $Argument.",
};
}
}
if ( $Param{Status} !~ m{Queued|Sent|Error} ) {
return {
Status => 'Failed',
Message => 'Status unknown.',
};
}
my $ArticleObject = $Kernel::OM->Get('Kernel::System::Ticket::Article');
my $TicketID = $ArticleObject->TicketIDLookup( ArticleID => $Param{ArticleID} );
my $PostMasterUserID = $Kernel::OM->Get('Kernel::Config')->Get('PostmasterUserID') || 1;
# Event Notification
return $Self->EventHandler(
Event => 'ArticleEmailSending' . $Param{Status},
Data => {
ArticleID => $Param{ArticleID},
Status => $Param{Status},
TicketID => $TicketID,
Message => $Param{Message},
},
Transaction => 1,
UserID => $PostMasterUserID,
);
}
=head2 _FiltersSQLAndBinds()
Build the filter sql and associated binds.
my ( $FilterSQL, $Binds ) = $MailQueue->_FiltersSQLAndBinds(
ID => '...' # optional
ArticleID => '...' # optional
CommunicationID => '...' # optional
Sender => '...' # optional
Recipient => '...' # optional
Attempts => '...' # optional
);
This returns something like:
$FilterSQL = '...';
$Binds = \[...];
=cut
sub _FiltersSQLAndBinds {
my ( $Self, %Param ) = @_;
my %PossibleFilters = (
ID => {},
ArticleID => {},
#CommunicationID => {},
#CommunicationLogMessageID => {},
Sender => {
Fulltext => 1,
},
Attempts => {},
Recipient => {
Fulltext => 1,
},
LastSMTPCode => {},
LastSMTPMessage => {
Fulltext => 1,
},
);
my @FilterFields = ();
my @Bind = ();
my $DBObject = $Kernel::OM->Get('Kernel::System::DB');
POSSIBLE_FILTER:
for my $PossibleFilter ( sort keys %PossibleFilters ) {
my $Value = $Param{$PossibleFilter};
if ( !defined $Value || !length $Value ) {
next POSSIBLE_FILTER;
}
my $PossibleFilterConfig = $PossibleFilters{$PossibleFilter};
my $FilterDBName = $DBNames{$PossibleFilter};
if ( $PossibleFilterConfig->{Fulltext} ) {
my $WhereClause = $DBObject->QueryCondition(
Key => $FilterDBName,
Value => $Value,
SearchPrefix => '*',
SearchSuffix => '*',
Extended => 1, # use also " " as "&&", e.g. "bob smith" -> "bob&&smith"
);
push @FilterFields, $WhereClause;
next POSSIBLE_FILTER;
}
push @FilterFields, sprintf( '(%s = ?)', $FilterDBName );
push @Bind, \$Value;
}
my $FinalFilterSQL = join ' AND ', @FilterFields;
return ( $FinalFilterSQL, \@Bind );
}
=head2 _CheckValidEmailAddresses()
Check if the provided email address(es) is valid.
my $IsValid = $MailQueue->_CheckValidEmailAddresses(
ParamName => '...' # name of the parameter that we are checking
Addresses => '...' || [] # email addresses to validate
);
Returns 1 or undef.
=cut
sub _CheckValidEmailAddresses {
my ( $Self, %Param ) = @_;
return 1 if !$Self->{CheckEmailAddresses};
my $ParamName = $Param{ParamName};
my $Addresses = $Param{Addresses};
if ( !ref $Addresses ) {
$Addresses = [$Addresses];
}
my $CheckItemObject = $Kernel::OM->Get('Kernel::System::CheckItem');
ADDRESS:
for my $Address ( @{$Addresses} ) {
next ADDRESS if $CheckItemObject->CheckEmail(
Address => $Address,
);
my $ErrorMessage = sprintf(
'Invalid email address %s - %s: %s',
$ParamName,
$Address,
$CheckItemObject->CheckError(),
);
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'error',
Message => $ErrorMessage,
);
if ( $Param{CommunicationLogObject} ) {
$Param{CommunicationLogObject}->ObjectLog(
ObjectLogType => 'Message',
Priority => 'Error',
Key => 'Kernel::System::MailQueue',
Value => $ErrorMessage,
);
}
$Self->_SetArticleTransmissionError(
%Param,
Message => $ErrorMessage,
);
return;
}
return 1;
}
=head2 _CheckValidMessageData()
Check if the provided data is a non-empty hash-ref.
my $IsValid = $MailQueue->_CheckValidMessageData(
Data => {...}
);
Returns 1 or undef.
=cut
sub _CheckValidMessageData {
my ( $Self, %Param ) = @_;
my $Data = $Param{Data};
if ( !IsHashRefWithData($Data) ) {
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'error',
Message => 'Received no message data to check.',
);
if ( $Param{CommunicationLogObject} ) {
$Param{CommunicationLogObject}->ObjectLog(
ObjectLogType => 'Message',
Priority => 'Error',
Key => 'Kernel::System::MailQueue',
Value => 'Received no message data to check.',
);
}
return;
}
return 1;
}
=head2 _SerializeMessage()
Serialize a simple perl structure to be save in the database.
Returns an encoded or a storable string.
=cut
sub _SerializeMessage {
my ( $Self, %Param ) = @_;
my $DBObject = $Kernel::OM->Get('Kernel::System::DB');
my $StorableObject = $Kernel::OM->Get('Kernel::System::Storable');
my $Message = $StorableObject->Serialize(
Data => $Param{Message},
);
if ( !$DBObject->GetDatabaseFunction('DirectBlob') ) {
$Message = encode_base64( $Message, );
}
return $Message;
}
=head2 _DeserializeMessage()
Deserialize a simple perl structure to the original format.
=cut
sub _DeserializeMessage {
my ( $Self, %Param ) = @_;
my $DBObject = $Kernel::OM->Get('Kernel::System::DB');
my $StorableObject = $Kernel::OM->Get('Kernel::System::Storable');
my $Message = $Param{Message};
if ( !$DBObject->GetDatabaseFunction('DirectBlob') ) {
$Message = decode_base64( $Message, );
}
return $StorableObject->Deserialize(
Data => $Message,
);
}
=head2 _IsArticleAlreadyQueued()
Checks if the article is already queued.
Returns 1 or undef.
=cut
sub _IsArticleAlreadyQueued {
my ( $Self, %Param ) = @_;
my $ArticleID = $Param{ArticleID};
return if !$ArticleID;
my $LogObject = $Kernel::OM->Get('Kernel::System::Log');
my $AddCommunicationLog = sub {
return if !$Param{CommunicationLogObject};
my %LocalParam = @_;
my $CommunicationLogObject = $Param{CommunicationLogObject};
$CommunicationLogObject->ObjectLog(
ObjectLogType => 'Message',
Key => ref($Self),
Value => $LocalParam{Message},
Priority => $LocalParam{Priority},
);
return;
};
# On Error consider as queued, do not try to insert or update.
my $Error = sub {
$AddCommunicationLog->(
Message => shift,
Priority => 'Error',
);
return 1;
};
my $ErrorWhileChecking = sub {
return $Error->(
sprintf(
"Error while checking if the article '%s' is already queued!",
$ArticleID,
),
);
};
my $ErrorAlreadQueued = sub {
return $Error->(
sprintf(
"The article '%s' is already queued!",
$ArticleID,
),
);
};
# Get the queue item for the article.
my $List = $Self->List(
ArticleID => $ArticleID,
);
if ( !$List ) {
return $ErrorWhileChecking->();
}
# If it's an update get the records that will be updated.
my $ToUpdate = $Param{ToUpdate};
if ($ToUpdate) {
return $ErrorWhileChecking->() if !$ToUpdate;
my $TotalToUpdate = scalar @{$ToUpdate};
if ( $TotalToUpdate > 1 ) {
return $Error->(
"Trying to set the same ArticleID to multiple queue elements!",
);
}
if ( $TotalToUpdate == 1 && @{$List} && $List->[0]->{ID} != $ToUpdate->[0]->{ID} ) {
return $ErrorAlreadQueued->();
}
return;
}
# It's an insert, this means if the List has records, the article is already queued.
if ( @{$List} ) {
return $ErrorAlreadQueued->();
}
return;
}
=head2 _DBInsert()
Inserts a new record in the table and returns the newly record id.
Returns a number (id of the new record inserted) or undef.
=cut
sub _DBInsert {
my ( $Self, %Param, ) = @_;
my $Error = sub { return { @_, Success => 0 }; };
my $Success = sub { return { @_, Success => 1 } };
my $InsertFingerprint = sprintf(
'%s-%s',
$$,
$Kernel::OM->Get('Kernel::System::Main')->GenerateRandomString(
Length => 32,
),
);
my @Cols = qw(article_id attempts sender recipient raw_message insert_fingerprint);
my @Binds = (
\$Param{ArticleID},
\( $Param{Attempts} || 0 ),
\$Param{Sender},
\$Param{Recipient},
\$Param{Message},
\$InsertFingerprint,
);
if ( $Param{ID} ) {
unshift @Cols, 'id';
unshift @Binds, \$Param{ID};
}
my @Placeholders = map {'?'} @Cols;
push @Cols, 'create_time';
my $DBObject = $Kernel::OM->Get('Kernel::System::DB');
my $Result = $DBObject->Do(
SQL => sprintf( '
INSERT INTO mail_queue(%s)
VALUES(%s, current_timestamp )
', join( ',', @Cols ), join( ',', @Placeholders ) ),
Bind => \@Binds,
);
if ( !$Result ) {
$Param{CommunicationLogObject}->ObjectLog(
ObjectLogType => 'Message',
Priority => 'Error',
Key => 'Kernel::System::MailQueue',
Value => "Could not save serialized message data: $Param{Message}",
);
$Self->_SetArticleTransmissionError(
%Param,
Message => 'Error while creating the mail queue element!',
);
return $Error->(
Action => 'db-insert',
);
}
# Get the newly inserted record to get the id.
return $Error->(
Action => 'get-id',
) if !$DBObject->Prepare(
SQL => 'SELECT id FROM mail_queue WHERE insert_fingerprint = ?',
Bind => [ \$InsertFingerprint ],
);
my @Row = $DBObject->FetchrowArray();
return $Success->(
ID => $Row[0],
);
}
=head2 _CreateCommunicationLogLookup()
Creates the mail-queue item communication-log message association.
It will also create the association for the article if any ArticleID was passed.
Returns 1 always.
=cut
sub _SetCommunicationLogLookup {
my ( $Self, %Param, ) = @_;
my $CommunicationLogObject = $Param{CommunicationLogObject};
if ( $Param{ID} ) {
$CommunicationLogObject->ObjectLookupSet(
ObjectLogType => 'Message',
TargetObjectType => 'MailQueueItem',
TargetObjectID => $Param{ID},
);
}
# If ArticleID is present, set also a lookup for it.
if ( $Param{ArticleID} ) {
$CommunicationLogObject->ObjectLookupSet(
ObjectLogType => 'Message',
TargetObjectType => 'Article',
TargetObjectID => $Param{ArticleID},
);
}
return 1;
}
=head2 _GetCommunicationLog()
Get the communication log associated to the queue item, if not found, creates a new one.
my $CommunicationLog = $Self->_GetCommunicationLog(
ID => '...' # mail-queue item ID
);
Returns communication-log object.
=cut
sub _GetCommunicationLog {
my ( $Self, %Param, ) = @_;
my $CommunicationLogDBObj = $Kernel::OM->Get('Kernel::System::CommunicationLog::DB');
my $LookupInfo = $CommunicationLogDBObj->ObjectLookupGet(
ObjectLogType => 'Message',
TargetObjectType => 'MailQueueItem',
TargetObjectID => $Param{ID},
);
# IF for any reason we can't get the lookup information (error or no record found),
# lets create a new communication log and communication log message.
if ( !$LookupInfo || !%{$LookupInfo} ) {
my $CommunicationLogObject = $Kernel::OM->Create(
'Kernel::System::CommunicationLog',
ObjectParams => {
Transport => 'Email',
Direction => 'Outgoing',
},
);
$CommunicationLogObject->ObjectLogStart(
ObjectLogType => 'Message',
);
$CommunicationLogObject->ObjectLog(
ObjectLogType => 'Message',
Key => ref($Self),
Value => sprintf(
q{It wasn't possible to recover the communication for queued ID "%s".},
$Param{ID},
),
);
# Set the new mail-queue item communication-log lookup.
my $ComLookupUpdated = $CommunicationLogObject->ObjectLookupSet(
ObjectLogType => 'Message',
TargetObjectType => 'MailQueueItem',
TargetObjectID => $Param{ID},
);
if ( !$ComLookupUpdated ) {
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'Error',
Message => 'Error while updating the communication message for ID: ' . $Param{ID},
);
}
return $CommunicationLogObject;
}
return $Kernel::OM->Create(
'Kernel::System::CommunicationLog',
ObjectParams => {
ObjectLogID => $LookupInfo->{ObjectLogID},
},
);
}
=head1 TERMS AND CONDITIONS
This software is part of the OTRS project (L).
This software comes with ABSOLUTELY NO WARRANTY. For details, see
the enclosed file COPYING for license information (GPL). If you
did not receive this file, see L.
=cut
1;