ProcessAutomatic.pm 15.2 KB
Newer Older
1
2
3
4
5
6
7
8
9
# -*- indent-tabs-mode: nil; -*-
# vim:ft=perl:et:sw=4
# $Id$

# Sympa - SYsteme de Multi-Postage Automatique
#
# Copyright (c) 1997, 1998, 1999 Institut Pasteur & Christophe Wolfhugel
# Copyright (c) 1997, 1998, 1999, 2000, 2001, 2002, 2003, 2004, 2005,
# 2006, 2007, 2008, 2009, 2010, 2011 Comite Reseau des Universites
10
# Copyright (c) 2011, 2012, 2013, 2014, 2015, 2016, 2017 GIP RENATER
11
12
# Copyright 2017, 2019, 2020 The Sympa Community. See the AUTHORS.md
# file at the top-level directory of this distribution and at
13
# <https://github.com/sympa-community/sympa.git>.
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

package Sympa::Spindle::ProcessAutomatic;

use strict;
use warnings;
use English qw(-no_match_vars);
use File::Copy qw();

use Sympa;
use Conf;
use Sympa::Family;
use Sympa::List;
use Sympa::Log;
use Sympa::Mailer;
41
use Sympa::Spindle::ProcessRequest;
42
use Sympa::Spool::Incoming;
43
use Sympa::Spool::Listmaster;
44
use Sympa::Tools::Data;
45
46
47
48
49
50
51
52
53
54
55

use base qw(Sympa::Spindle);

my $log = Sympa::Log->instance;

use constant _distaff => 'Sympa::Spool::Automatic';

sub _init {
    my $self  = shift;
    my $state = shift;

56
    if ($state == 1) {
57
        # Process grouped notifications.
58
        Sympa::Spool::Listmaster->instance->flush;
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
    }

    1;
}

sub _on_success {
    my $self    = shift;
    my $message = shift;
    my $handle  = shift;

    if ($self->{keepcopy}) {
        unless (
            File::Copy::copy(
                $self->{distaff}->{directory} . '/' . $handle->basename,
                $self->{keepcopy} . '/' . $handle->basename
            )
Luc Didry's avatar
Luc Didry committed
75
        ) {
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
            $log->syslog(
                'notice',
                'Could not rename %s/%s to %s/%s: %m',
                $self->{distaff}->{directory},
                $handle->basename,
                $self->{keepcopy},
                $handle->basename
            );
        }
    }

    $self->SUPER::_on_success($message, $handle);
}

# Old name: process_message() in sympa_automatic.pl.
sub _twist {
    my $self    = shift;
    my $message = shift;

    unless (defined $message->{'message_id'}
        and length $message->{'message_id'}) {
        $log->syslog('err', 'Message %s has no message ID', $message);
        $log->db_log(
            #'robot'        => $robot,
            #'list'         => $listname,
            'action'       => 'process_message',
            'parameters'   => $message->get_id,
            'target_email' => "",
            'msg_id'       => "",
            'status'       => 'error',
            'error_type'   => 'no_message_id',
            'user_email'   => $message->{'sender'}
        );
        return undef;
    }

    my $msg_id = $message->{message_id};

    $log->syslog(
        'notice',
        'Processing %s; envelope_sender=%s; message_id=%s; sender=%s',
        $message,
        $message->{envelope_sender},
        $message->{message_id},
        $message->{sender}
    );

    my $robot;
    my $listname;

    $robot =
        (ref $message->{context} eq 'Sympa::List')
        ? $message->{context}->{'domain'}
        : $message->{context};
    $listname = $message->{'listname'};

    ## Ignoring messages with no sender
    my $sender = $message->{'sender'};
    unless ($message->{'md5_check'} or $sender) {
        $log->syslog('err', 'No sender found in message %s', $message);
        $log->db_log(
            'robot'        => $robot,
            'list'         => $listname,
            'action'       => 'process_message',
            'parameters'   => "",
            'target_email' => "",
            'msg_id'       => $msg_id,
            'status'       => 'error',
            'error_type'   => 'no_sender',
            'user_email'   => $sender
        );
        return undef;
    }

150
    # Unknown robot.
151
152
    unless ($message->{'md5_check'} or Conf::valid_robot($robot)) {
        $log->syslog('err', 'Robot %s does not exist', $robot);
153
        Sympa::send_dsn('*', $message, {}, '5.1.2');
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
        $log->db_log(
            'robot'        => $robot,
            'list'         => $listname,
            'action'       => 'process_message',
            'parameters'   => "",
            'target_email' => "",
            'msg_id'       => $msg_id,
            'status'       => 'error',
            'error_type'   => 'unknown_robot',
            'user_email'   => $sender
        );
        return undef;
    }

    # Load spam status.
    $message->check_spam_status;
    # Check DKIM signatures.
    $message->check_dkim_signature;
    # Check S/MIME signature.
    $message->check_smime_signature;
    # Decrypt message.  On success, check nested S/MIME signature.
    if ($message->smime_decrypt and not $message->{'smime_signed'}) {
        $message->check_smime_signature;
    }

    # *** Now message content may be altered. ***

    # Enable SMTP logging if required.
182
    Sympa::Mailer->instance->{log_smtp} = $self->{log_smtp}
183
184
        || Sympa::Tools::Data::smart_eq(
        Conf::get_robot_conf($robot, 'log_smtp'), 'on');
185
    # setting log_level using conf unless it is set by calling option
186
187
188
189
    $log->{level} =
        (defined $self->{log_level})
        ? $self->{log_level}
        : Conf::get_robot_conf($robot, 'log_level');
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242

    ## Strip of the initial X-Sympa-To and X-Sympa-Checksum internal headers
    delete $message->{'rcpt'};
    delete $message->{'checksum'};

    my $list =
        (ref $message->{context} eq 'Sympa::List')
        ? $message->{context}
        : undef;

    # Maybe we are an automatic list
    #_amr ici on ne doit prendre que la première ligne !
    my ($dyn_list_family, $dyn_just_created);
    # we care of fake headers. If we put it, it's the 1st one.
    $dyn_list_family = $message->{'family'};

    unless (defined $dyn_list_family and length $dyn_list_family) {
        $log->syslog(
            'err',
            'Internal server error: Automatic lists creation daemon should never proceed message %s without X-Sympa-Family header',
            $message
        );
        Sympa::send_notify_to_listmaster(
            '*',
            'intern_error',
            {   'error' =>
                    sprintf(
                    'Internal server error: Automatic lists creation daemon should never proceed message %s without X-Sympa-Family header',
                    $message)
            }
        );
        return undef;
    }
    delete $message->{'family'};

    unless (ref $list eq 'Sympa::List') {
        ## Automatic creation of a mailing list, based on a family
        my $dyn_family;
        unless ($dyn_family = Sympa::Family->new($dyn_list_family, $robot)) {
            $log->syslog(
                'err',
                'Failed to process message %s: family %s does not exist, impossible to create the dynamic list',
                $message,
                $dyn_list_family
            );
            Sympa::send_notify_to_listmaster(
                $robot,
                'automatic_list_creation_failed',
                {   'family' => $dyn_list_family,
                    'robot'  => $robot,
                    'msg_id' => $msg_id,
                }
            );
243
            Sympa::send_dsn($robot, $message, {}, '5.3.5');
244
245
246
            return undef;
        }

247
        my $spindle_req = Sympa::Spindle::ProcessRequest->new(
Luc Didry's avatar
Luc Didry committed
248
249
            context          => $dyn_family,
            action           => 'create_automatic_list',
250
            parameters       => {listname => $listname},
Luc Didry's avatar
Luc Didry committed
251
252
253
254
            sender           => $sender,
            smime_signed     => $message->{'smime_signed'},
            md5_check        => $message->{'md5_check'},
            dkim_pass        => $message->{'dkim_pass'},
255
256
257
258
259
260
261
262
263
264
            scenario_context => {
                sender             => $sender,
                message            => $message,
                family             => $dyn_family,
                automatic_listname => $listname,
            },
        );
        unless ($spindle_req and $spindle_req->spin) {
            $log->syslog('err', 'Cannot create dynamic list %s', $listname);
            return undef;
Luc Didry's avatar
Luc Didry committed
265
        } elsif (
IKEDA Soji's avatar
IKEDA Soji committed
266
267
268
269
270
271
272
            not($spindle_req->success
                and $list = Sympa::List->new(
                    $listname,
                    $dyn_family->{'domain'},
                    {just_try => 1}
                )
            )
Luc Didry's avatar
Luc Didry committed
273
        ) {
274
275
276
277
            $log->syslog('err',
                'Unable to create list %s. Message %s ignored',
                $listname, $message);
            Sympa::send_notify_to_listmaster(
278
                $dyn_family->{'domain'},
279
280
281
282
283
284
285
                'automatic_list_creation_failed',
                {   'listname' => $listname,
                    'family'   => $dyn_list_family,
                    'robot'    => $robot,
                    'msg_id'   => $msg_id,
                }
            );
286
            Sympa::send_dsn($robot, $message, {}, '5.3.5');
287
            $log->db_log(
288
                'robot'        => $dyn_family->{'domain'},
289
290
                'list'         => $listname,
                'action'       => 'process_message',
291
                'parameters'   => $msg_id . "," . $dyn_family->{'domain'},
292
293
294
295
296
297
298
                'target_email' => '',
                'msg_id'       => $msg_id,
                'status'       => 'error',
                'error_type'   => 'internal',
                'user_email'   => $sender
            );
            return undef;
299
300
301
302
        } else {
            # Overwrite context of the message.
            $message->{context} = $list;
            $dyn_just_created = 1;
303
304
305
306
        }
    }

    if ($dyn_just_created) {
IKEDA Soji's avatar
IKEDA Soji committed
307
        unless (defined $list->sync_include('member')) {
308
309
310
311
312
313
            $log->syslog(
                'err',
                'Failed to synchronize list members of dynamic list %s from %s family',
                $list,
                $dyn_list_family
            );
314
315
            # As list may be purged, use robot context.
            Sympa::send_dsn($robot, $message, {}, '4.2.1');
316
317
318
319
320
321
322
323
324
325
326
327
328
            $log->db_log(
                'robot'        => $robot,
                'list'         => $list->{'name'},
                'action'       => 'process_message',
                'parameters'   => "",
                'target_email' => "",
                'msg_id'       => $msg_id,
                'status'       => 'error',
                'error_type'   => 'dyn_cant_sync',
                'user_email'   => $sender
            );
            # purge the unwanted empty automatic list
            if ($Conf::Conf{'automatic_list_removal'} =~ /if_empty/i) {
329
                Sympa::Spindle::ProcessRequest->new(
Luc Didry's avatar
Luc Didry committed
330
331
332
333
                    context          => $robot,
                    action           => 'close_list',
                    current_list     => $list,
                    mode             => 'purge',
334
335
                    scenario_context => {skip => 1},
                )->spin;
336
337
338
            }
            return undef;
        }
339
        unless ($list->get_total) {
340
341
342
            $log->syslog('err',
                'Dynamic list %s from %s family has ZERO subscribers',
                $list, $dyn_list_family);
343
344
            # As list may be purged, use robot context.
            Sympa::send_dsn($robot, $message, {}, '4.2.4');
345
346
347
348
349
350
351
352
353
354
355
356
357
            $log->db_log(
                'robot'        => $robot,
                'list'         => $list->{'name'},
                'action'       => 'process_message',
                'parameters'   => "",
                'target_email' => "",
                'msg_id'       => $msg_id,
                'status'       => 'error',
                'error_type'   => 'list_unknown',
                'user_email'   => $sender
            );
            # purge the unwanted empty automatic list
            if ($Conf::Conf{'automatic_list_removal'} =~ /if_empty/i) {
358
                Sympa::Spindle::ProcessRequest->new(
Luc Didry's avatar
Luc Didry committed
359
360
361
362
                    context          => $robot,
                    action           => 'close_list',
                    current_list     => $list,
                    mode             => 'purge',
363
364
                    scenario_context => {skip => 1},
                )->spin;
365
366
367
368
369
370
371
372
373
            }
            return undef;
        }
        $log->syslog('info',
            'Successfully create list %s with %s subscribers',
            $list, $list->get_total());
    }

    # Do not process messages in list creation.  Move them to main spool.
374
375
    my $marshalled =
        Sympa::Spool::Incoming->new->store($message, original => 1);
376
377
378
379
380
381
382
383
384
385
386
    if ($marshalled) {
        $log->syslog('notice',
            'Message %s is stored into incoming spool as <%s>',
            $message, $marshalled);
    } else {
        $log->syslog(
            'err',
            'Unable to move in spool for processing message %s to list %s (daemon_usage = creation)',
            $message,
            $list
        );
387
388
389
        Sympa::send_notify_to_listmaster($list, 'mail_intern_error',
            {error => '', who => $sender, msg_id => $msg_id,});
        Sympa::send_dsn($list, $message, {}, '5.3.0');
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
        return undef;
    }

    return 1;
}

1;
__END__

=encoding utf-8

=head1 NAME

Sympa::Spindle::ProcessAutomatic - Workflow of automatic list creation

=head1 SYNOPSIS

  use Sympa::Spindle::ProcessAutomatic;

  my $spindle = Sympa::Spindle::ProcessAutomatic->new;
  $spindle->spin;

=head1 DESCRIPTION

L<Sympa::Spindle::ProcessAutomatic> defines workflow to process messages
for automatic list creation.

When spin() method is invoked, it reads the messages in automatic spool.
If the list a message is bound for has not been there and list creation is
authorized, it will be created.  Then the message is stored into incoming
message spool again and waits for processing by
L<Sympa::Spindle::ProcessIncoming>.

423
Order to process messages in source spool are controlled by modification time
424
425
426
of files and delivery date.
Some messages are skipped according to these priorities
(See L<Sympa::Spool::Automatic>):
427
428
429
430
431
432
433
434
435

=over

=item *

Messages with lowest priority (C<z> or C<Z>) are skipped.

=item *

436
Messages with possibly higher priority are chosen.
437
438
439
440
441
This is done by skipping messages with lower priority than those already
found.

=back

442
443
444
445
446
447
=head2 Public methods

See also L<Sympa::Spindle/"Public methods">.

=over

448
449
450
=item new ( [ keepcopy =E<gt> $directory ],
[ log_level =E<gt> $level ],
[ log_smtp =E<gt> 0|1 ] )
451
452
453

=item spin ( )

454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
new() may take following options:

=over

=item keepcopy =E<gt> $directory

spin() keeps copy of successfully processed messages in $directory.

=item log_level =E<gt> $level

Overwrites log_level parameter in configuration.

=item log_smtp =E<gt> 0|1

Overwrites log_smtp parameter in configuration.

=back
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495

=back

=head2 Properties

See also L<Sympa::Spindle/"Properties">.

=over

=item {distaff}

Instance of L<Sympa::Spool::Automatic> class.

=back

=head1 SEE ALSO

L<Sympa::Message>,
L<Sympa::Spindle>, L<Sympa::Spool::Automatic>, L<Sympa::Spool::Incoming>.

=head1 HISTORY

L<Sympa::Spindle::ProcessAutomatic> appeared on Sympa 6.2.10.

=cut