1# -*- indent-tabs-mode: nil; -*-
2# vim:ft=perl:et:sw=4
3# $Id$
4
5# Sympa - SYsteme de Multi-Postage Automatique
6#
7# Copyright (c) 1997, 1998, 1999 Institut Pasteur & Christophe Wolfhugel
8# Copyright (c) 1997, 1998, 1999, 2000, 2001, 2002, 2003, 2004, 2005,
9# 2006, 2007, 2008, 2009, 2010, 2011 Comite Reseau des Universites
10# Copyright (c) 2011, 2012, 2013, 2014, 2015, 2016, 2017 GIP RENATER
11# Copyright 2017, 2019, 2020 The Sympa Community. See the AUTHORS.md
12# file at the top-level directory of this distribution and at
13# <https://github.com/sympa-community/sympa.git>.
14#
15# This program is free software; you can redistribute it and/or modify
16# it under the terms of the GNU General Public License as published by
17# the Free Software Foundation; either version 2 of the License, or
18# (at your option) any later version.
19#
20# This program is distributed in the hope that it will be useful,
21# but WITHOUT ANY WARRANTY; without even the implied warranty of
22# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
23# GNU General Public License for more details.
24#
25# You should have received a copy of the GNU General Public License
26# along with this program.  If not, see <http://www.gnu.org/licenses/>.
27
28package Sympa::Spindle::ProcessAutomatic;
29
30use strict;
31use warnings;
32use English qw(-no_match_vars);
33use File::Copy qw();
34
35use Sympa;
36use Conf;
37use Sympa::Family;
38use Sympa::List;
39use Sympa::Log;
40use Sympa::Mailer;
41use Sympa::Spindle::ProcessRequest;
42use Sympa::Spool::Incoming;
43use Sympa::Spool::Listmaster;
44use Sympa::Tools::Data;
45
46use base qw(Sympa::Spindle);
47
48my $log = Sympa::Log->instance;
49
50use constant _distaff => 'Sympa::Spool::Automatic';
51
52sub _init {
53    my $self  = shift;
54    my $state = shift;
55
56    if ($state == 1) {
57        # Process grouped notifications.
58        Sympa::Spool::Listmaster->instance->flush;
59    }
60
61    1;
62}
63
64sub _on_success {
65    my $self    = shift;
66    my $message = shift;
67    my $handle  = shift;
68
69    if ($self->{keepcopy}) {
70        unless (
71            File::Copy::copy(
72                $self->{distaff}->{directory} . '/' . $handle->basename,
73                $self->{keepcopy} . '/' . $handle->basename
74            )
75        ) {
76            $log->syslog(
77                'notice',
78                'Could not rename %s/%s to %s/%s: %m',
79                $self->{distaff}->{directory},
80                $handle->basename,
81                $self->{keepcopy},
82                $handle->basename
83            );
84        }
85    }
86
87    $self->SUPER::_on_success($message, $handle);
88}
89
90# Old name: process_message() in sympa_automatic.pl.
91sub _twist {
92    my $self    = shift;
93    my $message = shift;
94
95    unless (defined $message->{'message_id'}
96        and length $message->{'message_id'}) {
97        $log->syslog('err', 'Message %s has no message ID', $message);
98        $log->db_log(
99            #'robot'        => $robot,
100            #'list'         => $listname,
101            'action'       => 'process_message',
102            'parameters'   => $message->get_id,
103            'target_email' => "",
104            'msg_id'       => "",
105            'status'       => 'error',
106            'error_type'   => 'no_message_id',
107            'user_email'   => $message->{'sender'}
108        );
109        return undef;
110    }
111
112    my $msg_id = $message->{message_id};
113
114    $log->syslog(
115        'notice',
116        'Processing %s; envelope_sender=%s; message_id=%s; sender=%s',
117        $message,
118        $message->{envelope_sender},
119        $message->{message_id},
120        $message->{sender}
121    );
122
123    my $robot;
124    my $listname;
125
126    $robot =
127        (ref $message->{context} eq 'Sympa::List')
128        ? $message->{context}->{'domain'}
129        : $message->{context};
130    $listname = $message->{'listname'};
131
132    ## Ignoring messages with no sender
133    my $sender = $message->{'sender'};
134    unless ($message->{'md5_check'} or $sender) {
135        $log->syslog('err', 'No sender found in message %s', $message);
136        $log->db_log(
137            'robot'        => $robot,
138            'list'         => $listname,
139            'action'       => 'process_message',
140            'parameters'   => "",
141            'target_email' => "",
142            'msg_id'       => $msg_id,
143            'status'       => 'error',
144            'error_type'   => 'no_sender',
145            'user_email'   => $sender
146        );
147        return undef;
148    }
149
150    # Unknown robot.
151    unless ($message->{'md5_check'} or Conf::valid_robot($robot)) {
152        $log->syslog('err', 'Robot %s does not exist', $robot);
153        Sympa::send_dsn('*', $message, {}, '5.1.2');
154        $log->db_log(
155            'robot'        => $robot,
156            'list'         => $listname,
157            'action'       => 'process_message',
158            'parameters'   => "",
159            'target_email' => "",
160            'msg_id'       => $msg_id,
161            'status'       => 'error',
162            'error_type'   => 'unknown_robot',
163            'user_email'   => $sender
164        );
165        return undef;
166    }
167
168    # Load spam status.
169    $message->check_spam_status;
170    # Check DKIM signatures.
171    $message->check_dkim_signature;
172    # Check S/MIME signature.
173    $message->check_smime_signature;
174    # Decrypt message.  On success, check nested S/MIME signature.
175    if ($message->smime_decrypt and not $message->{'smime_signed'}) {
176        $message->check_smime_signature;
177    }
178
179    # *** Now message content may be altered. ***
180
181    # Enable SMTP logging if required.
182    Sympa::Mailer->instance->{log_smtp} = $self->{log_smtp}
183        || Sympa::Tools::Data::smart_eq(
184        Conf::get_robot_conf($robot, 'log_smtp'), 'on');
185    # setting log_level using conf unless it is set by calling option
186    $log->{level} =
187        (defined $self->{log_level})
188        ? $self->{log_level}
189        : Conf::get_robot_conf($robot, 'log_level');
190
191    ## Strip of the initial X-Sympa-To and X-Sympa-Checksum internal headers
192    delete $message->{'rcpt'};
193    delete $message->{'checksum'};
194
195    my $list =
196        (ref $message->{context} eq 'Sympa::List')
197        ? $message->{context}
198        : undef;
199
200    # Maybe we are an automatic list
201    #_amr ici on ne doit prendre que la première ligne !
202    my ($dyn_list_family, $dyn_just_created);
203    # we care of fake headers. If we put it, it's the 1st one.
204    $dyn_list_family = $message->{'family'};
205
206    unless (defined $dyn_list_family and length $dyn_list_family) {
207        $log->syslog(
208            'err',
209            'Internal server error: Automatic lists creation daemon should never proceed message %s without X-Sympa-Family header',
210            $message
211        );
212        Sympa::send_notify_to_listmaster(
213            '*',
214            'intern_error',
215            {   'error' =>
216                    sprintf(
217                    'Internal server error: Automatic lists creation daemon should never proceed message %s without X-Sympa-Family header',
218                    $message)
219            }
220        );
221        return undef;
222    }
223    delete $message->{'family'};
224
225    unless (ref $list eq 'Sympa::List') {
226        ## Automatic creation of a mailing list, based on a family
227        my $dyn_family;
228        unless ($dyn_family = Sympa::Family->new($dyn_list_family, $robot)) {
229            $log->syslog(
230                'err',
231                'Failed to process message %s: family %s does not exist, impossible to create the dynamic list',
232                $message,
233                $dyn_list_family
234            );
235            Sympa::send_notify_to_listmaster(
236                $robot,
237                'automatic_list_creation_failed',
238                {   'family' => $dyn_list_family,
239                    'robot'  => $robot,
240                    'msg_id' => $msg_id,
241                }
242            );
243            Sympa::send_dsn($robot, $message, {}, '5.3.5');
244            return undef;
245        }
246
247        my $spindle_req = Sympa::Spindle::ProcessRequest->new(
248            context          => $dyn_family,
249            action           => 'create_automatic_list',
250            parameters       => {listname => $listname},
251            sender           => $sender,
252            smime_signed     => $message->{'smime_signed'},
253            md5_check        => $message->{'md5_check'},
254            dkim_pass        => $message->{'dkim_pass'},
255            scenario_context => {
256                sender             => $sender,
257                message            => $message,
258                family             => $dyn_family,
259                automatic_listname => $listname,
260            },
261        );
262        unless ($spindle_req and $spindle_req->spin) {
263            $log->syslog('err', 'Cannot create dynamic list %s', $listname);
264            return undef;
265        } elsif (
266            not($spindle_req->success
267                and $list = Sympa::List->new(
268                    $listname,
269                    $dyn_family->{'domain'},
270                    {just_try => 1}
271                )
272            )
273        ) {
274            $log->syslog('err',
275                'Unable to create list %s. Message %s ignored',
276                $listname, $message);
277            Sympa::send_notify_to_listmaster(
278                $dyn_family->{'domain'},
279                'automatic_list_creation_failed',
280                {   'listname' => $listname,
281                    'family'   => $dyn_list_family,
282                    'robot'    => $robot,
283                    'msg_id'   => $msg_id,
284                }
285            );
286            Sympa::send_dsn($robot, $message, {}, '5.3.5');
287            $log->db_log(
288                'robot'        => $dyn_family->{'domain'},
289                'list'         => $listname,
290                'action'       => 'process_message',
291                'parameters'   => $msg_id . "," . $dyn_family->{'domain'},
292                'target_email' => '',
293                'msg_id'       => $msg_id,
294                'status'       => 'error',
295                'error_type'   => 'internal',
296                'user_email'   => $sender
297            );
298            return undef;
299        } else {
300            # Overwrite context of the message.
301            $message->{context} = $list;
302            $dyn_just_created = 1;
303        }
304    }
305
306    if ($dyn_just_created) {
307        unless (defined $list->sync_include('member')) {
308            $log->syslog(
309                'err',
310                'Failed to synchronize list members of dynamic list %s from %s family',
311                $list,
312                $dyn_list_family
313            );
314            # As list may be purged, use robot context.
315            Sympa::send_dsn($robot, $message, {}, '4.2.1');
316            $log->db_log(
317                'robot'        => $robot,
318                'list'         => $list->{'name'},
319                'action'       => 'process_message',
320                'parameters'   => "",
321                'target_email' => "",
322                'msg_id'       => $msg_id,
323                'status'       => 'error',
324                'error_type'   => 'dyn_cant_sync',
325                'user_email'   => $sender
326            );
327            # purge the unwanted empty automatic list
328            if ($Conf::Conf{'automatic_list_removal'} =~ /if_empty/i) {
329                Sympa::Spindle::ProcessRequest->new(
330                    context          => $robot,
331                    action           => 'close_list',
332                    current_list     => $list,
333                    mode             => 'purge',
334                    scenario_context => {skip => 1},
335                )->spin;
336            }
337            return undef;
338        }
339        unless ($list->get_total) {
340            $log->syslog('err',
341                'Dynamic list %s from %s family has ZERO subscribers',
342                $list, $dyn_list_family);
343            # As list may be purged, use robot context.
344            Sympa::send_dsn($robot, $message, {}, '4.2.4');
345            $log->db_log(
346                'robot'        => $robot,
347                'list'         => $list->{'name'},
348                'action'       => 'process_message',
349                'parameters'   => "",
350                'target_email' => "",
351                'msg_id'       => $msg_id,
352                'status'       => 'error',
353                'error_type'   => 'list_unknown',
354                'user_email'   => $sender
355            );
356            # purge the unwanted empty automatic list
357            if ($Conf::Conf{'automatic_list_removal'} =~ /if_empty/i) {
358                Sympa::Spindle::ProcessRequest->new(
359                    context          => $robot,
360                    action           => 'close_list',
361                    current_list     => $list,
362                    mode             => 'purge',
363                    scenario_context => {skip => 1},
364                )->spin;
365            }
366            return undef;
367        }
368        $log->syslog('info',
369            'Successfully create list %s with %s subscribers',
370            $list, $list->get_total());
371    }
372
373    # Do not process messages in list creation.  Move them to main spool.
374    my $marshalled =
375        Sympa::Spool::Incoming->new->store($message, original => 1);
376    if ($marshalled) {
377        $log->syslog('notice',
378            'Message %s is stored into incoming spool as <%s>',
379            $message, $marshalled);
380    } else {
381        $log->syslog(
382            'err',
383            'Unable to move in spool for processing message %s to list %s (daemon_usage = creation)',
384            $message,
385            $list
386        );
387        Sympa::send_notify_to_listmaster($list, 'mail_intern_error',
388            {error => '', who => $sender, msg_id => $msg_id,});
389        Sympa::send_dsn($list, $message, {}, '5.3.0');
390        return undef;
391    }
392
393    return 1;
394}
395
3961;
397__END__
398
399=encoding utf-8
400
401=head1 NAME
402
403Sympa::Spindle::ProcessAutomatic - Workflow of automatic list creation
404
405=head1 SYNOPSIS
406
407  use Sympa::Spindle::ProcessAutomatic;
408
409  my $spindle = Sympa::Spindle::ProcessAutomatic->new;
410  $spindle->spin;
411
412=head1 DESCRIPTION
413
414L<Sympa::Spindle::ProcessAutomatic> defines workflow to process messages
415for automatic list creation.
416
417When spin() method is invoked, it reads the messages in automatic spool.
418If the list a message is bound for has not been there and list creation is
419authorized, it will be created.  Then the message is stored into incoming
420message spool again and waits for processing by
421L<Sympa::Spindle::ProcessIncoming>.
422
423Order to process messages in source spool are controlled by modification time
424of files and delivery date.
425Some messages are skipped according to these priorities
426(See L<Sympa::Spool::Automatic>):
427
428=over
429
430=item *
431
432Messages with lowest priority (C<z> or C<Z>) are skipped.
433
434=item *
435
436Messages with possibly higher priority are chosen.
437This is done by skipping messages with lower priority than those already
438found.
439
440=back
441
442=head2 Public methods
443
444See also L<Sympa::Spindle/"Public methods">.
445
446=over
447
448=item new ( [ keepcopy =E<gt> $directory ],
449[ log_level =E<gt> $level ],
450[ log_smtp =E<gt> 0|1 ] )
451
452=item spin ( )
453
454new() may take following options:
455
456=over
457
458=item keepcopy =E<gt> $directory
459
460spin() keeps copy of successfully processed messages in $directory.
461
462=item log_level =E<gt> $level
463
464Overwrites log_level parameter in configuration.
465
466=item log_smtp =E<gt> 0|1
467
468Overwrites log_smtp parameter in configuration.
469
470=back
471
472=back
473
474=head2 Properties
475
476See also L<Sympa::Spindle/"Properties">.
477
478=over
479
480=item {distaff}
481
482Instance of L<Sympa::Spool::Automatic> class.
483
484=back
485
486=head1 SEE ALSO
487
488L<Sympa::Message>,
489L<Sympa::Spindle>, L<Sympa::Spool::Automatic>, L<Sympa::Spool::Incoming>.
490
491=head1 HISTORY
492
493L<Sympa::Spindle::ProcessAutomatic> appeared on Sympa 6.2.10.
494
495=cut
496