<html><head><meta name="color-scheme" content="light dark"></head><body><pre style="word-wrap: break-word; white-space: pre-wrap;"># --
# Kernel/System/Ticket/Article.pm - global article module for OTRS kernel
# Copyright (C) 2001-2014 OTRS AG, http://otrs.com/
# --
# This software comes with ABSOLUTELY NO WARRANTY. For details, see
# the enclosed file COPYING for license information (AGPL). If you
# did not receive this file, see http://www.gnu.org/licenses/agpl.txt.
# --

package Kernel::System::Ticket::Article;

use strict;
use warnings;

use Kernel::System::HTMLUtils;
use Kernel::System::PostMaster::LoopProtection;
use Kernel::System::TemplateGenerator;
use Kernel::System::Notification;
use Kernel::System::EmailParser;

use Kernel::System::VariableCheck qw(:all);

=head1 NAME

Kernel::System::Ticket::Article - sub module of Kernel::System::Ticket

=head1 SYNOPSIS

All article functions.

=head1 PUBLIC INTERFACE

=over 4

=cut

=item ArticleCreate()

create an article

    my $ArticleID = $TicketObject-&gt;ArticleCreate(
        TicketID         =&gt; 123,
        ArticleType      =&gt; 'note-internal',                        # email-external|email-internal|phone|fax|...
        SenderType       =&gt; 'agent',                                # agent|system|customer
        From             =&gt; 'Some Agent &lt;email@example.com&gt;',       # not required but useful
        To               =&gt; 'Some Customer A &lt;customer-a@example.com&gt;', # not required but useful
        Cc               =&gt; 'Some Customer B &lt;customer-b@example.com&gt;', # not required but useful
        ReplyTo          =&gt; 'Some Customer B &lt;customer-b@example.com&gt;', # not required
        Subject          =&gt; 'some short description',               # required
        Body             =&gt; 'the message text',                     # required
        MessageID        =&gt; '&lt;asdasdasd.123@example.com&gt;',          # not required but useful
        InReplyTo        =&gt; '&lt;asdasdasd.12@example.com&gt;',           # not required but useful
        References       =&gt; '&lt;asdasdasd.1@example.com&gt; &lt;asdasdasd.12@example.com&gt;', # not required but useful
        ContentType      =&gt; 'text/plain; charset=ISO-8859-15',      # or optional Charset &amp; MimeType
        HistoryType      =&gt; 'OwnerUpdate',                          # EmailCustomer|Move|AddNote|PriorityUpdate|WebRequestCustomer|...
        HistoryComment   =&gt; 'Some free text!',
        UserID           =&gt; 123,
        Attachment =&gt; [
            {
                Content     =&gt; $Content,
                ContentType =&gt; $ContentType,
                Filename    =&gt; 'lala.txt',
            },
            {
                Content     =&gt; $Content,
                ContentType =&gt; $ContentType,
                Filename    =&gt; 'lala1.txt',
            },
        ],
        NoAgentNotify    =&gt; 0,                                      # if you don't want to send agent notifications
        AutoResponseType =&gt; 'auto reply'                            # auto reject|auto follow up|auto reply/new ticket|auto remove

        ForceNotificationToUserID   =&gt; [ 1, 43, 56 ],               # if you want to force somebody
        ExcludeNotificationToUserID =&gt; [ 43,56 ],                   # if you want full exclude somebody from notfications,
                                                                    # will also be removed in To: line of article,
                                                                    # higher prio as ForceNotificationToUserID
        ExcludeMuteNotificationToUserID =&gt; [ 43,56 ],               # the same as ExcludeNotificationToUserID but only the
                                                                    # sending gets muted, agent will still shown in To:
                                                                    # line of article
    );

example with "Charset &amp; MimeType" and no "ContentType"

    my $ArticleID = $TicketObject-&gt;ArticleCreate(
        TicketID         =&gt; 123,
        ArticleType      =&gt; 'note-internal',                        # email-external|email-internal|phone|fax|...
        SenderType       =&gt; 'agent',                                # agent|system|customer
        From             =&gt; 'Some Agent &lt;email@example.com&gt;',       # not required but useful
        To               =&gt; 'Some Customer A &lt;customer-a@example.com&gt;', # not required but useful
        Subject          =&gt; 'some short description',               # required
        Body             =&gt; 'the message text',                     # required
        Charset          =&gt; 'ISO-8859-15',
        MimeType         =&gt; 'text/plain',
        HistoryType      =&gt; 'OwnerUpdate',                          # EmailCustomer|Move|AddNote|PriorityUpdate|WebRequestCustomer|...
        HistoryComment   =&gt; 'Some free text!',
        UserID           =&gt; 123,
    );

Events:
    ArticleCreate

=cut

sub ArticleCreate {
    my ( $Self, %Param ) = @_;

    my $ValidID = $Param{ValidID} || 1;
    my $IncomingTime = $Self-&gt;{TimeObject}-&gt;SystemTime();

    # create ArticleContentPath
    if ( !$Self-&gt;{ArticleContentPath} ) {
        $Self-&gt;{LogObject}-&gt;Log( Priority =&gt; 'error', Message =&gt; 'Need ArticleContentPath!' );
        return;
    }

    # lookups if no ids are passed
    if ( $Param{ArticleType} &amp;&amp; !$Param{ArticleTypeID} ) {
        $Param{ArticleTypeID} = $Self-&gt;ArticleTypeLookup( ArticleType =&gt; $Param{ArticleType} );
    }
    if ( $Param{SenderType} &amp;&amp; !$Param{SenderTypeID} ) {
        $Param{SenderTypeID} = $Self-&gt;ArticleSenderTypeLookup( SenderType =&gt; $Param{SenderType} );
    }

    # check needed stuff
    for (qw(TicketID UserID ArticleTypeID SenderTypeID HistoryType HistoryComment)) {
        if ( !$Param{$_} ) {
            $Self-&gt;{LogObject}-&gt;Log( Priority =&gt; 'error', Message =&gt; "Need $_!" );
            return;
        }
    }

    # check ContentType vs. Charset &amp; MimeType
    if ( !$Param{ContentType} ) {
        for (qw(Charset MimeType)) {
            if ( !$Param{$_} ) {
                $Self-&gt;{LogObject}-&gt;Log( Priority =&gt; 'error', Message =&gt; "Need $_!" );
                return;
            }
        }
        $Param{ContentType} = "$Param{MimeType}; charset=$Param{Charset}";
    }
    else {
        for (qw(ContentType)) {
            if ( !$Param{$_} ) {
                $Self-&gt;{LogObject}-&gt;Log( Priority =&gt; 'error', Message =&gt; "Need $_!" );
                return;
            }
        }
        $Param{Charset} = '';
        if ( $Param{ContentType} =~ /charset=/i ) {
            $Param{Charset} = $Param{ContentType};
            $Param{Charset} =~ s/.+?charset=("|'|)(\w+)/$2/gi;
            $Param{Charset} =~ s/"|'//g;
            $Param{Charset} =~ s/(.+?);.*/$1/g;

        }
        $Param{MimeType} = '';
        if ( $Param{ContentType} =~ /^(\w+\/\w+)/i ) {
            $Param{MimeType} = $1;
            $Param{MimeType} =~ s/"|'//g;
        }
    }

    # for the event handler, before any actions have taken place
    my %OldTicketData = $Self-&gt;TicketGet(
        TicketID      =&gt; $Param{TicketID},
        DynamicFields =&gt; 1,
    );

    my $HTMLUtilsObject = Kernel::System::HTMLUtils-&gt;new(
        LogObject    =&gt; $Self-&gt;{LogObject},
        ConfigObject =&gt; $Self-&gt;{ConfigObject},
        MainObject   =&gt; $Self-&gt;{MainObject},
        EncodeObject =&gt; $Self-&gt;{EncodeObject},
    );

    # add 'no body' if there is no body there!
    my @AttachmentConvert;
    if ( !length $Param{Body} ) {    # allow '0' as body
        $Param{Body} = 'No body';
    }

    # process html article
    elsif ( $Param{MimeType} =~ /text\/html/i ) {

        # add html article as attachment
        my $Attach = {
            Content     =&gt; $Param{Body},
            ContentType =&gt; "text/html; charset=\"$Param{Charset}\"",
            Filename    =&gt; 'file-2',
        };
        push @AttachmentConvert, $Attach;

        # get ascii body
        $Param{MimeType} = 'text/plain';
        $Param{ContentType} =~ s/html/plain/i;
        $Param{Body} = $HTMLUtilsObject-&gt;ToAscii(
            String =&gt; $Param{Body},
        );
    }

    # if body isn't text, attach body as attachment (mostly done by OE) :-/
    elsif ( $Param{MimeType} &amp;&amp; $Param{MimeType} !~ /\btext\b/i ) {

        # add non text as attachment
        my $FileName = 'unknown';
        if ( $Param{ContentType} =~ /name="(.+?)"/i ) {
            $FileName = $1;
        }
        my $Attach = {
            Content     =&gt; $Param{Body},
            ContentType =&gt; $Param{ContentType},
            Filename    =&gt; $FileName,
        };
        push @{ $Param{Attachment} }, $Attach;

        # set ascii body
        $Param{MimeType}    = 'text/plain';
        $Param{ContentType} = 'text/plain';
        $Param{Body}        = '- no text message =&gt; see attachment -';
    }

    # fix some bad stuff from some browsers (Opera)!
    else {
        $Param{Body} =~ s/(\n\r|\r\r\n|\r\n)/\n/g;
    }

    # strip not wanted stuff
    for (qw(From To Cc Subject MessageID InReplyTo References ReplyTo)) {
        if ( defined $Param{$_} ) {
            $Param{$_} =~ s/\n|\r//g;
        }
        else {
            $Param{$_} = '';
        }
    }
    for (qw(InReplyTo References)) {
        next if !$Param{$_};
        $Param{$_} = substr( $Param{$_}, 0, 3800 );
    }

    # check if this is the first article (for notifications)
    my @Index = $Self-&gt;ArticleIndex( TicketID =&gt; $Param{TicketID} );
    my $FirstArticle = scalar @Index ? 0 : 1;

    # calculate MD5 of Message ID
    if ( $Param{MessageID} ) {
        $Param{MD5} = $Self-&gt;{MainObject}-&gt;MD5sum( String =&gt; $Param{MessageID} );
    }

    # if the original article body contains just one pasted picture and no text, at this point of
    # the code the body is an empty string, Oracle databases will transform the empty string value
    # to NULL and will try to insert a NULL value in a field that should not be NULL. see bug 7533.
    if (
        $Self-&gt;{DBObject}-&gt;GetDatabaseFunction('Type') eq 'oracle'
        &amp;&amp; defined $Param{Body}
        &amp;&amp; !$Param{Body}
        )
    {
        $Param{Body} = ' ';
    }

    # do db insert
    return if !$Self-&gt;{DBObject}-&gt;Do(
        SQL =&gt; 'INSERT INTO article '
            . '(ticket_id, article_type_id, article_sender_type_id, a_from, a_reply_to, a_to, '
            . 'a_cc, a_subject, a_message_id, a_message_id_md5, a_in_reply_to, a_references, a_body, a_content_type, '
            . 'content_path, valid_id, incoming_time, create_time, create_by, change_time, change_by) '
            . 'VALUES '
            . '(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, current_timestamp, ?, current_timestamp, ?)',
        Bind =&gt; [
            \$Param{TicketID}, \$Param{ArticleTypeID}, \$Param{SenderTypeID},
            \$Param{From},     \$Param{ReplyTo},       \$Param{To},
            \$Param{Cc},       \$Param{Subject},       \$Param{MessageID},
            \$Param{MD5},
            \$Param{InReplyTo}, \$Param{References}, \$Param{Body},
            \$Param{ContentType}, \$Self-&gt;{ArticleContentPath}, \$ValidID,
            \$IncomingTime, \$Param{UserID}, \$Param{UserID},
        ],
    );

    # get article id
    my $ArticleID = $Self-&gt;_ArticleGetId(
        TicketID     =&gt; $Param{TicketID},
        MessageID    =&gt; $Param{MessageID},
        From         =&gt; $Param{From},
        Subject      =&gt; $Param{Subject},
        IncomingTime =&gt; $IncomingTime
    );

    # return if there is not article created
    if ( !$ArticleID ) {
        $Self-&gt;{LogObject}-&gt;Log(
            Priority =&gt; 'error',
            Message  =&gt; 'Can\'t get ArticleID from INSERT!',
        );
        return;
    }

    # check for base64 encoded images in html body and upload them
    for my $Attachment (@AttachmentConvert) {

        if (
            $Attachment-&gt;{ContentType} eq "text/html; charset=\"$Param{Charset}\""
            &amp;&amp; $Attachment-&gt;{Filename} eq 'file-2'
            )
        {
            $HTMLUtilsObject-&gt;EmbeddedImagesExtract(
                DocumentRef    =&gt; \$Attachment-&gt;{Content},
                AttachmentsRef =&gt; \@AttachmentConvert,
            );
        }
    }

    # add converted attachments
    for my $Attachment (@AttachmentConvert) {
        $Self-&gt;ArticleWriteAttachment(
            %{$Attachment},
            ArticleID =&gt; $ArticleID,
            UserID    =&gt; $Param{UserID},
        );
    }

    # add attachments
    if ( $Param{Attachment} ) {
        for my $Attachment ( @{ $Param{Attachment} } ) {
            $Self-&gt;ArticleWriteAttachment(
                %{$Attachment},
                ArticleID =&gt; $ArticleID,
                UserID    =&gt; $Param{UserID},
            );
        }
    }

    $Self-&gt;_TicketCacheClear( TicketID =&gt; $Param{TicketID} );

    # add history row
    $Self-&gt;HistoryAdd(
        ArticleID    =&gt; $ArticleID,
        TicketID     =&gt; $Param{TicketID},
        CreateUserID =&gt; $Param{UserID},
        HistoryType  =&gt; $Param{HistoryType},
        Name         =&gt; $Param{HistoryComment},
    );

    # event
    $Self-&gt;EventHandler(
        Event =&gt; 'ArticleCreate',
        Data  =&gt; {
            ArticleID     =&gt; $ArticleID,
            TicketID      =&gt; $Param{TicketID},
            OldTicketData =&gt; \%OldTicketData,
        },
        UserID =&gt; $Param{UserID},
    );

    # reset unlock if needed
    if ( !$Param{SenderType} ) {
        $Param{SenderType} = $Self-&gt;ArticleSenderTypeLookup( SenderTypeID =&gt; $Param{SenderTypeID} );
    }
    if ( !$Param{ArticleType} ) {
        $Param{ArticleType} = $Self-&gt;ArticleTypeLookup( ArticleTypeID =&gt; $Param{ArticleTypeID} );
    }

    # reset unlock time if customer sent an update
    if ( $Param{SenderType} eq 'customer' ) {

        # check if latest article comes from customer
        my $LastSender = '';
        return if !$Self-&gt;{DBObject}-&gt;Prepare(
            SQL =&gt; 'SELECT ast.name FROM article art, article_sender_type ast WHERE '
                . ' art.ticket_id = ? AND art.id NOT IN (?) AND '
                . ' art.article_sender_type_id = ast.id ORDER BY art.create_time ASC',
            Bind =&gt; [ \$Param{TicketID}, \$ArticleID ],
        );
        while ( my @Row = $Self-&gt;{DBObject}-&gt;FetchrowArray() ) {
            if ( $Row[0] ne 'system' ) {
                $LastSender = $Row[0];
            }
        }
        if ( $LastSender eq 'agent' ) {
            $Self-&gt;TicketUnlockTimeoutUpdate(
                UnlockTimeout =&gt; $Self-&gt;{TimeObject}-&gt;SystemTime(),
                TicketID      =&gt; $Param{TicketID},
                UserID        =&gt; $Param{UserID},
            );
        }
    }

    # check if latest article is sent to customer
    elsif (
        $Param{SenderType} eq 'agent'
        &amp;&amp; $Param{ArticleType} =~ /email-ext|phone|fax|sms|note-ext/
        )
    {
        $Self-&gt;TicketUnlockTimeoutUpdate(
            UnlockTimeout =&gt; $Self-&gt;{TimeObject}-&gt;SystemTime(),
            TicketID      =&gt; $Param{TicketID},
            UserID        =&gt; $Param{UserID},
        );
    }

    # send auto response
    if ( $Param{AutoResponseType} ) {
        $Self-&gt;SendAutoResponse(
            OrigHeader       =&gt; $Param{OrigHeader},
            TicketID         =&gt; $Param{TicketID},
            UserID           =&gt; $Param{UserID},
            AutoResponseType =&gt; $Param{AutoResponseType},
        );
    }

    # send no agent notification!?
    return $ArticleID if $Param{NoAgentNotify};

    my %Ticket = $Self-&gt;TicketGet(
        TicketID      =&gt; $Param{TicketID},
        DynamicFields =&gt; 0,
    );

    # remember already sent agent notifications
    my %AlreadySent;

    # remember agent to exclude notifications
    my %DoNotSend;
    if ( $Param{ExcludeNotificationToUserID} &amp;&amp; ref $Param{ExcludeNotificationToUserID} eq 'ARRAY' )
    {
        for my $UserID ( @{ $Param{ExcludeNotificationToUserID} } ) {
            $DoNotSend{$UserID} = 1;
        }
    }

    # remember agent to exclude notifications / already sent
    my %DoNotSendMute;
    if (
        $Param{ExcludeMuteNotificationToUserID}
        &amp;&amp; ref $Param{ExcludeMuteNotificationToUserID} eq 'ARRAY'
        )
    {
        for my $UserID ( @{ $Param{ExcludeMuteNotificationToUserID} } ) {
            $DoNotSendMute{$UserID} = 1;
        }
    }

    # send agent notification on ticket create
    if (
        $FirstArticle &amp;&amp;
        $Param{HistoryType}
        =~ /^(EmailAgent|EmailCustomer|PhoneCallCustomer|WebRequestCustomer|SystemRequest)$/i
        )
    {
        for my $UserID ( $Self-&gt;GetSubscribedUserIDsByQueueID( QueueID =&gt; $Ticket{QueueID} ) ) {

            # do not send to this user
            next if $DoNotSend{$UserID};

            # check if already sent
            next if $AlreadySent{$UserID};

            # check personal settings
            my %UserData = $Self-&gt;{UserObject}-&gt;GetUserData(
                UserID =&gt; $UserID,
                Valid  =&gt; 1,
            );
            next if !$UserData{UserSendNewTicketNotification};

            # remember to have sent
            $AlreadySent{$UserID} = 1;

            # do not send to this user (mute)
            next if $DoNotSendMute{$UserID};

            # send notification
            $Self-&gt;SendAgentNotification(
                Type                  =&gt; $Param{HistoryType},
                RecipientID           =&gt; $UserID,
                CustomerMessageParams =&gt; {%Param},
                TicketID              =&gt; $Param{TicketID},
                Queue                 =&gt; $Param{Queue},
                UserID                =&gt; $Param{UserID},
            );
        }
    }

    # send agent notification on adding a note
    elsif ( $Param{HistoryType} =~ /^AddNote$/i ) {

        # send notification to owner/responsible/watcher
        my @UserIDs = $Ticket{OwnerID};
        if ( $Self-&gt;{ConfigObject}-&gt;Get('Ticket::Responsible') ) {
            push @UserIDs, $Ticket{ResponsibleID};
        }
        push @UserIDs, $Self-&gt;TicketWatchGet(
            TicketID =&gt; $Param{TicketID},
            Notify   =&gt; 1,
            Result   =&gt; 'ARRAY',
        );
        for my $UserID (@UserIDs) {
            next if !$UserID;
            next if $UserID == 1;
            next if $UserID eq $Param{UserID};

            # do not send to this user
            next if $DoNotSend{$UserID};

            # check if alreay sent
            next if $AlreadySent{$UserID};

            # remember already sent info
            $AlreadySent{$UserID} = 1;

            # do not send to this user (mute)
            next if $DoNotSendMute{$UserID};

            # send notification
            $Self-&gt;SendAgentNotification(
                Type                  =&gt; $Param{HistoryType},
                RecipientID           =&gt; $UserID,
                CustomerMessageParams =&gt; {%Param},
                TicketID              =&gt; $Param{TicketID},
                Queue                 =&gt; $Param{Queue},
                UserID                =&gt; $Param{UserID},
            );
        }
    }

    # send agent notification on follow up
    elsif ( $Param{HistoryType} =~ /^FollowUp$/i ) {

        # send agent notification to all agents or only to owner
        if ( $Ticket{OwnerID} == 1 || $Ticket{Lock} eq 'unlock' ) {
            my @OwnerIDs;
            if ( $Self-&gt;{ConfigObject}-&gt;Get('PostmasterFollowUpOnUnlockAgentNotifyOnlyToOwner') ) {
                @OwnerIDs = ( $Ticket{OwnerID} );
            }
            else {
                @OwnerIDs = $Self-&gt;GetSubscribedUserIDsByQueueID( QueueID =&gt; $Ticket{QueueID} );
                push @OwnerIDs, $Self-&gt;TicketWatchGet(
                    TicketID =&gt; $Param{TicketID},
                    Notify   =&gt; 1,
                    Result   =&gt; 'ARRAY',
                );

                # add also owner to be notified
                push @OwnerIDs, $Ticket{OwnerID};
            }
            for my $UserID (@OwnerIDs) {
                next if !$UserID;
                next if $UserID == 1;
                next if $UserID eq $Param{UserID};

                # do not send to this user
                next if $DoNotSend{$UserID};

                # check if alreay sent
                next if $AlreadySent{$UserID};

                # check personal settings
                my %UserData = $Self-&gt;{UserObject}-&gt;GetUserData(
                    UserID =&gt; $UserID,
                    Valid  =&gt; 1,
                );
                next if !$UserData{UserSendFollowUpNotification};

                # remember already sent info
                $AlreadySent{$UserID} = 1;

                # do not send to this user (mute)
                next if $DoNotSendMute{$UserID};

                # send notification
                $Self-&gt;SendAgentNotification(
                    Type                  =&gt; $Param{HistoryType},
                    RecipientID           =&gt; $UserID,
                    CustomerMessageParams =&gt; {%Param},
                    TicketID              =&gt; $Param{TicketID},
                    Queue                 =&gt; $Param{Queue},
                    UserID                =&gt; $Param{UserID},
                );
            }
        }

        # send owner/responsible/watcher notification the agents who locked the ticket
        else {
            my @UserIDs = $Ticket{OwnerID};
            if ( $Self-&gt;{ConfigObject}-&gt;Get('Ticket::Responsible') ) {
                push @UserIDs, $Ticket{ResponsibleID};
            }
            push @UserIDs, $Self-&gt;TicketWatchGet(
                TicketID =&gt; $Param{TicketID},
                Notify   =&gt; 1,
                Result   =&gt; 'ARRAY',
            );
            for my $UserID (@UserIDs) {
                next if !$UserID;
                next if $UserID == 1;
                next if $UserID eq $Param{UserID};

                # do not send to this user
                next if $DoNotSend{$UserID};

                # check if alreay sent
                next if $AlreadySent{$UserID};

                # check personal settings
                my %UserData = $Self-&gt;{UserObject}-&gt;GetUserData(
                    UserID =&gt; $UserID,
                    Valid  =&gt; 1,
                );
                next if !$UserData{UserSendFollowUpNotification};

                # remember already sent info
                $AlreadySent{$UserID} = 1;

                # do not send to this user (mute)
                next if $DoNotSendMute{$UserID};

                # send notification
                $Self-&gt;SendAgentNotification(
                    Type                  =&gt; $Param{HistoryType},
                    RecipientID           =&gt; $UserID,
                    CustomerMessageParams =&gt; {%Param},
                    TicketID              =&gt; $Param{TicketID},
                    Queue                 =&gt; $Param{Queue},
                    UserID                =&gt; $Param{UserID},
                );
            }

            # send the rest of agents follow ups
            for my $UserID ( $Self-&gt;GetSubscribedUserIDsByQueueID( QueueID =&gt; $Ticket{QueueID} ) ) {
                next if !$UserID;
                next if $UserID == 1;
                next if $UserID eq $Param{UserID};

                # do not send to this user
                next if $DoNotSend{$UserID};

                # check if alreay sent
                next if $AlreadySent{$UserID};

                # check personal settings
                my %UserData = $Self-&gt;{UserObject}-&gt;GetUserData(
                    UserID =&gt; $UserID,
                    Valid  =&gt; 1,
                );
                if (
                    $UserData{UserSendFollowUpNotification}
                    &amp;&amp; $UserData{UserSendFollowUpNotification} == 2
                    &amp;&amp; $Ticket{OwnerID} ne 1
                    &amp;&amp; $Ticket{OwnerID} ne $Param{UserID}
                    &amp;&amp; $Ticket{OwnerID} ne $UserData{UserID}
                    )
                {

                    # remember already sent info
                    $AlreadySent{$UserID} = 1;

                    # do not send to this user (mute)
                    next if $DoNotSendMute{$UserID};

                    # send notification
                    $Self-&gt;SendAgentNotification(
                        Type                  =&gt; $Param{HistoryType},
                        RecipientID           =&gt; $UserID,
                        CustomerMessageParams =&gt; {%Param},
                        TicketID              =&gt; $Param{TicketID},
                        Queue                 =&gt; $Param{Queue},
                        UserID                =&gt; $Param{UserID},
                    );
                }
            }
        }
    }

    # send forced notifications
    if ( $Param{ForceNotificationToUserID} &amp;&amp; ref $Param{ForceNotificationToUserID} eq 'ARRAY' ) {
        for my $UserID ( @{ $Param{ForceNotificationToUserID} } ) {

            # do not send to this user
            next if $DoNotSend{$UserID};

            # check if alreay sent
            next if $AlreadySent{$UserID};

            # remember already sent info
            $AlreadySent{$UserID} = 1;

            # do not send to this user (mute)
            next if $DoNotSendMute{$UserID};

            # send notification
            $Self-&gt;SendAgentNotification(
                Type                  =&gt; $Param{HistoryType},
                RecipientID           =&gt; $UserID,
                CustomerMessageParams =&gt; {%Param},
                TicketID              =&gt; $Param{TicketID},
                UserID                =&gt; $Param{UserID},
            );
        }
    }

    # update note to: field
    if (%AlreadySent) {
        if ( !$Param{ArticleType} ) {
            $Param{ArticleType} = $Self-&gt;ArticleTypeLookup(
                ArticleTypeID =&gt; $Param{ArticleTypeID},
            );
        }
        if ( $Param{ArticleType} =~ /^note\-/ &amp;&amp; $Param{UserID} ne 1 ) {
            my $NewTo = $Param{To} || '';
            for my $UserID ( sort keys %AlreadySent ) {
                my %UserData = $Self-&gt;{UserObject}-&gt;GetUserData(
                    UserID =&gt; $UserID,
                    Valid  =&gt; 1,
                );
                if ($NewTo) {
                    $NewTo .= ', ';
                }
                $NewTo .= "$UserData{UserFirstname} $UserData{UserLastname} &lt;$UserData{UserEmail}&gt;";
            }
            if ($NewTo) {
                $Self-&gt;{DBObject}-&gt;Do(
                    SQL =&gt; 'UPDATE article SET a_to = ? WHERE id = ?',
                    Bind =&gt; [ \$NewTo, \$ArticleID ],
                );
            }
        }
    }

    # return ArticleID
    return $ArticleID;
}

=item ArticleGetTicketIDOfMessageID()

get ticket id of given message id

    my $TicketID = $TicketObject-&gt;ArticleGetTicketIDOfMessageID(
        MessageID =&gt; '&lt;13231231.1231231.32131231@example.com&gt;',
    );

=cut

sub ArticleGetTicketIDOfMessageID {
    my ( $Self, %Param ) = @_;

    # check needed stuff
    if ( !$Param{MessageID} ) {
        $Self-&gt;{LogObject}-&gt;Log( Priority =&gt; 'error', Message =&gt; 'Need MessageID!' );
        return;
    }
    my $MD5 = $Self-&gt;{MainObject}-&gt;MD5sum( String =&gt; $Param{MessageID} );

    # sql query
    return if !$Self-&gt;{DBObject}-&gt;Prepare(
        SQL   =&gt; 'SELECT ticket_id FROM article WHERE a_message_id_md5 = ?',
        Bind  =&gt; [ \$MD5 ],
        Limit =&gt; 10,
    );
    my $TicketID;
    my $Count = 0;
    while ( my @Row = $Self-&gt;{DBObject}-&gt;FetchrowArray() ) {
        $Count++;
        $TicketID = $Row[0];
    }

    # no reference found
    return if $Count == 0;

    # one found
    return $TicketID if $Count == 1;

    # more than one found! that should not be, a message_id should be unique!
    $Self-&gt;{LogObject}-&gt;Log(
        Priority =&gt; 'notice',
        Message  =&gt; "The MessageID '$Param{MessageID}' is in your database "
            . "more than one time! That should not be, a message_id should be unique!",
    );
    return;
}

=item ArticleGetContentPath()

get article content path

    my $Path = $TicketObject-&gt;ArticleGetContentPath(
        ArticleID =&gt; 123,
    );

=cut

sub ArticleGetContentPath {
    my ( $Self, %Param ) = @_;

    # check needed stuff
    if ( !$Param{ArticleID} ) {
        $Self-&gt;{LogObject}-&gt;Log( Priority =&gt; 'error', Message =&gt; 'Need ArticleID!' );
        return;
    }

    # check key
    my $CacheKey = 'ArticleGetContentPath::' . $Param{ArticleID};

    # check cache
    my $Cache = $Self-&gt;{CacheInternalObject}-&gt;Get( Key =&gt; $CacheKey );
    return $Cache if $Cache;

    # sql query
    return if !$Self-&gt;{DBObject}-&gt;Prepare(
        SQL  =&gt; 'SELECT content_path FROM article WHERE id = ?',
        Bind =&gt; [ \$Param{ArticleID} ],
    );
    my $Result;
    while ( my @Row = $Self-&gt;{DBObject}-&gt;FetchrowArray() ) {
        $Result = $Row[0];
    }

    # set cache
    $Self-&gt;{CacheInternalObject}-&gt;Set( Key =&gt; $CacheKey, Value =&gt; $Result );

    # return
    return $Result;
}

=item ArticleSenderTypeList()

get a article sender type list

    my @ArticleSenderTypeList = $TicketObject-&gt;ArticleSenderTypeList(
        Result =&gt; 'ARRAY', # optional, ARRAY|HASH
    );

=cut

sub ArticleSenderTypeList {
    my ( $Self, %Param ) = @_;

    return if !$Self-&gt;{DBObject}-&gt;Prepare(
        SQL =&gt; "SELECT id, name FROM article_sender_type WHERE "
            . "valid_id IN (${\(join ', ', $Self-&gt;{ValidObject}-&gt;ValidIDsGet())})",
    );

    my @Array;
    my %Hash;
    while ( my @Row = $Self-&gt;{DBObject}-&gt;FetchrowArray() ) {
        push @Array, $Row[1];
        $Hash{ $Row[0] } = $Row[1];
    }
    if ( $Param{Result} &amp;&amp; $Param{Result} eq 'HASH' ) {
        return %Hash;
    }
    return @Array;

}

=item ArticleSenderTypeLookup()

article sender lookup

    my $SenderTypeID = $TicketObject-&gt;ArticleSenderTypeLookup(
        SenderType =&gt; 'customer', # customer|system|agent
    );

    my $SenderType = $TicketObject-&gt;ArticleSenderTypeLookup(
        SenderTypeID =&gt; 1,
    );

=cut

sub ArticleSenderTypeLookup {
    my ( $Self, %Param ) = @_;

    # check needed stuff
    if ( !$Param{SenderType} &amp;&amp; !$Param{SenderTypeID} ) {
        $Self-&gt;{LogObject}-&gt;Log(
            Priority =&gt; 'error',
            Message  =&gt; 'Need SenderType or SenderTypeID!',
        );
        return;
    }

    # get key
    my $Key;
    my $CacheKey;
    if ( $Param{SenderType} ) {
        $Key      = $Param{SenderType};
        $CacheKey = 'ArticleSenderTypeLookup::' . $Param{SenderType};
    }
    else {
        $Key      = $Param{SenderTypeID};
        $CacheKey = 'ArticleSenderTypeLookup::' . $Param{SenderTypeID};
    }

    # check cache
    my $Cache = $Self-&gt;{CacheInternalObject}-&gt;Get( Key =&gt; $CacheKey );
    return $Cache if $Cache;

    # get data
    if ( $Param{SenderType} ) {
        return if !$Self-&gt;{DBObject}-&gt;Prepare(
            SQL  =&gt; 'SELECT id FROM article_sender_type WHERE name = ?',
            Bind =&gt; [ \$Param{SenderType} ],
        );
    }
    else {
        return if !$Self-&gt;{DBObject}-&gt;Prepare(
            SQL  =&gt; 'SELECT name FROM article_sender_type WHERE id = ?',
            Bind =&gt; [ \$Param{SenderTypeID} ],
        );
    }

    # store result
    my $Result;
    while ( my @Row = $Self-&gt;{DBObject}-&gt;FetchrowArray() ) {
        $Result = $Row[0];
    }

    # check if data exists
    if ( !$Result ) {
        $Self-&gt;{LogObject}-&gt;Log(
            Priority =&gt; 'error',
            Message  =&gt; "Found no SenderType(ID) for $Key!",
        );
        return;
    }

    # set cache
    $Self-&gt;{CacheInternalObject}-&gt;Set( Key =&gt; $CacheKey, Value =&gt; $Result );

    # return
    return $Result;
}

=item ArticleTypeLookup()

article type lookup

    my $ArticleTypeID = $TicketObject-&gt;ArticleTypeLookup(
        ArticleType =&gt; 'webrequest-customer', # note-internal|...
    );

    my $ArticleType = $TicketObject-&gt;ArticleTypeLookup(
        ArticleTypeID =&gt; 1,
    );

=cut

sub ArticleTypeLookup {
    my ( $Self, %Param ) = @_;

    # check needed stuff
    if ( !$Param{ArticleType} &amp;&amp; !$Param{ArticleTypeID} ) {
        $Self-&gt;{LogObject}-&gt;Log(
            Priority =&gt; 'error',
            Message  =&gt; 'Need ArticleType or ArticleTypeID!',
        );
        return;
    }

    # get key
    my $Key;
    my $CacheKey;
    if ( $Param{ArticleType} ) {
        $Key      = $Param{ArticleType};
        $CacheKey = 'ArticleTypeLookup::ArticleType::' . $Param{ArticleType};
    }
    else {
        $Key      = $Param{ArticleTypeID};
        $CacheKey = 'ArticleTypeLookup::ArticleTypeID::' . $Param{ArticleTypeID};
    }

    # check cache
    my $Cache = $Self-&gt;{CacheInternalObject}-&gt;Get( Key =&gt; $CacheKey );
    return $Cache if $Cache;

    # get data
    if ( $Param{ArticleType} ) {
        return if !$Self-&gt;{DBObject}-&gt;Prepare(
            SQL  =&gt; 'SELECT id FROM article_type WHERE name = ?',
            Bind =&gt; [ \$Param{ArticleType} ],
        );
    }
    else {
        return if !$Self-&gt;{DBObject}-&gt;Prepare(
            SQL  =&gt; 'SELECT name FROM article_type WHERE id = ?',
            Bind =&gt; [ \$Param{ArticleTypeID} ],
        );
    }

    # store result
    my $Result;
    while ( my @Row = $Self-&gt;{DBObject}-&gt;FetchrowArray() ) {
        $Result = $Row[0];
    }

    # check if data exists
    if ( !$Result ) {
        $Self-&gt;{LogObject}-&gt;Log(
            Priority =&gt; 'error',
            Message  =&gt; "Found no ArticleType(ID) for $Key!",
        );
        return;
    }

    # set cache
    $Self-&gt;{CacheInternalObject}-&gt;Set( Key =&gt; $CacheKey, Value =&gt; $Result );

    # return
    return $Result;
}

=item ArticleTypeList()

get a article type list

    my @ArticleTypeList = $TicketObject-&gt;ArticleTypeList(
        Result =&gt; 'ARRAY', # optional, ARRAY|HASH
    );

    # to get only article types visible for customers
    my @ArticleTypeList = $TicketObject-&gt;ArticleTypeList(
        Result =&gt; 'ARRAY',    # optional, ARRAY|HASH
        Type   =&gt; 'Customer', # optional to get only customer viewable article types
    );

=cut

sub ArticleTypeList {
    my ( $Self, %Param ) = @_;

    return if !$Self-&gt;{DBObject}-&gt;Prepare(
        SQL =&gt; "SELECT id, name FROM article_type WHERE "
            . "valid_id IN (${\(join ', ', $Self-&gt;{ValidObject}-&gt;ValidIDsGet())})",
    );
    my @Array;
    my %Hash;
    while ( my @Row = $Self-&gt;{DBObject}-&gt;FetchrowArray() ) {
        if ( $Param{Type} &amp;&amp; $Param{Type} eq 'Customer' ) {
            if ( $Row[1] !~ /int/i ) {
                push @Array, $Row[1];
                $Hash{ $Row[0] } = $Row[1];
            }
        }
        else {
            push @Array, $Row[1];
            $Hash{ $Row[0] } = $Row[1];
        }
    }
    if ( $Param{Result} &amp;&amp; $Param{Result} eq 'HASH' ) {
        return %Hash;
    }
    return @Array;
}

=item ArticleLastCustomerArticle()

get last customer article

    my %Article = $TicketObject-&gt;ArticleLastCustomerArticle(
        TicketID      =&gt; 123,
        Extended      =&gt; 1,      # 0 or 1, see ArticleGet(),
        DynamicFields =&gt; 1,      # 0 or 1, see ArticleGet(),
    );

=cut

sub ArticleLastCustomerArticle {
    my ( $Self, %Param ) = @_;

    # check needed stuff
    if ( !$Param{TicketID} ) {
        $Self-&gt;{LogObject}-&gt;Log( Priority =&gt; 'error', Message =&gt; "Need TicketID!" );
        return;
    }

    # get article index
    my @Index = $Self-&gt;ArticleIndex( TicketID =&gt; $Param{TicketID}, SenderType =&gt; 'customer' );

    # get article data
    if (@Index) {
        return $Self-&gt;ArticleGet(
            ArticleID     =&gt; $Index[-1],
            Extended      =&gt; $Param{Extended},
            DynamicFields =&gt; $Param{DynamicFields},
        );
    }

    # get whole article index
    @Index = $Self-&gt;ArticleIndex( TicketID =&gt; $Param{TicketID} );
    return if !@Index;

    # second try, return latest non internal article
    for my $ArticleID ( reverse @Index ) {
        my %Article = $Self-&gt;ArticleGet(
            ArticleID     =&gt; $ArticleID,
            Extended      =&gt; $Param{Extended},
            DynamicFields =&gt; $Param{DynamicFields},
        );
        if ( $Article{StateType} eq 'merged' || $Article{ArticleType} !~ /int/ ) {
            return %Article;
        }
    }

    # third try, if we got no internal article, return the latest one
    return $Self-&gt;ArticleGet(
        ArticleID     =&gt; $Index[-1],
        Extended      =&gt; $Param{Extended},
        DynamicFields =&gt; $Param{DynamicFields},
    );
}

=item ArticleFirstArticle()

get first article

    my %Article = $TicketObject-&gt;ArticleFirstArticle(
        TicketID      =&gt; 123,
        DynamicFields =&gt; 1,     # 0 or 1, see ArticleGet()
    );

=cut

sub ArticleFirstArticle {
    my ( $Self, %Param ) = @_;

    # check needed stuff
    if ( !$Param{TicketID} ) {
        $Self-&gt;{LogObject}-&gt;Log( Priority =&gt; 'error', Message =&gt; "Need TicketID!" );
        return;
    }

    # get article index
    my @Index = $Self-&gt;ArticleIndex( TicketID =&gt; $Param{TicketID} );

    # get article data
    return if !@Index;

    return $Self-&gt;ArticleGet(
        ArticleID     =&gt; $Index[0],
        Extended      =&gt; $Param{Extended},
        DynamicFields =&gt; $Param{DynamicFields},
    );
}

=item ArticleIndex()

returns an array with article IDs

    my @ArticleIDs = $TicketObject-&gt;ArticleIndex(
        TicketID =&gt; 123,
    );

    my @ArticleIDs = $TicketObject-&gt;ArticleIndex(
        SenderType =&gt; 'customer',                   # optional, to limit to a certain sender type
        TicketID   =&gt; 123,
    );

=cut

sub ArticleIndex {
    my ( $Self, %Param ) = @_;

    # check needed stuff
    if ( !$Param{TicketID} ) {
        $Self-&gt;{LogObject}-&gt;Log( Priority =&gt; 'error', Message =&gt; 'Need TicketID!' );
        return;
    }

    # Only cache known sender types, because the cache keys of
    #   unknown ones cannot be invalidated in _TicketCacheClear().
    my %CacheableSenderTypes = (
        'agent'    =&gt; 1,
        'customer' =&gt; 1,
        'system'   =&gt; 1,
        'ALL'      =&gt; 1,
    );

    my $UseCache = $CacheableSenderTypes{ $Param{SenderType} || 'ALL' };

    my $CacheKey = 'ArticleIndex::' . $Param{TicketID} . '::' . ( $Param{SenderType} || 'ALL' );

    if ($UseCache) {
        my $Cached = $Self-&gt;{CacheInternalObject}-&gt;Get(
            Key =&gt; $CacheKey,
        );

        if ( ref $Cached eq 'ARRAY' ) {
            return @{$Cached};
        }

    }

    # db query
    if ( $Param{SenderType} ) {
        return if !$Self-&gt;{DBObject}-&gt;Prepare(
            SQL =&gt; '
                SELECT art.id FROM article art, article_sender_type ast
                WHERE art.ticket_id = ?
                    AND art.article_sender_type_id = ast.id
                    AND ast.name = ?
                ORDER BY art.id',
            Bind =&gt; [ \$Param{TicketID}, \$Param{SenderType} ],
        );
    }
    else {
        return if !$Self-&gt;{DBObject}-&gt;Prepare(
            SQL =&gt; '
                SELECT id
                FROM article
                WHERE ticket_id = ?
                ORDER BY id',
            Bind =&gt; [ \$Param{TicketID} ],
        );
    }

    my @Index;
    while ( my @Row = $Self-&gt;{DBObject}-&gt;FetchrowArray() ) {
        push @Index, $Row[0];
    }

    if ($UseCache) {
        $Self-&gt;{CacheInternalObject}-&gt;Set(
            Key   =&gt; $CacheKey,
            Value =&gt; \@Index,
        );
    }

    return @Index;
}

=item ArticleContentIndex()

returns an array with hash ref (hash contains result of ArticleGet())

    my @ArticleBox = $TicketObject-&gt;ArticleContentIndex(
        TicketID      =&gt; 123,
        DynamicFields =&gt; 1,         # 0 or 1, default 1. To include or not the dynamic field values on the return structure.
        UserID        =&gt; 1,
    );

or with "StripPlainBodyAsAttachment =&gt; 1" feature to not include first
attachment / body and html body as attachment

    my @ArticleBox = $TicketObject-&gt;ArticleContentIndex(
        TicketID                   =&gt; 123,
        UserID                     =&gt; 1,
        StripPlainBodyAsAttachment =&gt; 1,
    );

or with "StripPlainBodyAsAttachment =&gt; 2" feature to not include first
attachment / body as attachment (html body will be shown as attachment)

    my @ArticleBox = $TicketObject-&gt;ArticleContentIndex(
        TicketID                   =&gt; 123,
        UserID                     =&gt; 1,
        StripPlainBodyAsAttachment =&gt; 2,
    );

returns an array with hash ref (hash contains result of ArticleGet())
only with given article types

    my @ArticleBox = $TicketObject-&gt;ArticleContentIndex(
        TicketID    =&gt; 123,
        UserID      =&gt; 1,
        ArticleType =&gt; [ $ArticleType1, $ArticleType2 ],
    );

example of how to access the hash ref

    for my $Article (@ArticleBox) {
        print "From: $Article-&gt;{From}\n";
    }

Note: If an attachment with html body content is available, the attachment id
is returned as 'AttachmentIDOfHTMLBody' in hash ref.

=cut

sub ArticleContentIndex {
    my ( $Self, %Param ) = @_;

    # check needed stuff
    for (qw(TicketID UserID)) {
        if ( !$Param{$_} ) {
            $Self-&gt;{LogObject}-&gt;Log( Priority =&gt; 'error', Message =&gt; "Need $_!" );
            return;
        }
    }

    my @ArticleBox = $Self-&gt;ArticleGet(
        TicketID      =&gt; $Param{TicketID},
        ArticleType   =&gt; $Param{ArticleType},
        UserID        =&gt; $Param{UserID},
        DynamicFields =&gt; $Param{DynamicFields},
    );

    # article attachments of each article
    for my $Article (@ArticleBox) {

        # get attachment index (without attachments)
        my %AtmIndex = $Self-&gt;ArticleAttachmentIndex(
            ContentPath                =&gt; $Article-&gt;{ContentPath},
            ArticleID                  =&gt; $Article-&gt;{ArticleID},
            StripPlainBodyAsAttachment =&gt; $Param{StripPlainBodyAsAttachment},
            Article                    =&gt; $Article,
            UserID                     =&gt; $Param{UserID},
        );
        $Article-&gt;{Atms} = \%AtmIndex;
    }
    return @ArticleBox;
}

=item ArticleGet()

returns article data

    my %Article = $TicketObject-&gt;ArticleGet(
        ArticleID     =&gt; 123,
        DynamicFields =&gt; 1,      # Optional. To include the dynamic field values for this article on the return structure.
        UserID        =&gt; 123,
    );

Article:
    ArticleID
    From
    To
    Cc
    Subject
    Body
    ReplyTo
    MessageID
    InReplyTo
    References
    SenderType
    SenderTypeID
    ArticleType
    ArticleTypeID
    ContentType
    Charset
    MimeType
    IncomingTime

    # If DynamicFields =&gt; 1 was passed, you'll get an entry like this for each dynamic field:
    DynamicField_X     =&gt; 'value_x',

Ticket:
    - see TicketGet() for ticket attributes -

returns articles in array / hash by given ticket id

    my @ArticleIndex = $TicketObject-&gt;ArticleGet(
        TicketID =&gt; 123,
        UserID   =&gt; 123,
    );

returns articles in array / hash by given ticket id but
only requested article types

    my @ArticleIndex = $TicketObject-&gt;ArticleGet(
        TicketID    =&gt; 123,
        ArticleType =&gt; [ $ArticleType1, $ArticleType2 ],
        UserID      =&gt; 123,
    );

returns articles in array / hash by given ticket id but
only requested article sender types (could be useful when
trying to exclude autoreplies sent by system sender from
certain views)

    my @ArticleIndex = $TicketObject-&gt;ArticleGet(
        TicketID            =&gt; 123,
        ArticleSenderType   =&gt; [ $ArticleSenderType1, $ArticleSenderType2 ],
        UserID              =&gt; 123,
    );

to get extended ticket attributes, use param Extended - see TicketGet() for extended attributes -

    my @ArticleIndex = $TicketObject-&gt;ArticleGet(
        TicketID =&gt; 123,
        UserID   =&gt; 123,
        Extended =&gt; 1,
    );

to get only a dedicated count you can use Limit and Order attributes

    my @ArticleIndex = $TicketObject-&gt;ArticleGet(
        TicketID =&gt; 123,
        UserID   =&gt; 123,
        Order    =&gt; 'DESC', # DESC,ASC - default is ASC
        Limit    =&gt; 5,
    );

=cut

sub ArticleGet {
    my ( $Self, %Param ) = @_;

    # check needed stuff
    if ( !$Param{ArticleID} &amp;&amp; !$Param{TicketID} ) {
        $Self-&gt;{LogObject}-&gt;Log( Priority =&gt; 'error', Message =&gt; 'Need ArticleID or TicketID!' );
        return;
    }

    my $FetchDynamicFields = $Param{DynamicFields} ? 1 : 0;

    # article type lookup
    my $ArticleTypeSQL = '';
    if ( $Param{ArticleType} &amp;&amp; ref $Param{ArticleType} eq 'ARRAY' ) {
        for ( @{ $Param{ArticleType} } ) {
            if ( $Self-&gt;ArticleTypeLookup( ArticleType =&gt; $_ ) ) {
                if ($ArticleTypeSQL) {
                    $ArticleTypeSQL .= ',';
                }
                $ArticleTypeSQL .= $Self-&gt;{DBObject}-&gt;Quote(
                    $Self-&gt;ArticleTypeLookup( ArticleType =&gt; $_ ),
                    'Integer',
                );
            }
        }
        if ($ArticleTypeSQL) {
            $ArticleTypeSQL = " AND sa.article_type_id IN ($ArticleTypeSQL)";
        }
    }

    # sender type lookup
    my $SenderTypeSQL = '';
    if ( $Param{ArticleSenderType} &amp;&amp; ref $Param{ArticleSenderType} eq 'ARRAY' ) {
        for ( @{ $Param{ArticleSenderType} } ) {
            if ( $Self-&gt;ArticleSenderTypeLookup( SenderType =&gt; $_ ) ) {
                if ($SenderTypeSQL) {
                    $SenderTypeSQL .= ',';
                }
                $SenderTypeSQL .= $Self-&gt;{DBObject}-&gt;Quote(
                    $Self-&gt;ArticleSenderTypeLookup( SenderType =&gt; $_ ),
                    'Integer',
                );
            }
        }
        if ($SenderTypeSQL) {
            $SenderTypeSQL = " AND sa.article_sender_type_id IN ($SenderTypeSQL)";
        }
    }

    # sql query
    my @Content;
    my @Bind;
    my $SQL = '
        SELECT sa.ticket_id, sa.a_from, sa.a_to, sa.a_cc, sa.a_subject,
            sa.a_reply_to, sa.a_message_id, sa.a_in_reply_to, sa.a_references, sa.a_body,
            st.create_time_unix, st.ticket_state_id, st.queue_id, sa.create_time,
            sa.a_content_type, sa.create_by, st.tn, article_sender_type_id, st.customer_id,
            st.until_time, st.ticket_priority_id, st.customer_user_id, st.user_id,
            st.responsible_user_id, sa.article_type_id,
            sa.incoming_time, sa.id,
            st.ticket_lock_id, st.title, st.escalation_update_time,
            st.type_id, st.service_id, st.sla_id, st.escalation_response_time,
            st.escalation_solution_time, st.escalation_time, st.change_time
        FROM article sa, ticket st
        WHERE ';

    if ( $Param{ArticleID} ) {
        $SQL .= 'sa.id = ?';
        push @Bind, \$Param{ArticleID};
    }
    else {
        $SQL .= 'sa.ticket_id = ?';
        push @Bind, \$Param{TicketID};
    }
    $SQL .= ' AND sa.ticket_id = st.id ';

    # add article types
    if ($ArticleTypeSQL) {
        $SQL .= $ArticleTypeSQL;
    }

    # add sender types
    if ($SenderTypeSQL) {
        $SQL .= $SenderTypeSQL;
    }

    # set order
    if ( $Param{Order} &amp;&amp; $Param{Order} eq 'DESC' ) {
        $SQL .= ' ORDER BY sa.create_time DESC, sa.id DESC';
    }
    else {
        $SQL .= ' ORDER BY sa.create_time, sa.id ASC';
    }

    return if !$Self-&gt;{DBObject}-&gt;Prepare( SQL =&gt; $SQL, Bind =&gt; \@Bind, Limit =&gt; $Param{Limit} );
    my %Ticket;
    while ( my @Row = $Self-&gt;{DBObject}-&gt;FetchrowArray() ) {
        my %Data;
        $Data{TicketID}   = $Row[0];
        $Ticket{TicketID} = $Data{TicketID};
        $Data{From}       = $Row[1];
        $Data{To}         = $Row[2];
        $Data{Cc}         = $Row[3];
        $Data{Subject}    = $Row[4];

        $Data{ReplyTo}    = $Row[5];
        $Data{MessageID}  = $Row[6];
        $Data{InReplyTo}  = $Row[7];
        $Data{References} = $Row[8];
        $Data{Body}       = $Row[9];

        $Ticket{CreateTimeUnix} = $Row[10];
        $Data{StateID}          = $Row[11];
        $Ticket{StateID}        = $Row[11];
        $Data{QueueID}          = $Row[12];
        $Ticket{QueueID}        = $Row[12];
        $Ticket{AgeTimeUnix}    = $Self-&gt;{TimeObject}-&gt;SystemTime()
            - $Self-&gt;{TimeObject}-&gt;TimeStamp2SystemTime( String =&gt; $Row[13] );
        $Ticket{Created}
            = $Self-&gt;{TimeObject}-&gt;SystemTime2TimeStamp( SystemTime =&gt; $Ticket{CreateTimeUnix} );
        $Data{ContentType} = $Row[14];

        $Data{CreatedBy}           = $Row[15];
        $Data{TicketNumber}        = $Row[16];
        $Data{SenderTypeID}        = $Row[17];
        $Data{CustomerID}          = $Row[18];
        $Ticket{CustomerID}        = $Row[18];
        $Data{RealTillTimeNotUsed} = $Row[19];

        $Data{PriorityID}       = $Row[20];
        $Ticket{PriorityID}     = $Row[20];
        $Data{CustomerUserID}   = $Row[21];
        $Ticket{CustomerUserID} = $Row[21];
        $Data{OwnerID}          = $Row[22];
        $Ticket{OwnerID}        = $Row[22];
        $Data{ResponsibleID}    = $Row[23] || 1;
        $Ticket{ResponsibleID}  = $Row[23] || 1;
        $Data{ArticleTypeID}    = $Row[24];

        $Data{IncomingTime} = $Row[25];
        $Data{Created}      = $Self-&gt;{TimeObject}-&gt;SystemTime2TimeStamp(
            SystemTime =&gt; $Row[25],
        );
        $Data{ArticleID}              = $Row[26];
        $Ticket{LockID}               = $Row[27];
        $Data{Title}                  = $Row[28];
        $Ticket{Title}                = $Data{Title};
        $Data{EscalationUpdateTime}   = $Row[29];
        $Ticket{EscalationUpdateTime} = $Data{EscalationUpdateTime};

        $Data{TypeID}                   = $Row[30];
        $Ticket{TypeID}                 = $Row[30];
        $Data{ServiceID}                = $Row[31];
        $Ticket{ServiceID}              = $Row[31];
        $Data{SLAID}                    = $Row[32];
        $Ticket{SLAID}                  = $Row[32];
        $Data{EscalationResponseTime}   = $Row[33];
        $Ticket{EscalationResponseTime} = $Data{EscalationResponseTime};
        $Data{EscalationSolutionTime}   = $Row[34];
        $Ticket{EscalationSolutionTime} = $Data{EscalationSolutionTime};
        $Data{EscalationTime}           = $Row[35];
        $Ticket{EscalationTime}         = $Data{EscalationTime};
        $Ticket{Changed}                = $Row[36];

        if ( $Data{ContentType} &amp;&amp; $Data{ContentType} =~ /charset=/i ) {
            $Data{Charset} = $Data{ContentType};
            $Data{Charset} =~ s/.+?charset=("|'|)(\w+)/$2/gi;
            $Data{Charset} =~ s/"|'//g;
            $Data{Charset} =~ s/(.+?);.*/$1/g;

        }
        else {
            $Data{Charset} = '';
        }

        # compat.
        $Data{ContentCharset} = $Data{Charset};

        if ( $Data{ContentType} &amp;&amp; $Data{ContentType} =~ /^(\w+\/\w+)/i ) {
            $Data{MimeType} = $1;
            $Data{MimeType} =~ s/"|'//g;
        }
        else {
            $Data{MimeType} = '';
        }

        # fill up dynamic varaibles
        $Data{Age} = $Self-&gt;{TimeObject}-&gt;SystemTime() - $Ticket{CreateTimeUnix};

        # strip not wanted stuff
        for my $Key (qw(From To Cc Subject)) {
            next if !$Data{$Key};
            $Data{$Key} =~ s/\n|\r//g;
        }

        push @Content, { %Ticket, %Data };
    }

    # checl if need to return dynamic fields
    if ($FetchDynamicFields) {

        my $DynamicFieldArticleList = $Self-&gt;{DynamicFieldObject}-&gt;DynamicFieldListGet(
            ObjectType =&gt; 'Article'
        );

        my $DynamicFieldTicketList = $Self-&gt;{DynamicFieldObject}-&gt;DynamicFieldListGet(
            ObjectType =&gt; 'Ticket'
        );

        for my $Article (@Content) {
            DYNAMICFIELD:
            for my $DynamicFieldConfig ( @{$DynamicFieldArticleList} ) {

                # validate each dynamic field
                next DYNAMICFIELD if !$DynamicFieldConfig;
                next DYNAMICFIELD if !IsHashRefWithData($DynamicFieldConfig);
                next DYNAMICFIELD if !$DynamicFieldConfig-&gt;{Name};
                next DYNAMICFIELD if !IsHashRefWithData( $DynamicFieldConfig-&gt;{Config} );

                # get the current value for each dynamic field
                my $Value = $Self-&gt;{DynamicFieldBackendObject}-&gt;ValueGet(
                    DynamicFieldConfig =&gt; $DynamicFieldConfig,
                    ObjectID           =&gt; $Article-&gt;{ArticleID},
                );

                # set the dynamic field name and value into the ticket hash
                $Article-&gt;{ 'DynamicField_' . $DynamicFieldConfig-&gt;{Name} } = $Value;
            }

            DYNAMICFIELD:
            for my $DynamicFieldConfig ( @{$DynamicFieldTicketList} ) {

                # validate each dynamic field
                next DYNAMICFIELD if !$DynamicFieldConfig;
                next DYNAMICFIELD if !IsHashRefWithData($DynamicFieldConfig);
                next DYNAMICFIELD if !$DynamicFieldConfig-&gt;{Name};
                next DYNAMICFIELD if !IsHashRefWithData( $DynamicFieldConfig-&gt;{Config} );

                # get the current value for each dynamic field
                my $Value = $Self-&gt;{DynamicFieldBackendObject}-&gt;ValueGet(
                    DynamicFieldConfig =&gt; $DynamicFieldConfig,
                    ObjectID           =&gt; $Article-&gt;{TicketID},
                );

                # set the dynamic field name and value into the ticket hash
                $Article-&gt;{ 'DynamicField_' . $DynamicFieldConfig-&gt;{Name} } = $Value;

                # check if field is TicketFreeKey[1-16], TicketFreeText[1-6] or TicketFreeTime[1-6]
                # Compatibility feature can be removed on further versions
                if (
                    $DynamicFieldConfig-&gt;{Name} =~ m{
                        \A
                        (
                            TicketFree
                            (?:
                                (?:Text|Key)
                                (?:1[0-6]|[1-9])
                                |
                                (?:Time [1-6])
                            )
                        )
                        \z
                    }smxi
                    )
                {

                    # Set field for 3.0 and 2.4 compatibility
                    $Article-&gt;{ $DynamicFieldConfig-&gt;{Name} } = $Value;
                }
            }
        }
    }

    # return if content is empty
    if ( !@Content ) {

        # Log an error only if a specific article was requested and there is no filter active.
        if ( $Param{ArticleID} &amp;&amp; !$ArticleTypeSQL &amp;&amp; !$SenderTypeSQL ) {
            $Self-&gt;{LogObject}-&gt;Log(
                Priority =&gt; 'error',
                Message  =&gt; "No such article for ArticleID ($Param{ArticleID})!",
            );
        }

        return;
    }

    # get type
    $Ticket{Type} = $Self-&gt;{TypeObject}-&gt;TypeLookup( TypeID =&gt; $Ticket{TypeID} || 1 );

    # get owner
    $Ticket{Owner} = $Self-&gt;{UserObject}-&gt;UserLookup( UserID =&gt; $Ticket{OwnerID} );

    # get responsible
    $Ticket{Responsible} = $Self-&gt;{UserObject}-&gt;UserLookup( UserID =&gt; $Ticket{ResponsibleID} );

    # get priority
    $Ticket{Priority} = $Self-&gt;{PriorityObject}-&gt;PriorityLookup(
        PriorityID =&gt; $Ticket{PriorityID},
    );

    # get lock
    $Ticket{Lock} = $Self-&gt;{LockObject}-&gt;LockLookup( LockID =&gt; $Ticket{LockID} );

    # get service
    if ( $Ticket{ServiceID} ) {
        $Ticket{Service} = $Self-&gt;{ServiceObject}-&gt;ServiceLookup( ServiceID =&gt; $Ticket{ServiceID} );
    }

    # get sla
    if ( $Ticket{SLAID} ) {
        $Ticket{SLA} = $Self-&gt;{SLAObject}-&gt;SLALookup( SLAID =&gt; $Ticket{SLAID} );
    }

    # get queue name and other stuff
    my %Queue = $Self-&gt;{QueueObject}-&gt;QueueGet( ID =&gt; $Ticket{QueueID} );

    # get state info
    my %StateData = $Self-&gt;{StateObject}-&gt;StateGet( ID =&gt; $Ticket{StateID} );
    $Ticket{StateType} = $StateData{TypeName};
    $Ticket{State}     = $StateData{Name};

    # get escalation attributes
    my %Escalation = $Self-&gt;TicketEscalationDateCalculation(
        Ticket =&gt; \%Ticket,
        UserID =&gt; $Param{UserID} || 1,
    );
    for my $Part (@Content) {
        for ( sort keys %Escalation ) {
            $Part-&gt;{$_} = $Escalation{$_};
        }
    }

    # do extended lookups
    if ( $Param{Extended} ) {
        my %TicketExtended = $Self-&gt;_TicketGetExtended(
            TicketID =&gt; $Ticket{TicketID},
            Ticket   =&gt; \%Ticket,
        );
        for my $Key ( sort keys %TicketExtended ) {
            $Ticket{$Key} = $TicketExtended{$Key};
        }
        for my $Part (@Content) {
            for ( sort keys %TicketExtended ) {
                $Part-&gt;{$_} = $TicketExtended{$_};
            }
        }
    }

    # article stuff
    for my $Part (@Content) {

        # get type
        $Part-&gt;{Type} = $Ticket{Type};

        # get owner
        $Part-&gt;{Owner} = $Ticket{Owner};

        # get responsible
        $Part-&gt;{Responsible} = $Ticket{Responsible};

        # get sender type
        $Part-&gt;{SenderType} = $Self-&gt;ArticleSenderTypeLookup(
            SenderTypeID =&gt; $Part-&gt;{SenderTypeID},
        );

        # get article type
        $Part-&gt;{ArticleType} = $Self-&gt;ArticleTypeLookup(
            ArticleTypeID =&gt; $Part-&gt;{ArticleTypeID},
        );

        # get priority name
        $Part-&gt;{Priority} = $Ticket{Priority};
        $Part-&gt;{LockID}   = $Ticket{LockID};
        $Part-&gt;{Lock}     = $Ticket{Lock};
        $Part-&gt;{Queue}    = $Queue{Name};
        $Part-&gt;{Service}  = $Ticket{Service} || '';
        $Part-&gt;{SLA}      = $Ticket{SLA} || '';
        if ( !$Part-&gt;{RealTillTimeNotUsed} || $StateData{TypeName} !~ /^pending/i ) {
            $Part-&gt;{UntilTime} = 0;
        }
        else {
            $Part-&gt;{UntilTime} = $Part-&gt;{RealTillTimeNotUsed} - $Self-&gt;{TimeObject}-&gt;SystemTime();
        }
        $Part-&gt;{StateType} = $StateData{TypeName};
        $Part-&gt;{State}     = $StateData{Name};

        # add real name lines
        my $EmailParser = Kernel::System::EmailParser-&gt;new( %{$Self}, Mode =&gt; 'Standalone' );
        for my $Key (qw( From To Cc)) {
            next if !$Part-&gt;{$Key};

            # check if it's a queue
            if ( $Part-&gt;{$Key} !~ /@/ ) {
                $Part-&gt;{ $Key . 'Realname' } = $Part-&gt;{$Key};
                next;
            }

            # strip out real names
            my $Realname = '';
            for my $EmailSplit ( $EmailParser-&gt;SplitAddressLine( Line =&gt; $Part-&gt;{$Key} ) ) {
                my $Name = $EmailParser-&gt;GetRealname( Email =&gt; $EmailSplit );
                if ( !$Name ) {
                    $Name = $EmailParser-&gt;GetEmailAddress( Email =&gt; $EmailSplit );
                }
                next if !$Name;
                if ($Realname) {
                    $Realname .= ', ';
                }
                $Realname .= $Name;
            }

            $Part-&gt;{ $Key . 'Realname' } = $Realname;
        }
    }

    if ( $Param{ArticleID} ) {
        return %{ $Content[0] };
    }
    return @Content;
}

=begin Internal:

=cut

sub _ArticleGetId {
    my ( $Self, %Param ) = @_;

    # check needed stuff
    for (qw(TicketID MessageID From Subject IncomingTime)) {
        if ( !defined $Param{$_} ) {
            $Self-&gt;{LogObject}-&gt;Log( Priority =&gt; 'error', Message =&gt; "Need $_!" );
            return;
        }
    }

    # sql query
    my @Bind = ( \$Param{TicketID} );
    my $SQL  = 'SELECT id FROM article WHERE ticket_id = ? AND ';
    if ( $Param{MessageID} ) {
        $SQL .= 'a_message_id = ? AND ';
        push @Bind, \$Param{MessageID};
    }
    if ( $Param{From} ) {
        $SQL .= 'a_from = ? AND ';
        push @Bind, \$Param{From};
    }
    if ( $Param{Subject} ) {
        $SQL .= 'a_subject = ? AND ';
        push @Bind, \$Param{Subject};
    }
    $SQL .= ' incoming_time = ? ORDER BY id DESC';
    push @Bind, \$Param{IncomingTime};

    # start query
    return if !$Self-&gt;{DBObject}-&gt;Prepare(
        SQL   =&gt; $SQL,
        Bind  =&gt; \@Bind,
        Limit =&gt; 1,
    );
    my $ID;
    while ( my @Row = $Self-&gt;{DBObject}-&gt;FetchrowArray() ) {
        $ID = $Row[0];
    }
    return $ID;
}

=end Internal:

=item ArticleUpdate()

update an article

Note: Keys "Body", "Subject", "From", "To", "Cc", "ArticleType" and "SenderType" are implemented.

    my $Success = $TicketObject-&gt;ArticleUpdate(
        ArticleID =&gt; 123,
        Key       =&gt; 'Body',
        Value     =&gt; 'New Body',
        UserID    =&gt; 123,
        TicketID  =&gt; 123,
    );

    my $Success = $TicketObject-&gt;ArticleUpdate(
        ArticleID =&gt; 123,
        Key       =&gt; 'ArticleType',
        Value     =&gt; 'email-internal',
        UserID    =&gt; 123,
        TicketID  =&gt; 123,
    );

Events:
    ArticleUpdate

=cut

sub ArticleUpdate {
    my ( $Self, %Param ) = @_;

    # check needed stuff
    for (qw(ArticleID UserID Key TicketID)) {
        if ( !$Param{$_} ) {
            $Self-&gt;{LogObject}-&gt;Log( Priority =&gt; 'error', Message =&gt; "Need $_!" );
            return;
        }
    }

    # check needed stuff
    if ( !defined $Param{Value} ) {
        $Self-&gt;{LogObject}-&gt;Log( Priority =&gt; 'error', Message =&gt; 'Need Value!' );
        return;
    }

    # lookup for ArticleType
    if ( $Param{Key} eq 'ArticleType' ) {
        $Param{Key}   = 'ArticleTypeID';
        $Param{Value} = $Self-&gt;ArticleTypeLookup(
            ArticleType =&gt; $Param{Value},
        );
    }

    # lookup for SenderType
    if ( $Param{Key} eq 'SenderType' ) {
        $Param{Key}   = 'SenderTypeID';
        $Param{Value} = $Self-&gt;ArticleSenderTypeLookup(
            SenderType =&gt; $Param{Value},
        );
    }

    # map
    my %Map = (
        Body          =&gt; 'a_body',
        Subject       =&gt; 'a_subject',
        From          =&gt; 'a_from',
        To            =&gt; 'a_to',
        Cc            =&gt; 'a_cc',
        ArticleTypeID =&gt; 'article_type_id',
        SenderTypeID  =&gt; 'article_sender_type_id',
    );

    # db update
    return if !$Self-&gt;{DBObject}-&gt;Do(
        SQL =&gt; "UPDATE article SET $Map{$Param{Key}} = ?, "
            . "change_time = current_timestamp, change_by = ? WHERE id = ?",
        Bind =&gt; [ \$Param{Value}, \$Param{UserID}, \$Param{ArticleID} ],
    );

    $Self-&gt;_TicketCacheClear( TicketID =&gt; $Param{TicketID} );

    # event
    $Self-&gt;EventHandler(
        Event =&gt; 'ArticleUpdate',
        Data  =&gt; {
            TicketID  =&gt; $Param{TicketID},
            ArticleID =&gt; $Param{ArticleID},
        },
        UserID =&gt; $Param{UserID},
    );
    return 1;
}

=item ArticleSend()

send article via email and create article with attachments

    my $ArticleID = $TicketObject-&gt;ArticleSend(
        TicketID    =&gt; 123,
        ArticleType =&gt; 'note-internal',                                        # email-external|email-internal|phone|fax|...
        SenderType  =&gt; 'agent',                                                # agent|system|customer
        From        =&gt; 'Some Agent &lt;email@example.com&gt;',                       # not required but useful
        To          =&gt; 'Some Customer A &lt;customer-a@example.com&gt;',             # not required but useful
        Cc          =&gt; 'Some Customer B &lt;customer-b@example.com&gt;',             # not required but useful
        ReplyTo     =&gt; 'Some Customer B &lt;customer-b@example.com&gt;',             # not required, is possible to use 'Reply-To' instead
        Subject     =&gt; 'some short description',                               # required
        Body        =&gt; 'the message text',                                     # required
        InReplyTo   =&gt; '&lt;asdasdasd.12@example.com&gt;',                           # not required but useful
        References  =&gt; '&lt;asdasdasd.1@example.com&gt; &lt;asdasdasd.12@example.com&gt;', # not required but useful
        Charset     =&gt; 'iso-8859-15'
        MimeType    =&gt; 'text/plain',
        Loop        =&gt; 0, # 1|0 used for bulk emails
        Attachment =&gt; [
            {
                Content     =&gt; $Content,
                ContentType =&gt; $ContentType,
                Filename    =&gt; 'lala.txt',
            },
            {
                Content     =&gt; $Content,
                ContentType =&gt; $ContentType,
                Filename    =&gt; 'lala1.txt',
            },
        ],
        Sign =&gt; {
            Type    =&gt; 'PGP',
            SubType =&gt; 'Inline|Detached',
            Key     =&gt; '81877F5E',
            Type    =&gt; 'SMIME',
            Key     =&gt; '3b630c80',
        },
        Crypt =&gt; {
            Type    =&gt; 'PGP',
            SubType =&gt; 'Inline|Detached',
            Key     =&gt; '81877F5E',
            Type    =&gt; 'SMIME',
            Key     =&gt; '3b630c80',
        },
        HistoryType    =&gt; 'OwnerUpdate',  # Move|AddNote|PriorityUpdate|WebRequestCustomer|...
        HistoryComment =&gt; 'Some free text!',
        NoAgentNotify  =&gt; 0,            # if you don't want to send agent notifications
        UserID         =&gt; 123,
    );

Events:
    ArticleSend

=cut

sub ArticleSend {
    my ( $Self, %Param ) = @_;

    my $ToOrig      = $Param{To}          || '';
    my $Loop        = $Param{Loop}        || 0;
    my $HistoryType = $Param{HistoryType} || 'SendAnswer';

    # check needed stuff
    for (qw(TicketID UserID From Body Charset MimeType)) {
        if ( !$Param{$_} ) {
            $Self-&gt;{LogObject}-&gt;Log( Priority =&gt; 'error', Message =&gt; "Need $_!" );
            return;
        }
    }

    if ( !$Param{ArticleType} &amp;&amp; !$Param{ArticleTypeID} ) {
        $Self-&gt;{LogObject}-&gt;Log(
            Priority =&gt; 'error',
            Message  =&gt; 'Need ArticleType or ArticleTypeID!',
        );
        return;
    }
    if ( !$Param{SenderType} &amp;&amp; !$Param{SenderTypeID} ) {
        $Self-&gt;{LogObject}-&gt;Log(
            Priority =&gt; 'error',
            Message  =&gt; 'Need SenderType or SenderTypeID!',
        );
        return;
    }

    # map ReplyTo into Reply-To if present
    if ( $Param{ReplyTo} ) {
        $Param{'Reply-To'} = $Param{ReplyTo};
    }

    # clean up
    $Param{Body} =~ s/(\r\n|\n\r)/\n/g;
    $Param{Body} =~ s/\r/\n/g;

    # check for base64 images in body and process them
    my $HTMLUtilsObject = Kernel::System::HTMLUtils-&gt;new(
        LogObject    =&gt; $Self-&gt;{LogObject},
        ConfigObject =&gt; $Self-&gt;{ConfigObject},
        MainObject   =&gt; $Self-&gt;{MainObject},
        EncodeObject =&gt; $Self-&gt;{EncodeObject},
    );
    $HTMLUtilsObject-&gt;EmbeddedImagesExtract(
        DocumentRef =&gt; \$Param{Body},
        AttachmentsRef =&gt; $Param{Attachment} || [],
    );

    # create article
    my $Time      = $Self-&gt;{TimeObject}-&gt;SystemTime();
    my $Random    = rand 999999;
    my $FQDN      = $Self-&gt;{ConfigObject}-&gt;Get('FQDN');
    my $MessageID = "&lt;$Time.$Random.$Param{TicketID}.$Param{UserID}\@$FQDN&gt;";
    my $ArticleID = $Self-&gt;ArticleCreate(
        %Param,
        MessageID =&gt; $MessageID,
    );
    return if !$ArticleID;

    # send mail
    my ( $HeadRef, $BodyRef ) = $Self-&gt;{SendmailObject}-&gt;Send(
        'Message-ID' =&gt; $MessageID,
        %Param,
    );

    # return if no mail was able to send
    if ( !$HeadRef || !$BodyRef ) {
        $Self-&gt;{LogObject}-&gt;Log(
            Message  =&gt; "Impossible to send message to: $Param{'To'} .",
            Priority =&gt; 'error',
        );
        return;
    }

    # write article to fs
    my $Plain = $Self-&gt;ArticleWritePlain(
        ArticleID =&gt; $ArticleID,
        Email     =&gt; ${$HeadRef} . "\n" . ${$BodyRef},
        UserID    =&gt; $Param{UserID}
    );
    return if !$Plain;

    # log
    $Self-&gt;{LogObject}-&gt;Log(
        Priority =&gt; 'info',
        Message  =&gt; "Sent email to '$ToOrig' from '$Param{From}'. "
            . "HistoryType =&gt; $HistoryType, Subject =&gt; $Param{Subject};",
    );

    # event
    $Self-&gt;EventHandler(
        Event =&gt; 'ArticleSend',
        Data  =&gt; {
            TicketID  =&gt; $Param{TicketID},
            ArticleID =&gt; $ArticleID,
        },
        UserID =&gt; $Param{UserID},
    );
    return $ArticleID;
}

=item ArticleBounce()

bounce an article

    my $Success = $TicketObject-&gt;ArticleBounce(
        From      =&gt; 'some@example.com',
        To        =&gt; 'webmaster@example.com',
        TicketID  =&gt; 123,
        ArticleID =&gt; 123,
        UserID    =&gt; 123,
    );

Events:
    ArticleBounce

=cut

sub ArticleBounce {
    my ( $Self, %Param ) = @_;

    # check needed stuff
    for (qw(TicketID ArticleID From To UserID)) {
        if ( !$Param{$_} ) {
            $Self-&gt;{LogObject}-&gt;Log( Priority =&gt; 'error', Message =&gt; "Need $_!" );
            return;
        }
    }

    # create message id
    my $Time         = $Self-&gt;{TimeObject}-&gt;SystemTime();
    my $Random       = rand 999999;
    my $FQDN         = $Self-&gt;{ConfigObject}-&gt;Get('FQDN');
    my $NewMessageID = "&lt;$Time.$Random.$Param{TicketID}.0.$Param{UserID}\@$FQDN&gt;";
    my $Email        = $Self-&gt;ArticlePlain( ArticleID =&gt; $Param{ArticleID} );

    # check if plain email exists
    if ( !$Email ) {
        $Self-&gt;{LogObject}-&gt;Log(
            Priority =&gt; 'error',
            Message  =&gt; "No such plain article for ArticleID ($Param{ArticleID})!",
        );
        return;
    }

    # pipe all into sendmail
    return if !$Self-&gt;{SendmailObject}-&gt;Bounce(
        MessageID =&gt; $NewMessageID,
        From      =&gt; $Param{From},
        To        =&gt; $Param{To},
        Email     =&gt; $Email,
    );

    # write history
    my $HistoryType = $Param{HistoryType} || 'Bounce';
    $Self-&gt;HistoryAdd(
        TicketID     =&gt; $Param{TicketID},
        ArticleID    =&gt; $Param{ArticleID},
        HistoryType  =&gt; $HistoryType,
        Name         =&gt; "\%\%$Param{To}",
        CreateUserID =&gt; $Param{UserID},
    );

    # event
    $Self-&gt;EventHandler(
        Event =&gt; 'ArticleBounce',
        Data  =&gt; {
            TicketID  =&gt; $Param{TicketID},
            ArticleID =&gt; $Param{ArticleID},
        },
        UserID =&gt; $Param{UserID},
    );
    return 1;
}

=item SendAgentNotification()

send an agent notification via email

    my $Success = $TicketObject-&gt;SendAgentNotification(
        TicketID    =&gt; 123,
        CustomerMessageParams =&gt; {
            SomeParams =&gt; 'For the message!',
        },
        Type        =&gt; 'Move', # notification types, see database
        RecipientID =&gt; $UserID,
        UserID      =&gt; 123,
    );

Events:
    ArticleAgentNotification

=cut

sub SendAgentNotification {
    my ( $Self, %Param ) = @_;

    # check needed stuff
    for (qw(CustomerMessageParams TicketID Type RecipientID UserID)) {
        if ( !$Param{$_} ) {
            $Self-&gt;{LogObject}-&gt;Log( Priority =&gt; 'error', Message =&gt; "Need $_!" );
            return;
        }
    }

    # return if no notification is active
    return 1 if $Self-&gt;{SendNoNotification};

    # Check if agent receives notifications for actions done by himself.
    if (
        !$Self-&gt;{ConfigObject}-&gt;Get('AgentSelfNotifyOnAction')
        &amp;&amp; ( $Param{RecipientID} eq $Param{UserID} )
        )
    {
        return 1;
    }

    # compat Type
    if (
        $Param{Type}
        =~ /(EmailAgent|EmailCustomer|PhoneCallCustomer|WebRequestCustomer|SystemRequest)/
        )
    {
        $Param{Type} = 'NewTicket';
    }

    # get recipient
    my %User = $Self-&gt;{UserObject}-&gt;GetUserData(
        UserID =&gt; $Param{RecipientID},
        Valid  =&gt; 1,
    );

    # check recipients
    return if !$User{UserEmail};
    return if $User{UserEmail} !~ /@/;

    # get ticket object to check state
    my %Ticket = $Self-&gt;TicketGet(
        TicketID      =&gt; $Param{TicketID},
        DynamicFields =&gt; 0,
    );

    if (
        $Ticket{StateType} eq 'closed' &amp;&amp;
        $Param{Type} eq 'NewTicket'
        )
    {
        return;
    }

    my $TemplateGeneratorObject = Kernel::System::TemplateGenerator-&gt;new(
        MainObject         =&gt; $Self-&gt;{MainObject},
        DBObject           =&gt; $Self-&gt;{DBObject},
        ConfigObject       =&gt; $Self-&gt;{ConfigObject},
        EncodeObject       =&gt; $Self-&gt;{EncodeObject},
        LogObject          =&gt; $Self-&gt;{LogObject},
        CustomerUserObject =&gt; $Self-&gt;{CustomerUserObject},
        QueueObject        =&gt; $Self-&gt;{QueueObject},
        UserObject         =&gt; $Self-&gt;{UserObject},
        TicketObject       =&gt; $Self,
    );

    my %Notification = $TemplateGeneratorObject-&gt;NotificationAgent(
        Type                  =&gt; $Param{Type},
        TicketID              =&gt; $Param{TicketID},
        CustomerMessageParams =&gt; $Param{CustomerMessageParams},
        RecipientID           =&gt; $Param{RecipientID},
        UserID                =&gt; $Param{UserID},
    );

    # send notify
    $Self-&gt;{SendmailObject}-&gt;Send(
        From =&gt; $Self-&gt;{ConfigObject}-&gt;Get('NotificationSenderName') . ' &lt;'
            . $Self-&gt;{ConfigObject}-&gt;Get('NotificationSenderEmail') . '&gt;',
        To       =&gt; $User{UserEmail},
        Subject  =&gt; $Notification{Subject},
        MimeType =&gt; $Notification{ContentType} || 'text/plain',
        Charset  =&gt; $Notification{Charset},
        Body     =&gt; $Notification{Body},
        Loop     =&gt; 1,
    );

    # write history
    $Self-&gt;HistoryAdd(
        TicketID     =&gt; $Param{TicketID},
        HistoryType  =&gt; 'SendAgentNotification',
        Name         =&gt; "\%\%$Param{Type}\%\%$User{UserEmail}",
        CreateUserID =&gt; $Param{UserID},
    );

    # log event
    $Self-&gt;{LogObject}-&gt;Log(
        Priority =&gt; 'info',
        Message  =&gt; "Sent agent '$Param{Type}' notification to '$User{UserEmail}'.",
    );

    # event
    $Self-&gt;EventHandler(
        Event =&gt; 'ArticleAgentNotification',
        Data  =&gt; {
            TicketID =&gt; $Param{TicketID},
        },
        UserID =&gt; $Param{UserID},
    );

    return 1;
}

=item SendCustomerNotification()

DEPRECATED. This function is incompatible with the rich text editor, don't use it any more!

send a customer notification via email

    my $ArticleID = $TicketObject-&gt;SendCustomerNotification(
        Type =&gt; 'Move', # notification types, see database
        CustomerMessageParams =&gt; {
            SomeParams =&gt; 'For the message!',
        },
        TicketID =&gt; 123,
        UserID   =&gt; 123,
    );

Events:
    ArticleCustomerNotification

=cut

sub SendCustomerNotification {
    my ( $Self, %Param ) = @_;

    # check needed stuff
    for (qw(CustomerMessageParams TicketID UserID Type)) {
        if ( !$Param{$_} ) {
            $Self-&gt;{LogObject}-&gt;Log( Priority =&gt; 'error', Message =&gt; "Need $_!" );
            return;
        }
    }

    # return if no notification is active
    return 1 if $Self-&gt;{SendNoNotification};

    # get old article for quoteing
    my %Article = $Self-&gt;ArticleLastCustomerArticle(
        TicketID      =&gt; $Param{TicketID},
        DynamicFields =&gt; 1,
    );

    # check if notification should be send
    my %Queue = $Self-&gt;{QueueObject}-&gt;QueueGet( ID =&gt; $Article{QueueID} );
    if ( $Param{Type} =~ /^StateUpdate$/ &amp;&amp; !$Queue{StateNotify} ) {

        # need no notification
        return;
    }
    elsif ( $Param{Type} =~ /^OwnerUpdate$/ &amp;&amp; !$Queue{OwnerNotify} ) {

        # need no notification
        return;
    }
    elsif ( $Param{Type} =~ /^QueueUpdate$/ &amp;&amp; !$Queue{MoveNotify} ) {

        # need no notification
        return;
    }
    elsif ( $Param{Type} =~ /^LockUpdate$/ &amp;&amp; !$Queue{LockNotify} ) {

        # need no notification
        return;
    }

    # check if customer notifications should be send
    if (
        $Self-&gt;{ConfigObject}-&gt;Get('CustomerNotifyJustToRealCustomer')
        &amp;&amp; !$Article{CustomerUserID}
        )
    {
        $Self-&gt;{LogObject}-&gt;Log(
            Priority =&gt; 'info',
            Message  =&gt; 'Send no customer notification because no customer is set!',
        );
        return;
    }

    # check customer email
    elsif ( $Self-&gt;{ConfigObject}-&gt;Get('CustomerNotifyJustToRealCustomer') ) {
        my %CustomerUser = $Self-&gt;{CustomerUserObject}-&gt;CustomerUserDataGet(
            User =&gt; $Article{CustomerUserID},
        );
        if ( !$CustomerUser{UserEmail} ) {
            $Self-&gt;{LogObject}-&gt;Log(
                Priority =&gt; 'info',
                Message  =&gt; "Send no customer notification because of missing "
                    . "customer email (CustomerUserID=$CustomerUser{CustomerUserID})!",
            );
            return;
        }
    }

    # get language and send recipient
    my $Language = $Self-&gt;{ConfigObject}-&gt;Get('DefaultLanguage') || 'en';
    if ( $Article{CustomerUserID} ) {
        my %CustomerUser = $Self-&gt;{CustomerUserObject}-&gt;CustomerUserDataGet(
            User =&gt; $Article{CustomerUserID},
        );
        if ( $CustomerUser{UserEmail} ) {
            $Article{From} = $CustomerUser{UserEmail};
        }

        # get user language
        if ( $CustomerUser{UserLanguage} ) {
            $Language = $CustomerUser{UserLanguage};
        }
    }

    # check recipients
    if ( !$Article{From} || $Article{From} !~ /@/ ) {
        return;
    }

    # get notification data
    my $NotificationObject = Kernel::System::Notification-&gt;new(
        MainObject   =&gt; $Self-&gt;{MainObject},
        DBObject     =&gt; $Self-&gt;{DBObject},
        EncodeObject =&gt; $Self-&gt;{EncodeObject},
        ConfigObject =&gt; $Self-&gt;{ConfigObject},
        LogObject    =&gt; $Self-&gt;{LogObject},
        UserObject   =&gt; $Self-&gt;{UserObject},
    );
    my %Notification = $NotificationObject-&gt;NotificationGet(
        Name =&gt; $Language . '::Customer::' . $Param{Type},
    );

    # get notify texts
    for (qw(Subject Body)) {
        if ( !$Notification{$_} ) {
            $Notification{$_} = "No CustomerNotification $_ for $Param{Type} found!";
        }
    }

    # prepare customer realname
    if ( $Notification{Body} =~ /&lt;OTRS_CUSTOMER_REALNAME&gt;/ ) {

        # get realname
        my $From = '';
        if ( $Article{CustomerUserID} ) {
            $From = $Self-&gt;{CustomerUserObject}-&gt;CustomerName(
                UserLogin =&gt; $Article{CustomerUserID},
            );
        }
        if ( !$From ) {
            $From = $Notification{From} || '';
            $From =~ s/&lt;.*&gt;|\(.*\)|\"|;|,//g;
            $From =~ s/( $)|(  $)//g;
        }
        $Notification{Body} =~ s/&lt;OTRS_CUSTOMER_REALNAME&gt;/$From/g;
    }

    # replace config options
    $Notification{Body} =~ s{&lt;OTRS_CONFIG_(.+?)&gt;}{$Self-&gt;{ConfigObject}-&gt;Get($1)}egx;
    $Notification{Subject} =~ s{&lt;OTRS_CONFIG_(.+?)&gt;}{$Self-&gt;{ConfigObject}-&gt;Get($1)}egx;

    # cleanup
    $Notification{Subject} =~ s/&lt;OTRS_CONFIG_.+?&gt;/-/gi;
    $Notification{Body} =~ s/&lt;OTRS_CONFIG_.+?&gt;/-/gi;

    # COMPAT
    $Notification{Body} =~ s/&lt;OTRS_TICKET_ID&gt;/$Param{TicketID}/gi;
    $Notification{Body} =~ s/&lt;OTRS_TICKET_NUMBER&gt;/$Article{TicketNumber}/gi;
    if ( $Param{Queue} ) {
        $Notification{Body} =~ s/&lt;OTRS_QUEUE&gt;/$Param{Queue}/gi;
    }

    # ticket data
    my %Ticket = $Self-&gt;TicketGet(
        TicketID      =&gt; $Param{TicketID},
        DynamicFields =&gt; 1,
    );
    for ( sort keys %Ticket ) {
        if ( defined $Ticket{$_} ) {
            $Notification{Body} =~ s/&lt;OTRS_TICKET_$_&gt;/$Ticket{$_}/gi;
            $Notification{Subject} =~ s/&lt;OTRS_TICKET_$_&gt;/$Ticket{$_}/gi;
        }
    }

    # cleanup
    $Notification{Subject} =~ s/&lt;OTRS_TICKET_.+?&gt;/-/gi;
    $Notification{Body} =~ s/&lt;OTRS_TICKET_.+?&gt;/-/gi;

    # get current user data
    my %CurrentPreferences = $Self-&gt;{UserObject}-&gt;GetUserData( UserID =&gt; $Param{UserID} );
    for ( sort keys %CurrentPreferences ) {
        if ( $CurrentPreferences{$_} ) {
            $Notification{Body} =~ s/&lt;OTRS_CURRENT_$_&gt;/$CurrentPreferences{$_}/gi;
            $Notification{Subject} =~ s/&lt;OTRS_CURRENT_$_&gt;/$CurrentPreferences{$_}/gi;
        }
    }

    # cleanup
    $Notification{Subject} =~ s/&lt;OTRS_CURRENT_.+?&gt;/-/gi;
    $Notification{Body} =~ s/&lt;OTRS_CURRENT_.+?&gt;/-/gi;

    # get owner data
    my %OwnerPreferences = $Self-&gt;{UserObject}-&gt;GetUserData( UserID =&gt; $Article{OwnerID}, );
    for ( sort keys %OwnerPreferences ) {
        if ( $OwnerPreferences{$_} ) {
            $Notification{Body} =~ s/&lt;OTRS_OWNER_$_&gt;/$OwnerPreferences{$_}/gi;
            $Notification{Subject} =~ s/&lt;OTRS_OWNER_$_&gt;/$OwnerPreferences{$_}/gi;
        }
    }

    # cleanup
    $Notification{Subject} =~ s/&lt;OTRS_OWNER_.+?&gt;/-/gi;
    $Notification{Body} =~ s/&lt;OTRS_OWNER_.+?&gt;/-/gi;

    # get responsible data
    my %ResponsiblePreferences = $Self-&gt;{UserObject}-&gt;GetUserData(
        UserID =&gt; $Article{ResponsibleID},
    );
    for ( sort keys %ResponsiblePreferences ) {
        if ( $ResponsiblePreferences{$_} ) {
            $Notification{Body} =~ s/&lt;OTRS_RESPONSIBLE_$_&gt;/$ResponsiblePreferences{$_}/gi;
            $Notification{Subject} =~ s/&lt;OTRS_RESPONSIBLE_$_&gt;/$ResponsiblePreferences{$_}/gi;
        }
    }

    # cleanup
    $Notification{Subject} =~ s/&lt;OTRS_RESPONSIBLE_.+?&gt;/-/gi;
    $Notification{Body} =~ s/&lt;OTRS_RESPONSIBLE_.+?&gt;/-/gi;

    # get ref of email params
    my %GetParam = %{ $Param{CustomerMessageParams} };
    for ( sort keys %GetParam ) {
        if ( $GetParam{$_} ) {
            $Notification{Body} =~ s/&lt;OTRS_CUSTOMER_DATA_$_&gt;/$GetParam{$_}/gi;
            $Notification{Subject} =~ s/&lt;OTRS_CUSTOMER_DATA_$_&gt;/$GetParam{$_}/gi;
        }
    }

    # get customer data and replace it with &lt;OTRS_CUSTOMER_DATA_...
    if ( $Article{CustomerUserID} ) {
        my %CustomerUser = $Self-&gt;{CustomerUserObject}-&gt;CustomerUserDataGet(
            User =&gt; $Article{CustomerUserID},
        );

        # replace customer stuff with tags
        for ( sort keys %CustomerUser ) {
            if ( $CustomerUser{$_} ) {
                $Notification{Body} =~ s/&lt;OTRS_CUSTOMER_DATA_$_&gt;/$CustomerUser{$_}/gi;
                $Notification{Subject} =~ s/&lt;OTRS_CUSTOMER_DATA_$_&gt;/$CustomerUser{$_}/gi;
            }
        }
    }

    # cleanup all not needed &lt;OTRS_CUSTOMER_DATA_ tags
    $Notification{Body} =~ s/&lt;OTRS_CUSTOMER_DATA_.+?&gt;/-/gi;
    $Notification{Subject} =~ s/&lt;OTRS_CUSTOMER_DATA_.+?&gt;/-/gi;

    # format body
    if ( $Article{Body} ) {
        $Article{Body} =~ s/(^&gt;.+|.{4,72})(?:\s|\z)/$1\n/gm;
    }
    for ( sort keys %Article ) {
        if ( $Article{$_} ) {
            $Notification{Body} =~ s/&lt;OTRS_CUSTOMER_$_&gt;/$Article{$_}/gi;
            $Notification{Subject} =~ s/&lt;OTRS_CUSTOMER_$_&gt;/$Article{$_}/gi;
        }
    }

    # prepare subject (insert old subject)
    $Article{Subject} = $Self-&gt;TicketSubjectClean(
        TicketNumber =&gt; $Article{TicketNumber},
        Subject =&gt; $Article{Subject} || '',
    );
    if ( $Notification{Subject} =~ /&lt;OTRS_CUSTOMER_SUBJECT\[(.+?)\]&gt;/ ) {
        my $SubjectChar = $1;
        $Article{Subject} =~ s/^(.{$SubjectChar}).*$/$1 [...]/;
        $Notification{Subject} =~ s/&lt;OTRS_CUSTOMER_SUBJECT\[.+?\]&gt;/$Article{Subject}/g;
    }
    $Notification{Subject} = $Self-&gt;TicketSubjectBuild(
        TicketNumber =&gt; $Article{TicketNumber},
        Subject =&gt; $Notification{Subject} || '',
    );

    # prepare body (insert old email)
    if ( $Notification{Body} =~ /&lt;OTRS_CUSTOMER_EMAIL\[(.+?)\]&gt;/g ) {
        my $Line       = $1;
        my @Body       = split( /\n/, $Article{Body} );
        my $NewOldBody = '';
        for ( my $i = 0; $i &lt; $Line; $i++ ) {

            # 2002-06-14 patch of Pablo Ruiz Garcia
            # http://lists.otrs.org/pipermail/dev/2002-June/000012.html
            if ( $#Body &gt;= $i ) {
                $NewOldBody .= "&gt; $Body[$i]\n";
            }
        }
        chomp $NewOldBody;
        $Notification{Body} =~ s/&lt;OTRS_CUSTOMER_EMAIL\[.+?\]&gt;/$NewOldBody/g;
    }

    # cleanup all not needed &lt;OTRS_CUSTOMER_ tags
    $Notification{Body} =~ s/&lt;OTRS_CUSTOMER_.+?&gt;/-/gi;
    $Notification{Subject} =~ s/&lt;OTRS_CUSTOMER_.+?&gt;/-/gi;

    # send notify
    my %Address = $Self-&gt;{QueueObject}-&gt;GetSystemAddress( QueueID =&gt; $Article{QueueID} );
    $Self-&gt;ArticleSend(
        ArticleType    =&gt; 'email-notification-ext',
        SenderType     =&gt; 'system',
        TicketID       =&gt; $Param{TicketID},
        HistoryType    =&gt; 'SendCustomerNotification',
        HistoryComment =&gt; "\%\%$Article{From}",
        From           =&gt; "$Address{RealName} &lt;$Address{Email}&gt;",
        To             =&gt; $Article{From},
        Subject        =&gt; $Notification{Subject},
        Body           =&gt; $Notification{Body},
        MimeType       =&gt; 'text/plain',
        Charset        =&gt; $Notification{Charset},
        UserID         =&gt; $Param{UserID},
        Loop           =&gt; 1,
    );

    # log event
    $Self-&gt;{LogObject}-&gt;Log(
        Priority =&gt; 'info',
        Message  =&gt; "Sent customer '$Param{Type}' notification to '$Article{From}'.",
    );

    # event
    $Self-&gt;EventHandler(
        Event =&gt; 'ArticleCustomerNotification',
        Data  =&gt; {
            TicketID =&gt; $Param{TicketID},
        },
        UserID =&gt; $Param{UserID},
    );

    return 1;
}

=item SendAutoResponse()

send an auto response to a customer via email

    my $ArticleID = $TicketObject-&gt;SendAutoResponse(
        TicketID         =&gt; 123,
        AutoResponseType =&gt; 'auto reply',
        OrigHeader       =&gt; {
            From    =&gt; 'some@example.com',
            Subject =&gt; 'For the message!',
        },
        UserID          =&gt; 123,
    );

Events:
    ArticleAutoResponse

=cut

sub SendAutoResponse {
    my ( $Self, %Param ) = @_;

    # check needed stuff
    for (qw(TicketID UserID OrigHeader AutoResponseType)) {
        if ( !$Param{$_} ) {
            $Self-&gt;{LogObject}-&gt;Log( Priority =&gt; 'error', Message =&gt; "Need $_!" );
            return;
        }
    }

    # return if no notification is active
    return 1 if $Self-&gt;{SendNoNotification};

    # get orig email header
    my %OrigHeader = %{ $Param{OrigHeader} };

    # get ticket
    my %Ticket = $Self-&gt;TicketGet(
        TicketID =&gt; $Param{TicketID},
        DynamicFields =&gt; 0,    # not needed here, TemplateGenerator will fetch the ticket on its own
    );

    # get auto default responses
    my $TemplateGeneratorObject = Kernel::System::TemplateGenerator-&gt;new(
        MainObject         =&gt; $Self-&gt;{MainObject},
        DBObject           =&gt; $Self-&gt;{DBObject},
        EncodeObject       =&gt; $Self-&gt;{EncodeObject},
        ConfigObject       =&gt; $Self-&gt;{ConfigObject},
        LogObject          =&gt; $Self-&gt;{LogObject},
        CustomerUserObject =&gt; $Self-&gt;{CustomerUserObject},
        QueueObject        =&gt; $Self-&gt;{QueueObject},
        UserObject         =&gt; $Self-&gt;{UserObject},
        TicketObject       =&gt; $Self,
    );
    my %AutoResponse = $TemplateGeneratorObject-&gt;AutoResponse(
        TicketID         =&gt; $Param{TicketID},
        AutoResponseType =&gt; $Param{AutoResponseType},
        OrigHeader       =&gt; $Param{OrigHeader},
        UserID           =&gt; $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 = $Self-&gt;{StateObject}-&gt;StateGet( ID =&gt; $Ticket{StateID} );
    if (
        $Param{AutoResponseType} eq 'auto reply'
        &amp;&amp; ( $State{TypeName} eq 'closed' || $State{TypeName} eq 'removed' )
        )
    {

        # add history row
        $Self-&gt;HistoryAdd(
            TicketID    =&gt; $Param{TicketID},
            HistoryType =&gt; 'Misc',
            Name        =&gt; "Sent no auto response or agent notification because ticket is "
                . "state-type '$State{TypeName}'!",
            CreateUserID =&gt; $Param{UserID},
        );

        # return
        return;
    }

    # log that no auto response was sent!
    if ( $OrigHeader{'X-OTRS-Loop'} ) {

        # add history row
        $Self-&gt;HistoryAdd(
            TicketID    =&gt; $Param{TicketID},
            HistoryType =&gt; 'Misc',
            Name        =&gt; "Sent no auto-response because the sender doesn't want "
                . "an auto-response (e. g. loop or precedence header)",
            CreateUserID =&gt; $Param{UserID},
        );
        $Self-&gt;{LogObject}-&gt;Log(
            Priority =&gt; 'info',
            Message  =&gt; "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};
    }

    # check / loop protection!
    my $LoopProtectionObject = Kernel::System::PostMaster::LoopProtection-&gt;new(
        LogObject    =&gt; $Self-&gt;{LogObject},
        ConfigObject =&gt; $Self-&gt;{ConfigObject},
        MainObject   =&gt; $Self-&gt;{MainObject},
        DBObject     =&gt; $Self-&gt;{DBObject},
    );

    my $EmailParser = Kernel::System::EmailParser-&gt;new(
        %{$Self},
        Mode =&gt; 'Standalone',
    );

    my @AutoReplyAddresses;
    my @Addresses = $EmailParser-&gt;SplitAddressLine( Line =&gt; $OrigHeader{From} );
    ADDRESS:
    for my $Address (@Addresses) {
        my $Email = $EmailParser-&gt;GetEmailAddress( Email =&gt; $Address );
        if ( !$Email ) {

            # add it to ticket history
            $Self-&gt;HistoryAdd(
                TicketID     =&gt; $Param{TicketID},
                CreateUserID =&gt; $Param{UserID},
                HistoryType  =&gt; 'Misc',
                Name         =&gt; "Sent no auto response to '$Address' - no valid email address.",
            );

            # log
            $Self-&gt;{LogObject}-&gt;Log(
                Priority =&gt; 'notice',
                Message  =&gt; "Sent no auto response to '$Address' because of invalid address.",
            );
            next ADDRESS;

        }
        if ( !$LoopProtectionObject-&gt;Check( To =&gt; $Email ) ) {

            # add history row
            $Self-&gt;HistoryAdd(
                TicketID     =&gt; $Param{TicketID},
                HistoryType  =&gt; 'LoopProtection',
                Name         =&gt; "\%\%$Email",
                CreateUserID =&gt; $Param{UserID},
            );

            # log
            $Self-&gt;{LogObject}-&gt;Log(
                Priority =&gt; 'notice',
                Message  =&gt; "Sent no '$Param{AutoResponseType}' for Ticket ["
                    . "$Ticket{TicketNumber}] ($Email) because of loop protection."
            );
            next ADDRESS;
        }
        else {

            # increase loop count
            return if !$LoopProtectionObject-&gt;SendEmail( To =&gt; $Email );
        }

        # check if sender is e. g. MAILER-DAEMON or Postmaster
        my $NoAutoRegExp = $Self-&gt;{ConfigObject}-&gt;Get('SendNoAutoResponseRegExp');
        if ( $Email =~ /$NoAutoRegExp/i ) {

            # add it to ticket history
            $Self-&gt;HistoryAdd(
                TicketID     =&gt; $Param{TicketID},
                CreateUserID =&gt; $Param{UserID},
                HistoryType  =&gt; 'Misc',
                Name =&gt; "Sent no auto response to '$Email', SendNoAutoResponseRegExp matched.",
            );

            # log
            $Self-&gt;{LogObject}-&gt;Log(
                Priority =&gt; 'info',
                Message  =&gt; "Sent no auto response to '$Email' because config"
                    . " option SendNoAutoResponseRegExp (/$NoAutoRegExp/i) matched.",
            );
            next ADDRESS;
        }

        push @AutoReplyAddresses, $Address;
    }

    my $AutoReplyAddresses = join( ', ', @AutoReplyAddresses );
    my $ToAll = $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 = $Self-&gt;{CustomerUserObject}-&gt;CustomerUserDataGet(
            User =&gt; $Ticket{CustomerUserID},
        );
        if ( $CustomerUser{UserEmail} &amp;&amp; $OrigHeader{From} !~ /\Q$CustomerUser{UserEmail}\E/i ) {
            $ToAll .= ', ' . $CustomerUser{UserEmail};
            $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 &amp;&amp; !$Cc ) {
        $Self-&gt;{LogObject}-&gt;Log(
            Priority =&gt; 'info',
            Message  =&gt; "No auto response addresses for Ticket [$Ticket{TicketNumber}]"
                . " (TicketID=$Param{TicketID})."
        );
        return;
    }

    # send email
    my $ArticleID = $Self-&gt;ArticleSend(
        ArticleType    =&gt; 'email-external',
        SenderType     =&gt; 'system',
        TicketID       =&gt; $Param{TicketID},
        HistoryType    =&gt; $HistoryType,
        HistoryComment =&gt; "\%\%$AutoReplyAddresses",
        From           =&gt; "$AutoResponse{SenderRealname} &lt;$AutoResponse{SenderAddress}&gt;",
        To             =&gt; $AutoReplyAddresses,
        Cc             =&gt; $Cc,
        Charset        =&gt; $AutoResponse{Charset},
        MimeType       =&gt; $AutoResponse{ContentType},
        Subject        =&gt; $AutoResponse{Subject},
        Body           =&gt; $AutoResponse{Text},
        InReplyTo      =&gt; $OrigHeader{'Message-ID'},
        Loop           =&gt; 1,
        UserID         =&gt; $Param{UserID},
    );

    # log
    $Self-&gt;{LogObject}-&gt;Log(
        Priority =&gt; 'info',
        Message  =&gt; "Sent auto response ($HistoryType) for Ticket [$Ticket{TicketNumber}]"
            . " (TicketID=$Param{TicketID}, ArticleID=$ArticleID) to '$AutoReplyAddresses'."
    );

    # event
    $Self-&gt;EventHandler(
        Event =&gt; 'ArticleAutoResponse',
        Data  =&gt; {
            TicketID =&gt; $Param{TicketID},
        },
        UserID =&gt; $Param{UserID},
    );

    return 1;
}

=item ArticleFlagSet()

set article flags

    my $Success = $TicketObject-&gt;ArticleFlagSet(
        ArticleID =&gt; 123,
        Key       =&gt; 'Seen',
        Value     =&gt; 1,
        UserID    =&gt; 123,
    );

Events:
    ArticleFlagSet

=cut

sub ArticleFlagSet {
    my ( $Self, %Param ) = @_;

    # check needed stuff
    for (qw(ArticleID Key Value UserID)) {
        if ( !defined $Param{$_} ) {
            $Self-&gt;{LogObject}-&gt;Log( Priority =&gt; 'error', Message =&gt; "Need $_!" );
            return;
        }
    }

    my %Flag = $Self-&gt;ArticleFlagGet(%Param);

    # check if set is needed
    return 1 if defined $Flag{ $Param{Key} } &amp;&amp; $Flag{ $Param{Key} } eq $Param{Value};

    # set flag
    return if !$Self-&gt;{DBObject}-&gt;Do(
        SQL =&gt; '
            DELETE FROM article_flag
            WHERE article_id = ?
                AND article_key = ?
                AND create_by = ?',
        Bind =&gt; [ \$Param{ArticleID}, \$Param{Key}, \$Param{UserID} ],
    );
    return if !$Self-&gt;{DBObject}-&gt;Do(
        SQL =&gt; 'INSERT INTO article_flag
            (article_id, article_key, article_value, create_time, create_by)
            VALUES (?, ?, ?, current_timestamp, ?)',
        Bind =&gt; [ \$Param{ArticleID}, \$Param{Key}, \$Param{Value}, \$Param{UserID} ],
    );

    # event
    my %Article = $Self-&gt;ArticleGet(
        ArticleID     =&gt; $Param{ArticleID},
        UserID        =&gt; $Param{UserID},
        DynamicFields =&gt; 0,
    );
    $Self-&gt;EventHandler(
        Event =&gt; 'ArticleFlagSet',
        Data  =&gt; {
            TicketID  =&gt; $Article{TicketID},
            ArticleID =&gt; $Param{ArticleID},
            Key       =&gt; $Param{Key},
            Value     =&gt; $Param{Value},
            UserID    =&gt; $Param{UserID},
        },
        UserID =&gt; $Param{UserID},
    );

    return 1;
}

=item ArticleFlagDelete()

delete article flag

    my $Success = $TicketObject-&gt;ArticleFlagDelete(
        ArticleID =&gt; 123,
        Key       =&gt; 'seen',
        UserID    =&gt; 123,
    );

    my $Success = $TicketObject-&gt;ArticleFlagDelete(
        ArticleID =&gt; 123,
        Key       =&gt; 'seen',
        AllUsers  =&gt; 1,         # delete for all users
    );

Events:
    ArticleFlagDelete

=cut

sub ArticleFlagDelete {
    my ( $Self, %Param ) = @_;

    # check needed stuff
    for (qw(ArticleID Key)) {
        if ( !$Param{$_} ) {
            $Self-&gt;{LogObject}-&gt;Log( Priority =&gt; 'error', Message =&gt; "Need $_!" );
            return;
        }
    }

    if ( !$Param{AllUsers} &amp;&amp; !$Param{UserID} ) {
        if ( !$Param{$_} ) {
            $Self-&gt;{LogObject}-&gt;Log( Priority =&gt; 'error', Message =&gt; "Need AllUsers or UserID!" );
            return;
        }
    }

    if ( $Param{AllUsers} ) {
        return if !$Self-&gt;{DBObject}-&gt;Do(
            SQL =&gt; '
                DELETE FROM article_flag
                WHERE article_id = ?
                    AND article_key = ?',
            Bind =&gt; [ \$Param{ArticleID}, \$Param{Key} ],
        );
    }
    else {
        return if !$Self-&gt;{DBObject}-&gt;Do(
            SQL =&gt; '
                DELETE FROM article_flag
                WHERE article_id = ?
                    AND create_by = ?
                    AND article_key = ?',
            Bind =&gt; [ \$Param{ArticleID}, \$Param{UserID}, \$Param{Key} ],
        );

        # event
        my %Article = $Self-&gt;ArticleGet(
            ArticleID     =&gt; $Param{ArticleID},
            UserID        =&gt; $Param{UserID},
            DynamicFields =&gt; 0,
        );

        $Self-&gt;EventHandler(
            Event =&gt; 'ArticleFlagDelete',
            Data  =&gt; {
                TicketID  =&gt; $Article{TicketID},
                ArticleID =&gt; $Param{ArticleID},
                Key       =&gt; $Param{Key},
                UserID    =&gt; $Param{UserID},
            },
            UserID =&gt; $Param{UserID},
        );
    }

    return 1;
}

=item ArticleFlagGet()

get article flags

    my %Flags = $TicketObject-&gt;ArticleFlagGet(
        ArticleID =&gt; 123,
        UserID    =&gt; 123,
    );

=cut

sub ArticleFlagGet {
    my ( $Self, %Param ) = @_;

    # check needed stuff
    for (qw(ArticleID UserID)) {
        if ( !$Param{$_} ) {
            $Self-&gt;{LogObject}-&gt;Log( Priority =&gt; 'error', Message =&gt; "Need $_!" );
            return;
        }
    }

    # sql query
    return if !$Self-&gt;{DBObject}-&gt;Prepare(
        SQL =&gt; '
            SELECT article_key, article_value
            FROM article_flag
            WHERE article_id = ?
                AND create_by = ?',
        Bind =&gt; [ \$Param{ArticleID}, \$Param{UserID} ],
        Limit =&gt; 1500,
    );
    my %Flag;
    while ( my @Row = $Self-&gt;{DBObject}-&gt;FetchrowArray() ) {
        $Flag{ $Row[0] } = $Row[1];
    }
    return %Flag;
}

=item ArticleFlagsOfTicketGet()

get all article flags of a ticket

    my %Flags = $TicketObject-&gt;ArticleFlagsOfTicketGet(
        TicketID  =&gt; 123,
        UserID    =&gt; 123,
    );

    returns (
        123 =&gt; {                    # ArticleID
            'Seen'  =&gt; 1,
            'Other' =&gt; 'something',
        },
    )

=cut

sub ArticleFlagsOfTicketGet {
    my ( $Self, %Param ) = @_;

    # check needed stuff
    for (qw(TicketID UserID)) {
        if ( !$Param{$_} ) {
            $Self-&gt;{LogObject}-&gt;Log( Priority =&gt; 'error', Message =&gt; "Need $_!" );
            return;
        }
    }

    # sql query
    return if !$Self-&gt;{DBObject}-&gt;Prepare(
        SQL =&gt; '
            SELECT article.id, article_flag.article_key, article_flag.article_value
            FROM article_flag, article
            WHERE article.id = article_flag.article_id
                AND article.ticket_id = ?
                AND article_flag.create_by = ?',
        Bind =&gt; [ \$Param{TicketID}, \$Param{UserID} ],
        Limit =&gt; 1500,
    );
    my %Flag;
    while ( my @Row = $Self-&gt;{DBObject}-&gt;FetchrowArray() ) {
        $Flag{ $Row[0] }-&gt;{ $Row[1] } = $Row[2];
    }
    return %Flag;
}

=item ArticleAccountedTimeGet()

returns the accounted time of a article.

    my $AccountedTime = $TicketObject-&gt;ArticleAccountedTimeGet(
        ArticleID =&gt; $ArticleID,
    );

=cut

sub ArticleAccountedTimeGet {
    my ( $Self, %Param ) = @_;

    # check needed stuff
    if ( !$Param{ArticleID} ) {
        $Self-&gt;{LogObject}-&gt;Log( Priority =&gt; 'error', Message =&gt; 'Need ArticleID!' );
        return;
    }

    # db query
    return if !$Self-&gt;{DBObject}-&gt;Prepare(
        SQL  =&gt; 'SELECT time_unit FROM time_accounting WHERE article_id = ?',
        Bind =&gt; [ \$Param{ArticleID} ],
    );
    my $AccountedTime = 0;
    while ( my @Row = $Self-&gt;{DBObject}-&gt;FetchrowArray() ) {
        $Row[0] =~ s/,/./g;
        $AccountedTime = $AccountedTime + $Row[0];
    }
    return $AccountedTime;
}

=item ArticleAccountedTimeDelete()

delete accounted time of article

    my $Success = $TicketObject-&gt;ArticleAccountedTimeDelete(
        ArticleID =&gt; $ArticleID,
    );

=cut

sub ArticleAccountedTimeDelete {
    my ( $Self, %Param ) = @_;

    # check needed stuff
    if ( !$Param{ArticleID} ) {
        $Self-&gt;{LogObject}-&gt;Log( Priority =&gt; 'error', Message =&gt; 'Need ArticleID!' );
        return;
    }

    # db query
    return if !$Self-&gt;{DBObject}-&gt;Do(
        SQL  =&gt; 'DELETE FROM time_accounting WHERE article_id = ?',
        Bind =&gt; [ \$Param{ArticleID} ],
    );
    return 1;
}

1;

# the following is the pod for Kernel/System/Ticket/ArticleStorage*.pm

=item ArticleDelete()

delete an article, its plain message, and all attachments

    my $Success = $TicketObject-&gt;ArticleDelete(
        ArticleID =&gt; 123,
        UserID    =&gt; 123,
    );

=item ArticleDeletePlain()

delete a plain article

    my $Success = $TicketObject-&gt;ArticleDeletePlain(
        ArticleID =&gt; 123,
        UserID    =&gt; 123,
    );

=item ArticleDeleteAttachment()

delete all attachments of an article

    my $Success = $TicketObject-&gt;ArticleDeleteAttachment(
        ArticleID =&gt; 123,
        UserID    =&gt; 123,
    );

=item ArticleWritePlain()

write a plain email to storage

    my $Success = $TicketObject-&gt;ArticleWritePlain(
        ArticleID =&gt; 123,
        Email     =&gt; $EmailAsString,
        UserID    =&gt; 123,
    );

=item ArticlePlain()

get plain article/email

    my $PlainMessage = $TicketObject-&gt;ArticlePlain(
        ArticleID =&gt; 123,
        UserID    =&gt; 123,
    );

=item ArticleWriteAttachment()

write an article attachment to storage

    my $Success = $TicketObject-&gt;ArticleWriteAttachment(
        Content            =&gt; $ContentAsString,
        ContentType        =&gt; 'text/html; charset="iso-8859-15"',
        Filename           =&gt; 'lala.html',
        ContentID          =&gt; 'cid-1234', # optional
        ContentAlternative =&gt; 0,          # optional, alternative content to shown as body
        ArticleID          =&gt; 123,
        UserID             =&gt; 123,
    );

You also can use "Force =&gt; 1" to not check if a filename already exists, it force to use the given file name. Otherwise a new file name like "oldfile-2.html" is used.

=item ArticleAttachment()

get article attachment (Content, ContentType, Filename and optional ContentID, ContentAlternative)

    my %Attachment = $TicketObject-&gt;ArticleAttachment(
        ArticleID =&gt; 123,
        FileID    =&gt; 1,   # as returned by ArticleAttachmentIndex
        UserID    =&gt; 123,
    );

returns:

    my %Attachment = (
        Content            =&gt; "xxxx",     # actual attachment contents
        ContentAlternative =&gt; "",
        ContentID          =&gt; "",
        ContentType        =&gt; "application/pdf",
        Filename           =&gt; "StdAttachment-Test1.pdf",
        Filesize           =&gt; "4.6 KBytes",
        FilesizeRaw        =&gt; 4722,
    );

=item ArticleAttachmentIndex()

get article attachment index as hash

 (ID =&gt; hashref (Filename, Filesize, ContentID (if exists), ContentAlternative(if exists) ))

    my %Index = $TicketObject-&gt;ArticleAttachmentIndex(
        ArticleID =&gt; 123,
        UserID    =&gt; 123,
    );

or with "StripPlainBodyAsAttachment =&gt; 1" feature to not include first
attachment (not include text body, html body as attachment and inline attachments)

    my %Index = $TicketObject-&gt;ArticleAttachmentIndex(
        ArticleID                  =&gt; 123,
        UserID                     =&gt; 123,
        Article                    =&gt; \%Article,
        StripPlainBodyAsAttachment =&gt; 1,
    );

or with "StripPlainBodyAsAttachment =&gt; 2" feature to not include first
attachment (not include text body as attachment)

    my %Index = $TicketObject-&gt;ArticleAttachmentIndex(
        ArticleID                  =&gt; 123,
        UserID                     =&gt; 123,
        Article                    =&gt; \%Article,
        StripPlainBodyAsAttachment =&gt; 2,
    );

or with "StripPlainBodyAsAttachment =&gt; 3" feature to not include first
attachment (not include text body and html body as attachment)

    my %Index = $TicketObject-&gt;ArticleAttachmentIndex(
        ArticleID                  =&gt; 123,
        UserID                     =&gt; 123,
        Article                    =&gt; \%Article,
        StripPlainBodyAsAttachment =&gt; 3,
    );

returns:

    my %Index = {
        '1' =&gt; {
            'ContentAlternative' =&gt; '',
            'ContentID' =&gt; '',
            'Filesize' =&gt; '4.6 KBytes',
            'ContentType' =&gt; 'application/pdf',
            'Filename' =&gt; 'StdAttachment-Test1.pdf',
            'FilesizeRaw' =&gt; 4722
        },
        '2' =&gt; {
            'ContentAlternative' =&gt; '',
            'ContentID' =&gt; '',
            'Filesize' =&gt; '183 Bytes',
            'ContentType' =&gt; 'text/html; charset="utf-8"',
            'Filename' =&gt; 'file-2',
            'FilesizeRaw' =&gt; 183
        },
    };

=cut

sub ArticleAttachmentIndex {
    my ( $Self, %Param ) = @_;

    # check needed stuff
    for (qw(ArticleID UserID)) {
        if ( !$Param{$_} ) {
            $Self-&gt;{LogObject}-&gt;Log( Priority =&gt; 'error', Message =&gt; "Need $_!" );
            return;
        }
    }

    # get attachment index from backend
    my %Attachments = $Self-&gt;ArticleAttachmentIndexRaw(%Param);

    # stript plain attachments and e. g. html attachments
    if ( $Param{StripPlainBodyAsAttachment} &amp;&amp; $Param{Article} ) {

        # plain attachment mime type vs. html attachment mime type check
        # remove plain body, rename html attachment
        my $AttachmentIDPlain = 0;
        my $AttachmentIDHTML  = 0;
        for my $AttachmentID ( sort keys %Attachments ) {
            my %File = %{ $Attachments{$AttachmentID} };

            # find plain attachment
            if (
                !$AttachmentIDPlain
                &amp;&amp;
                $File{Filename} eq 'file-1'
                &amp;&amp; $File{ContentType} =~ /text\/plain/i
                )
            {
                $AttachmentIDPlain = $AttachmentID;
            }

            # find html attachment
            #  o file-[12], is plain+html attachment
            #  o file-1.html, is only html attachment
            if (
                !$AttachmentIDHTML
                &amp;&amp;
                ( $File{Filename} =~ /^file-[12]$/ || $File{Filename} eq 'file-1.html' )
                &amp;&amp; $File{ContentType} =~ /text\/html/i
                )
            {
                $AttachmentIDHTML = $AttachmentID;
            }
        }
        if ($AttachmentIDHTML) {
            delete $Attachments{$AttachmentIDPlain};

            # remove any files with content-id from attachment list and listed in html body
            if ( $Param{StripPlainBodyAsAttachment} eq 1 ) {

                # get html body
                my %Attachment = $Self-&gt;ArticleAttachment(
                    ArticleID =&gt; $Param{ArticleID},
                    FileID    =&gt; $AttachmentIDHTML,
                    UserID    =&gt; $Param{UserID},
                );

                for my $AttachmentID ( sort keys %Attachments ) {
                    my %File = %{ $Attachments{$AttachmentID} };
                    next if !$File{ContentID};

                    # content id cleanup
                    $File{ContentID} =~ s/^&lt;//;
                    $File{ContentID} =~ s/&gt;$//;
                    if ( $File{ContentID} &amp;&amp; $Attachment{Content} =~ /\Q$File{ContentID}\E/i ) {
                        delete $Attachments{$AttachmentID};
                    }
                }
            }

            # only strip html body attachment by "1" or "3"
            if (
                $Param{StripPlainBodyAsAttachment} eq 1
                || $Param{StripPlainBodyAsAttachment} eq 3
                )
            {
                delete $Attachments{$AttachmentIDHTML};
            }
            $Param{Article}-&gt;{AttachmentIDOfHTMLBody} = $AttachmentIDHTML;
        }

        # plain body size vs. attched body size check
        # and remove attachment if it's email body
        if ( !$AttachmentIDHTML ) {
            my $AttachmentIDPlain = 0;
            my %AttachmentFilePlain;
            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'
                    &amp;&amp; $File{ContentType} =~ /text\/plain/i
                    )
                {
                    $AttachmentIDPlain   = $AttachmentID;
                    %AttachmentFilePlain = %File;
                    last;
                }
            }

            # plain attachment detected and remove it from attachment index
            if (%AttachmentFilePlain) {

                # check body size vs. attachment size to be sure
                my $BodySize = bytes::length( $Param{Article}-&gt;{Body} );

                # check size by tolerance of 1.1 factor (because of charset difs)
                if (
                    $BodySize / 1.1 &lt; $AttachmentFilePlain{FilesizeRaw}
                    &amp;&amp; $BodySize * 1.1 &gt; $AttachmentFilePlain{FilesizeRaw}
                    )
                {
                    delete $Attachments{$AttachmentIDPlain};
                }
            }
        }
    }

    return %Attachments;
}

1;

=back

=head1 TERMS AND CONDITIONS

This software is part of the OTRS project (L&lt;http://otrs.org/&gt;).

This software comes with ABSOLUTELY NO WARRANTY. For details, see
the enclosed file COPYING for license information (AGPL). If you
did not receive this file, see L&lt;http://www.gnu.org/licenses/agpl.txt&gt;.

=cut
</pre></body></html>