1# --
2# Copyright (C) 2001-2020 OTRS AG, https://otrs.com/
3# --
4# This software comes with ABSOLUTELY NO WARRANTY. For details, see
5# the enclosed file COPYING for license information (GPL). If you
6# did not receive this file, see https://www.gnu.org/licenses/gpl-3.0.txt.
7# --
8
9package Kernel::System::Email::SMTP;
10
11use strict;
12use warnings;
13
14use Net::SMTP;
15
16our @ObjectDependencies = (
17    'Kernel::Config',
18    'Kernel::System::DB',
19    'Kernel::System::Encode',
20    'Kernel::System::Log',
21    'Kernel::System::CommunicationLog',
22);
23
24sub new {
25    my ( $Type, %Param ) = @_;
26
27    # allocate new hash for object
28    my $Self = {%Param};
29    bless( $Self, $Type );
30
31    # debug
32    $Self->{Debug} = $Param{Debug} || 0;
33    if ( $Self->{Debug} > 2 ) {
34
35        # shown on STDERR
36        $Self->{SMTPDebug} = 1;
37    }
38
39    ( $Self->{SMTPType} ) = ( $Type =~ m/::Email::(.*)$/i );
40
41    return $Self;
42}
43
44sub Check {
45    my ( $Self, %Param ) = @_;
46
47    $Param{CommunicationLogObject}->ObjectLogStart(
48        ObjectLogType => 'Connection',
49    );
50
51    my $Return = sub {
52        my %LocalParam = @_;
53        $Param{CommunicationLogObject}->ObjectLogStop(
54            ObjectLogType => 'Connection',
55            Status        => $LocalParam{Success} ? 'Successful' : 'Failed',
56        );
57
58        return %LocalParam;
59    };
60
61    my $ReturnSuccess = sub { return $Return->( @_, Success => 1, ); };
62    my $ReturnError   = sub { return $Return->( @_, Success => 0, ); };
63
64    # get config object
65    my $ConfigObject = $Kernel::OM->Get('Kernel::Config');
66
67    # get config data
68    $Self->{FQDN}     = $ConfigObject->Get('FQDN');
69    $Self->{MailHost} = $ConfigObject->Get('SendmailModule::Host')
70        || die "No SendmailModule::Host found in Kernel/Config.pm";
71    $Self->{SMTPPort} = $ConfigObject->Get('SendmailModule::Port');
72    $Self->{User}     = $ConfigObject->Get('SendmailModule::AuthUser');
73    $Self->{Password} = $ConfigObject->Get('SendmailModule::AuthPassword');
74
75    $Param{CommunicationLogObject}->ObjectLog(
76        ObjectLogType => 'Connection',
77        Priority      => 'Debug',
78        Key           => 'Kernel::System::Email::SMTP',
79        Value         => 'Testing connection to SMTP service (3 attempts max.).',
80    );
81
82    # 3 possible attempts to connect to the SMTP server.
83    # (MS Exchange Servers have sometimes problems on port 25)
84    my $SMTP;
85
86    my $TryConnectMessage = sprintf
87        "%%s: Trying to connect to '%s%s' on %s with SMTP type '%s'.",
88        $Self->{MailHost},
89        ( $Self->{SMTPPort} ? ':' . $Self->{SMTPPort} : '' ),
90        $Self->{FQDN},
91        $Self->{SMTPType};
92    TRY:
93    for my $Try ( 1 .. 3 ) {
94
95        $Param{CommunicationLogObject}->ObjectLog(
96            ObjectLogType => 'Connection',
97            Priority      => 'Debug',
98            Key           => 'Kernel::System::Email::SMTP',
99            Value         => sprintf( $TryConnectMessage, $Try, ),
100        );
101
102        # connect to mail server
103        eval {
104            $SMTP = $Self->_Connect(
105                MailHost  => $Self->{MailHost},
106                FQDN      => $Self->{FQDN},
107                SMTPPort  => $Self->{SMTPPort},
108                SMTPDebug => $Self->{SMTPDebug},
109            );
110            return 1;
111        } || do {
112            my $Error = $@;
113            $Kernel::OM->Get('Kernel::System::Log')->Log(
114                Priority => 'error',
115                Message  => sprintf(
116                    "SMTP, connection try %s, unexpected error captured: %s",
117                    $Try,
118                    $Error,
119                ),
120            );
121        };
122
123        last TRY if $SMTP;
124
125        $Param{CommunicationLogObject}->ObjectLog(
126            ObjectLogType => 'Connection',
127            Priority      => 'Debug',
128            Key           => 'Kernel::System::Email::SMTP',
129            Value         => "$Try: Connection could not be established. Waiting for 0.3 seconds.",
130        );
131
132        # sleep 0,3 seconds;
133        select( undef, undef, undef, 0.3 );    ## no critic
134    }
135
136    # return if no connect was possible
137    if ( !$SMTP ) {
138
139        $Param{CommunicationLogObject}->ObjectLog(
140            ObjectLogType => 'Connection',
141            Priority      => 'Error',
142            Key           => 'Kernel::System::Email::SMTP',
143            Value         => "Could not connect to host '$Self->{MailHost}'. ErrorMessage: $!",
144        );
145
146        return $ReturnError->(
147            ErrorMessage => "Can't connect to $Self->{MailHost}: $!!",
148        );
149    }
150
151    # Enclose SMTP in a wrapper to handle unexpected exceptions
152    $SMTP = $Self->_GetSMTPSafeWrapper(
153        SMTP => $SMTP,
154    );
155
156    # use smtp auth if configured
157    if ( $Self->{User} && $Self->{Password} ) {
158
159        $Param{CommunicationLogObject}->ObjectLog(
160            ObjectLogType => 'Connection',
161            Priority      => 'Debug',
162            Key           => 'Kernel::System::Email::SMTP',
163            Value         => "Using SMTP authentication with user '$Self->{User}' and (hidden) password.",
164        );
165
166        if ( !$SMTP->( 'auth', $Self->{User}, $Self->{Password} ) ) {
167
168            my $Code  = $SMTP->( 'code', );
169            my $Error = $Code . ', ' . $SMTP->( 'message', );
170
171            $SMTP->( 'quit', );
172
173            $Param{CommunicationLogObject}->ObjectLog(
174                ObjectLogType => 'Connection',
175                Priority      => 'Error',
176                Key           => 'Kernel::System::Email::SMTP',
177                Value         => "SMTP authentication failed (SMTP code: $Code, ErrorMessage: $Error).",
178            );
179
180            return $ReturnError->(
181                ErrorMessage => "SMTP authentication failed: $Error!",
182                Code         => $Code,
183            );
184        }
185    }
186
187    return $ReturnSuccess->(
188        SMTP => $SMTP,
189    );
190}
191
192sub Send {
193    my ( $Self, %Param ) = @_;
194
195    $Param{CommunicationLogObject}->ObjectLog(
196        ObjectLogType => 'Message',
197        Priority      => 'Info',
198        Key           => 'Kernel::System::Email::SMTP',
199        Value         => 'Received message for sending, validating message contents.',
200    );
201
202    # check needed stuff
203    for (qw(Header Body ToArray)) {
204        if ( !$Param{$_} ) {
205
206            $Param{CommunicationLogObject}->ObjectLog(
207                ObjectLogType => 'Message',
208                Priority      => 'Error',
209                Key           => 'Kernel::System::Email::SMTP',
210                Value         => "Need $_!",
211            );
212
213            return $Self->_SendError(
214                %Param,
215                ErrorMessage => "Need $_!",
216            );
217        }
218    }
219    if ( !$Param{From} ) {
220        $Param{From} = '';
221    }
222
223    # connect to smtp server
224    my %Result = $Self->Check(%Param);
225
226    if ( !$Result{Success} ) {
227        return $Self->_SendError( %Param, %Result, );
228    }
229
230    # set/get SMTP handle
231    my $SMTP = $Result{SMTP};
232
233    $Param{CommunicationLogObject}->ObjectLog(
234        ObjectLogType => 'Message',
235        Priority      => 'Debug',
236        Key           => 'Kernel::System::Email::SMTP',
237        Value         => "Sending envelope from (mail from: $Param{From}) to server.",
238    );
239
240    # set envelope from, return if from was not accepted by the server
241    if ( !$SMTP->( 'mail', $Param{From}, ) ) {
242
243        my $FullErrorMessage = sprintf(
244            "Envelope from '%s' not accepted by the server: %s, %s!",
245            $Param{From},
246            $SMTP->( 'code', ),
247            $SMTP->( 'message', ),
248        );
249
250        $Param{CommunicationLogObject}->ObjectLog(
251            ObjectLogType => 'Message',
252            Priority      => 'Error',
253            Key           => 'Kernel::System::Email::SMTP',
254            Value         => $FullErrorMessage,
255        );
256
257        return $Self->_SendError(
258            %Param,
259            ErrorMessage => $FullErrorMessage,
260            SMTP         => $SMTP,
261        );
262    }
263
264    TO:
265    for my $To ( @{ $Param{ToArray} } ) {
266
267        $Param{CommunicationLogObject}->ObjectLog(
268            ObjectLogType => 'Message',
269            Priority      => 'Debug',
270            Key           => 'Kernel::System::Email::SMTP',
271            Value         => "Sending envelope to (rcpt to: $To) to server.",
272        );
273
274        # Check if the recipient is valid
275        next TO if $SMTP->( 'to', $To, );
276
277        my $FullErrorMessage = sprintf(
278            "Envelope to '%s' not accepted by the server: %s, %s!",
279            $To,
280            $SMTP->( 'code', ),
281            $SMTP->( 'message', ),
282        );
283
284        $Param{CommunicationLogObject}->ObjectLog(
285            ObjectLogType => 'Message',
286            Priority      => 'Error',
287            Key           => 'Kernel::System::Email::SMTP',
288            Value         => $FullErrorMessage,
289        );
290
291        return $Self->_SendError(
292            %Param,
293            ErrorMessage => $FullErrorMessage,
294            SMTP         => $SMTP,
295        );
296    }
297
298    my $ToString = join ',', @{ $Param{ToArray} };
299
300    # get encode object
301    my $EncodeObject = $Kernel::OM->Get('Kernel::System::Encode');
302
303    # encode utf8 header strings (of course, there should only be 7 bit in there!)
304    $EncodeObject->EncodeOutput( $Param{Header} );
305
306    # encode utf8 body strings
307    $EncodeObject->EncodeOutput( $Param{Body} );
308
309    # send data
310    $Param{CommunicationLogObject}->ObjectLog(
311        ObjectLogType => 'Message',
312        Priority      => 'Debug',
313        Key           => 'Kernel::System::Email::SMTP',
314        Value         => "Sending message data to server.",
315    );
316
317    # Send email data by chunks because when in SSL mode, each SSL
318    # frame has a maximum of 16kB (Bug #12957).
319    # We send always the first 4000 characters until '$Data' is empty.
320    # If any error occur while sending data to the smtp server an exception
321    # is thrown and '$DataSent' will be undefined.
322    my $DataSent = eval {
323        my $Data      = ${ $Param{Header} } . "\n" . ${ $Param{Body} };
324        my $ChunkSize = 4000;
325
326        $SMTP->( 'data', ) || die "error starting data sending";
327
328        while ( my $DataLength = length $Data ) {
329            my $TmpChunkSize = ( $ChunkSize > $DataLength ) ? $DataLength : $ChunkSize;
330            my $Chunk        = substr $Data, 0, $TmpChunkSize;
331
332            $SMTP->( 'datasend', $Chunk, ) || die "error sending data chunk";
333
334            $Data = substr $Data, $TmpChunkSize;
335        }
336
337        $SMTP->( 'dataend', ) || die "error ending data sending";
338
339        return 1;
340    };
341
342    if ( !$DataSent ) {
343        my $FullErrorMessage = sprintf(
344            "Could not send message to server: %s, %s!",
345            $SMTP->( 'code', ),
346            $SMTP->( 'message', ),
347        );
348
349        $Param{CommunicationLogObject}->ObjectLog(
350            ObjectLogType => 'Message',
351            Priority      => 'Error',
352            Key           => 'Kernel::System::Email::SMTP',
353            Value         => $FullErrorMessage,
354        );
355
356        return $Self->_SendError(
357            %Param,
358            ErrorMessage => $FullErrorMessage,
359            SMTP         => $SMTP,
360        );
361    }
362
363    # debug
364    if ( $Self->{Debug} > 2 ) {
365        $Kernel::OM->Get('Kernel::System::Log')->Log(
366            Priority => 'notice',
367            Message  => "Sent email to '$ToString' from '$Param{From}'.",
368        );
369    }
370
371    $Param{CommunicationLogObject}->ObjectLog(
372        ObjectLogType => 'Message',
373        Priority      => 'Info',
374        Key           => 'Kernel::System::Email::SMTP',
375        Value         => "Email successfully sent from '$Param{From}' to '$ToString'.",
376    );
377
378    return $Self->_SendSuccess(
379        SMTP => $SMTP,
380        %Param
381    );
382}
383
384sub _Connect {
385    my ( $Self, %Param ) = @_;
386
387    # check needed stuff
388    for (qw(MailHost FQDN)) {
389        if ( !$Param{$_} ) {
390            $Kernel::OM->Get('Kernel::System::Log')->Log(
391                Priority => 'error',
392                Message  => "Need $_!",
393            );
394            return;
395        }
396    }
397
398    # Remove a possible port from the FQDN value
399    my $FQDN = $Param{FQDN};
400    $FQDN =~ s{:\d+}{}smx;
401
402    # set up connection connection
403    my $SMTP = Net::SMTP->new(
404        $Param{MailHost},
405        Hello   => $FQDN,
406        Port    => $Param{SMTPPort} || 25,
407        Timeout => 30,
408        Debug   => $Param{SMTPDebug},
409    );
410
411    return $SMTP;
412}
413
414sub _SendResult {
415    my ( $Self, %Param ) = @_;
416
417    my $SMTP = delete $Param{SMTP};
418    $SMTP->( 'quit', ) if $SMTP;
419
420    return {%Param};
421}
422
423sub _SendSuccess {
424    my ( $Self, %Param ) = @_;
425    return $Self->_SendResult(
426        Success => 1,
427        %Param
428    );
429}
430
431sub _SendError {
432    my ( $Self, %Param ) = @_;
433
434    my $SMTP = $Param{SMTP};
435    if ( $SMTP && !defined $Param{Code} ) {
436        $Param{Code} = $SMTP->( 'code', );
437    }
438
439    return $Self->_SendResult(
440        Success => 0,
441        %Param,
442        SMTPError => 1,
443    );
444}
445
446sub _GetSMTPSafeWrapper {
447    my ( $Self, %Param, ) = @_;
448
449    my $SMTP = $Param{SMTP};
450
451    return sub {
452        my $Operation   = shift;
453        my @LocalParams = @_;
454
455        my $ScalarResult;
456        my @ArrayResult = ();
457        my $Wantarray   = wantarray;
458
459        eval {
460            if ($Wantarray) {
461                @ArrayResult = $SMTP->$Operation( @LocalParams, );
462            }
463            else {
464                $ScalarResult = $SMTP->$Operation( @LocalParams, );
465            }
466
467            return 1;
468        } || do {
469            my $Error = $@;
470            $Kernel::OM->Get('Kernel::System::Log')->Log(
471                Priority => 'error',
472                Message  => sprintf(
473                    "Error while executing 'SMTP->%s(%s)': %s",
474                    $Operation,
475                    join( ',', @LocalParams ),
476                    $Error,
477                ),
478            );
479        };
480
481        return @ArrayResult if $Wantarray;
482        return $ScalarResult;
483    };
484}
485
4861;
487