1#!--PERL--
2# -*- indent-tabs-mode: nil; -*-
3# vim:ft=perl:et:sw=4
4# $Id$
5
6# Sympa - SYsteme de Multi-Postage Automatique
7#
8# Copyright (c) 1997, 1998, 1999 Institut Pasteur & Christophe Wolfhugel
9# Copyright (c) 1997, 1998, 1999, 2000, 2001, 2002, 2003, 2004, 2005,
10# 2006, 2007, 2008, 2009, 2010, 2011 Comite Reseau des Universites
11# Copyright (c) 2011, 2012, 2013, 2014, 2015, 2016, 2017 GIP RENATER
12# Copyright 2017, 2019 The Sympa Community. See the AUTHORS.md file at
13# the top-level directory of this distribution and at
14# <https://github.com/sympa-community/sympa.git>.
15#
16# This program is free software; you can redistribute it and/or modify
17# it under the terms of the GNU General Public License as published by
18# the Free Software Foundation; either version 2 of the License, or
19# (at your option) any later version.
20#
21# This program is distributed in the hope that it will be useful,
22# but WITHOUT ANY WARRANTY; without even the implied warranty of
23# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
24# GNU General Public License for more details.
25#
26# You should have received a copy of the GNU General Public License
27# along with this program.  If not, see <http://www.gnu.org/licenses/>.
28
29use lib split(/:/, $ENV{SYMPALIB} || ''), '--modulesdir--';
30use strict;
31use warnings;
32use English qw(-no_match_vars);
33use Getopt::Long;
34use Pod::Usage;
35use POSIX qw();
36
37use Conf;
38use Sympa::Constants;
39use Sympa::Crash;    # Show traceback.
40use Sympa::DatabaseManager;
41use Sympa::Language;
42use Sympa::Log;
43use Sympa::Mailer;
44use Sympa::Process;
45use Sympa::Spindle::ProcessDigest;
46use Sympa::Spindle::ProcessIncoming;
47use Sympa::Spool::Listmaster;
48use Sympa::Tools::Data;
49
50my $process = Sympa::Process->instance;
51$process->init(pidname => 'sympa_msg', name => 'sympa/msg');
52
53## Internal tuning
54# delay between each read of the digestqueue
55my $digestsleep = 5;
56
57## Init random engine
58srand(time());
59
60# Check options.
61my %options;
62unless (
63    GetOptions(
64        \%main::options, 'debug|d',  'log_level=s', 'foreground',
65        'config|f=s',    'lang|l=s', 'mail|m',      'keepcopy|k=s',
66        'help|h',        'version|v',
67    )
68) {
69    pod2usage(-exitval => 1, -output => \*STDERR);
70}
71if ($main::options{'help'}) {
72    pod2usage(0);
73} elsif ($main::options{'version'}) {
74    printf "Sympa %s\n", Sympa::Constants::VERSION;
75    exit 0;
76}
77$Conf::sympa_config = $main::options{config};
78
79if ($main::options{'debug'}) {
80    $main::options{'log_level'} = 2 unless $main::options{'log_level'};
81    $main::options{'foreground'} = 1;
82}
83
84my $log = Sympa::Log->instance;
85$log->{log_to_stderr} = 'all' if $main::options{'foreground'};
86
87my $language = Sympa::Language->instance;
88my $mailer   = Sympa::Mailer->instance;
89
90_load();
91
92# Put ourselves in background if we're not in debug mode.
93unless ($main::options{'foreground'}) {
94    $process->daemonize;
95
96    # Fork a new process dedicated to automatic list creation, if required.
97    if ($Conf::Conf{'automatic_list_feature'} eq 'on') {
98        my $child_pid = fork;
99        if ($child_pid) {
100            waitpid $child_pid, 0;
101            $CHILD_ERROR and die;
102        } elsif (not defined $child_pid) {
103            die sprintf 'Cannot fork: %s', $ERRNO;
104        } else {
105            # We're in the specialized child process:
106            # automatic lists creation.
107            exec q{--sbindir--/sympa_automatic.pl}, map {
108                defined $main::options{$_}
109                    ? ("--$_", $main::options{$_})
110                    : ()
111            } qw(config log_level mail);
112            die sprintf 'Cannot exec: %s', $ERRNO;
113        }
114    }
115}
116
117$log->openlog($Conf::Conf{'syslog'}, $Conf::Conf{'log_socket_type'});
118
119# Create and write the PID file.
120$process->write_pid(initial => 1);
121# If process is running in foreground, don't write STDERR to a dedicated file.
122unless ($main::options{foreground}) {
123    $process->direct_stderr_to_file;
124}
125
126# Start multiple processes if required.
127unless ($main::options{'foreground'}) {
128    if (0 == $process->{generation}
129        and ($Conf::Conf{'incoming_max_count'} || 0) > 1) {
130        # Disconnect from database before fork to prevent DB handles
131        # to be shared by different processes.  Sharing database
132        # handles may crash sympa_msg.pl.
133        Sympa::DatabaseManager->disconnect;
134
135        for my $process_count (2 .. $Conf::Conf{'incoming_max_count'}) {
136            my $child_pid = $process->fork;
137            if ($child_pid) {
138                $log->syslog('info', 'Starting child daemon, PID %s',
139                    $child_pid);
140                # Saves the PID number
141                $process->write_pid(pid => $child_pid);
142                #$created_children{$child_pid} = 1;
143                sleep 1;
144            } elsif (not defined $child_pid) {
145                $log->syslog('err', 'Cannot fork: %m');
146                last;
147            } else {
148                # We're in a child process
149                close STDERR;
150                $process->direct_stderr_to_file;
151                $log->openlog($Conf::Conf{'syslog'},
152                    $Conf::Conf{'log_socket_type'});
153                $log->syslog('info', 'Slave daemon started with PID %s',
154                    $PID);
155                last;
156            }
157        }
158
159        # Restore persistent connection.
160        Sympa::DatabaseManager->instance
161            or die 'Reconnecting database failed';
162    }
163}
164
165# Set the User ID & Group ID for the process
166$GID = $EGID = (getgrnam(Sympa::Constants::GROUP))[2];
167$UID = $EUID = (getpwnam(Sympa::Constants::USER))[2];
168
169## Required on FreeBSD to change ALL IDs
170## (effective UID + real UID + saved UID)
171POSIX::setuid((getpwnam(Sympa::Constants::USER))[2]);
172POSIX::setgid((getgrnam(Sympa::Constants::GROUP))[2]);
173
174## Check if the UID has correctly been set (useful on OS X)
175unless (($GID == (getgrnam(Sympa::Constants::GROUP))[2])
176    && ($UID == (getpwnam(Sympa::Constants::USER))[2])) {
177    die
178        "Failed to change process user ID and group ID. Note that on some OS Perl scripts can't change their real UID. In such circumstances Sympa should be run via sudo.\n";
179}
180
181# Sets the UMASK
182umask(oct($Conf::Conf{'umask'}));
183
184## Most initializations have now been done.
185$log->syslog('notice', 'Sympa/msg %s Started', Sympa::Constants::VERSION());
186
187# Check for several files.
188# Prevent that 2 processes perform checks at the same time...
189#FIXME: This would be done in --health_check mode.
190unless (Conf::checkfiles()) {
191    die "Missing files.\n";
192    ## No return.
193}
194
195## Do we have right access in the directory
196if ($main::options{'keepcopy'}) {
197    if (!-d $main::options{'keepcopy'}) {
198        $log->syslog(
199            'notice',
200            'Cannot keep a copy of incoming messages: %s is not a directory',
201            $main::options{'keepcopy'}
202        );
203        delete $main::options{'keepcopy'};
204    } elsif (!-w $main::options{'keepcopy'}) {
205        $log->syslog(
206            'notice',
207            'Cannot keep a copy of incoming messages: no write access to %s',
208            $main::options{'keepcopy'}
209        );
210        delete $main::options{'keepcopy'};
211    }
212}
213
214my $spindle = Sympa::Spindle::ProcessIncoming->new(
215    keepcopy  => $main::options{keepcopy},
216    lang      => $main::options{lang},
217    log_level => $main::options{log_level},
218    log_smtp  => $main::options{mail},
219    #FIXME: Is it required?
220    debug_virus_check => $main::options{debug},
221);
222
223# Catch signals, in order to exit cleanly, whenever possible.
224$SIG{'TERM'} = 'sigterm';
225$SIG{'INT'}  = 'sigterm';    # Interrupt from terminal.
226$SIG{'HUP'}  = 'sighup';
227$SIG{'PIPE'} = 'IGNORE';     # Ignore SIGPIPE ; prevents process from dying
228
229# Main loop.
230# This loop is run foreach HUP signal received.
231
232my $index_queuedigest = 0;   # verify the digest queue
233
234while (not $spindle->{finish} or $spindle->{finish} ne 'term') {
235    # Process digest only in master process ({generation} is 0).
236    # Scan queuedigest.
237    if (0 == $process->{generation}
238        and $index_queuedigest++ >= $digestsleep) {
239        $index_queuedigest = 0;
240        Sympa::Spindle::ProcessDigest->new->spin;
241    }
242
243    $spindle->spin;
244
245    if ($spindle->{finish} and $spindle->{finish} eq 'hup') {
246        # Disconnect from Database
247        Sympa::DatabaseManager->disconnect;
248
249        $log->syslog('notice', 'Sympa %s reload config',
250            Sympa::Constants::VERSION);
251        _load();
252        $spindle = Sympa::Spindle::ProcessIncoming->new(
253            keepcopy  => $main::options{keepcopy},
254            lang      => $main::options{lang},
255            log_level => $main::options{log_level},
256            log_smtp  => $main::options{mail},
257            #FIXME: Is it required?
258            debug_virus_check => $main::options{debug},
259        );
260        next;
261    } elsif ($spindle->{finish}) {
262        last;
263    }
264
265    # Sleep for a while if spool is empty.
266    sleep $Conf::Conf{'sleep'};
267}
268
269# Purge grouped notifications
270Sympa::Spool::Listmaster->instance->flush(purge => 1);
271
272$log->syslog('notice', 'Sympa/msg exited normally due to signal');
273$process->remove_pid;
274
275exit(0);
276
277# Load configuration.
278sub _load {
279    ## Load sympa.conf.
280    unless (Conf::load(Conf::get_sympa_conf(), 'no_db')) {    #Site and Robot
281        die sprintf
282            "Unable to load sympa configuration, file %s or one of the vhost robot.conf files contain errors. Exiting.\n",
283            Conf::get_sympa_conf();
284    }
285
286    ## Open the syslog and say we're read out stuff.
287    $log->openlog($Conf::Conf{'syslog'}, $Conf::Conf{'log_socket_type'});
288
289    # Enable SMTP logging if required
290    $mailer->{log_smtp} = $main::options{'mail'}
291        || Sympa::Tools::Data::smart_eq($Conf::Conf{'log_smtp'}, 'on');
292
293    # setting log_level using conf unless it is set by calling option
294    if (defined $main::options{'log_level'}) {
295        $log->{level} = $main::options{'log_level'};
296        $log->syslog(
297            'info',
298            'Configuration file read, log level set using options: %s',
299            $main::options{'log_level'}
300        );
301    } else {
302        $log->{level} = $Conf::Conf{'log_level'};
303        $log->syslog(
304            'info',
305            'Configuration file read, default log level %s',
306            $Conf::Conf{'log_level'}
307        );
308    }
309
310    # Check database connectivity.
311    unless (Sympa::DatabaseManager->instance) {
312        die sprintf
313            "Database %s defined in sympa.conf is unreachable. verify db_xxx parameters in sympa.conf\n",
314            $Conf::Conf{'db_name'};
315    }
316
317    # Now trying to load full config (including database)
318    unless (Conf::load()) {    #FIXME: load Site, then robot cache
319        die sprintf
320            "Unable to load Sympa configuration, file %s or any of the virtual host robot.conf files contain errors. Exiting.\n",
321            Conf::get_sympa_conf();
322    }
323
324    ## Set locale configuration
325    ## Compatibility with version < 2.3.3
326    $main::options{'lang'} =~ s/\.cat$//
327        if defined $main::options{'lang'};
328    $language->set_lang($main::options{'lang'}, $Conf::Conf{'lang'}, 'en');
329
330    ## Main program
331    if (!chdir($Conf::Conf{'home'})) {
332        die sprintf 'Can\'t chdir to %s: %s', $Conf::Conf{'home'}, $ERRNO;
333        ## Function never returns.
334    }
335
336    ## Check for several files.
337    unless (Conf::checkfiles_as_root()) {
338        die "Missing files\n";
339    }
340}
341
342############################################################
343# sigterm
344############################################################
345#  When we catch signal, just changes the value of the $signal
346#  loop variable.
347#
348# IN : -
349#
350# OUT : -
351#
352############################################################
353sub sigterm {
354    my ($sig) = @_;
355    $log->syslog('notice',
356        'Signal %s received, still processing current task', $sig);
357    $spindle->{finish} = 'term';
358}
359
360############################################################
361# sighup
362############################################################
363#  When we catch SIGHUP, changes the value of the $signal
364#  loop variable and puts the "-mail" logging option
365#
366# IN : -
367#
368# OUT : -
369#
370###########################################################
371sub sighup {
372    if ($mailer->{log_smtp}) {
373        $log->syslog('notice',
374            'signal HUP received, switch of the "-mail" logging option and continue current task'
375        );
376        $mailer->{log_smtp} = undef;
377    } else {
378        $log->syslog('notice',
379            'signal HUP received, switch on the "-mail" logging option and continue current task'
380        );
381        $mailer->{log_smtp} = 1;
382    }
383    $spindle->{finish} = 'hup';
384}
385
386# Moved to Sympa::Spindle::ProcessIncoming::_twist().
387#sub process_message;
388
389#sub DoSendMessage($message);
390#DEPRECATED: Run upgrade_send_spool.pl to migrate message with old format.
391
392# Moved to Sympa::Spindle::DoForward::_twist().
393#sub DoForward;
394
395# Moved (divided) to Sympa::Spindle::DoMessage::_twist() &
396# Sympa::Spindle::AuthorizeMessage::_twist().
397#sub DoMessage;
398
399# Old name: tools::checkcommand().
400# Moved to Sympa::Spindle::DoMessage::_check_command().
401#sub _check_command;
402
403# Moved to Sympa::Spindle::DoCommand::_twist().
404#sub DoCommand;
405
406# DEPRECATED.  Use Sympa::Spindle::ProcessDigest class.
407#sub SendDigest;
408
409# Moved to Sympa::Spindle::ProcessIncoming::_clean_msgid_table().
410#sub clean_msgid_table;
411
412__END__
413
414=encoding utf-8
415
416=head1 NAME
417
418sympa_msg, sympa_msg.pl - Daemon to handle incoming messages
419
420=head1 SYNOPSIS
421
422C<sympa_msg.pl> S<[ C<-d>, C<--debug> ]>
423S<[ C<-f>, C<--file>=I<another.sympa.conf> ]>
424S<[ C<-k>, C<--keepcopy>=I<directory> ]>
425S<[ C<-l>, C<--lang>=I<lang> ]> S<[ C<-m>, C<--mail> ]>
426S<[ C<-h>, C<--help> ]> S<[ C<-v>, C<--version> ]>
427
428=head1 DESCRIPTION
429
430Sympa_msg.pl is a program which scans permanently the incoming message spool
431and processes each message.
432
433Messages bound for the lists and authorized sending are modified as necessity
434and at last stored into digest spool, archive spool and outgoing spool.
435Those bound for command addresses are interpreted and appropriate actions are
436taken.
437Those bound for listmasters or list admins are forwarded to them.
438
439=head1 OPTIONS
440
441Sympa_msg.pl follows the usual GNU command line syntax,
442with long options starting with two dashes (C<-->).  A summary of
443options is included below.
444
445=over 4
446
447=item C<-d>, C<--debug>
448
449Enable debug mode.
450
451=item C<-f>, C<--config=>I<file>
452
453Force Sympa to use an alternative configuration file instead
454of F<--CONFIG-->.
455
456=item C<-l>, C<--lang=>I<lang>
457
458Set this option to use a language for Sympa. The corresponding
459gettext catalog file must be located in F<$LOCALEDIR>
460directory.
461
462=item C<--log_level=>I<level>
463
464Sets Sympa log level.
465
466=back
467
468F<sympa_msg.pl> may run in daemon mode with following options.
469
470=over 4
471
472=item C<--foreground>
473
474The process remains attached to the TTY.
475
476=item C<-k>, C<--keepcopy=>F<directory>
477
478This option tells Sympa to keep a copy of every incoming message,
479instead of deleting them. `directory' is the directory to
480store messages.
481
482=item C<-m>, C<--mail>
483
484Sympa will log calls to sendmail, including recipients. This option is
485useful for keeping track of each mail sent (log files may grow faster
486though).
487
488=item C<--service=>I<service>
489
490B<Note>:
491This option was deprecated.
492
493Process is dedicated to messages distribution (C<process_message>),
494commands (C<process_command>) or to automatic lists
495creation (C<process_creation>, default three of them).
496
497=back
498
499With following options F<sympa_msg.pl> will print some information and exit.
500
501=over 4
502
503=item C<-h>, C<--help>
504
505Print this help message.
506
507=item C<-v>, C<--version>
508
509Print the version number.
510
511=back
512
513=head1 FILES
514
515F<--CONFIG--> main configuration file.
516
517F<$PIDDIR/sympa_msg.pid> this file contains the process ID
518of F<sympa_msg.pl>.
519
520=head1 SEE ALSO
521
522L<sympa.conf(5)>, L<sympa(1)>.
523
524L<archived(8)>, L<bulk(8)>, L<bounced(8)>, L<sympa_automatic(8)>,
525L<task_manager(8)>.
526
527L<Sympa::Spindle::ProcessDigest>,
528L<Sympa::Spindle::ProcessIncoming>.
529
530=head1 HISTORY
531
532F<sympa.pl> was originally written by:
533
534=over 4
535
536=item Serge Aumont
537
538ComitE<233> RE<233>seau des UniversitE<233>s
539
540=item Olivier SalaE<252>n
541
542ComitE<233> RE<233>seau des UniversitE<233>s
543
544=back
545
546As of Sympa 6.2b.4, it was split into three programs:
547F<sympa.pl> command line utility, F<sympa_automatic.pl> daemon and
548F<sympa_msg.pl> daemon.
549
550=cut
551