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, 2018, 2019, 2020 The Sympa Community. See the AUTHORS.md
13# file at 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 Digest::MD5;
33use English qw(-no_match_vars);
34use Fcntl qw();
35use File::Basename qw();
36use File::Copy qw();
37use File::Path qw();
38use Getopt::Long;
39use Pod::Usage;
40use POSIX qw();
41
42use Sympa;
43use Sympa::Aliases;
44use Conf;
45use Sympa::Constants;
46use Sympa::DatabaseManager;
47use Sympa::Family;
48use Sympa::Language;
49use Sympa::List;
50use Sympa::Log;
51use Sympa::Mailer;
52use Sympa::Message;
53use Sympa::Spindle::ProcessDigest;
54use Sympa::Spindle::ProcessRequest;
55use Sympa::Spool::Archive;
56use Sympa::Template;
57use Sympa::Tools::Data;
58use Sympa::Upgrade;
59
60## Init random engine
61srand(time());
62
63# Check options.
64my %options;
65unless (
66    GetOptions(
67        \%main::options,                'dump=s',
68        'debug|d',                      'log_level=s',
69        'config|f=s',                   'lang|l=s',
70        'mail|m',                       'help|h',
71        'version|v',                    'import=s',
72        'make_alias_file',              'lowercase',
73        'sync_list_db',                 'md5_encode_password',
74        'close_list=s',                 'rename_list=s',
75        'copy_list=s',                  'new_listname=s',
76        'new_listrobot=s',              'purge_list=s',
77        'create_list',                  'instantiate_family=s',
78        'robot=s',                      'add_list=s',
79        'modify_list=s',                'close_family=s',
80        'md5_digest=s',                 'change_user_email',
81        'current_email=s',              'new_email=s',
82        'input_file=s',                 'sync_include=s',
83        'upgrade',                      'upgrade_shared',
84        'from=s',                       'to=s',
85        'reload_list_config',           'list=s',
86        'quiet',                        'close_unknown',
87        'test_database_message_buffer', 'conf_2_db',
88        'export_list',                  'health_check',
89        'send_digest',                  'keep_digest',
90        'upgrade_config_location',      'role=s',
91        'dump_users',                   'restore_users',
92        'open_list=s',                  'show_pending_lists=s',
93        'notify',                       'rebuildarc=s'
94    )
95) {
96    pod2usage(-exitval => 1, -output => \*STDERR);
97}
98if ($main::options{'help'}) {
99    pod2usage(0);
100} elsif ($main::options{'version'}) {
101    printf "Sympa %s\n", Sympa::Constants::VERSION;
102    exit 0;
103}
104$Conf::sympa_config = $main::options{config};
105
106if ($main::options{'debug'}) {
107    $main::options{'log_level'} = 2 unless $main::options{'log_level'};
108}
109
110my $log = Sympa::Log->instance;
111$log->{log_to_stderr} = 'notice,err'
112    if $main::options{'upgrade'}
113    || $main::options{'reload_list_config'}
114    || $main::options{'test_database_message_buffer'}
115    || $main::options{'conf_2_db'};
116
117if ($main::options{'upgrade_config_location'}) {
118    my $config_file = Conf::get_sympa_conf();
119
120    if (-f $config_file) {
121        printf "Sympa configuration already located at %s\n", $config_file;
122        exit 0;
123    }
124
125    my ($file, $dir, $suffix) = File::Basename::fileparse($config_file);
126    my $old_dir = $dir;
127    $old_dir =~ s/sympa\///;
128
129    # Try to create config path if it does not exist
130    unless (-d $dir) {
131        my $error;
132        File::Path::make_path(
133            $dir,
134            {   mode  => 0755,
135                owner => Sympa::Constants::USER(),
136                group => Sympa::Constants::GROUP(),
137                error => \$error
138            }
139        );
140        if (@$error) {
141            my $diag = pop @$error;
142            my ($target, $error) = %$diag;
143            die "Unable to create $target: $error";
144        }
145    }
146
147    # Check ownership of config folder
148    my @stat = stat($dir);
149    my $user = (getpwuid $stat[4])[0];
150    if ($user ne Sympa::Constants::USER()) {
151        die sprintf
152            "Config dir %s exists but is not owned by %s (owned by %s).\n",
153            $dir, Sympa::Constants::USER(), $user;
154    }
155
156    # Check permissions on config folder
157    if (($stat[2] & Fcntl::S_IRWXU()) != Fcntl::S_IRWXU()) {
158        die
159            "Config dir $dir exists, but sympa does not have rwx permissions on it";
160    }
161
162    # Move files from old location to new one
163    opendir(my $dh, $old_dir) or die("Could not open $dir for reading");
164    my @files = grep(/^(ww)?sympa\.conf.*$/, readdir($dh));
165    closedir($dh);
166
167    foreach my $file (@files) {
168        unless (File::Copy::move("$old_dir/$file", "$dir/$file")) {
169            die sprintf 'Could not move %s/%s to %s/%s: %s', $old_dir, $file,
170                $dir, $file, $ERRNO;
171        }
172    }
173
174    printf "Sympa configuration moved to %s\n", $dir;
175    exit 0;
176} elsif ($main::options{'health_check'}) {
177    ## Health check
178
179    ## Load configuration file. Ignoring database config for now: it avoids
180    ## trying to load a database that could not exist yet.
181    unless (Conf::load(Conf::get_sympa_conf(), 'no_db')) {
182        #FIXME: force reload
183        die sprintf
184            "Configuration file %s has errors.\n",
185            Conf::get_sympa_conf();
186    }
187
188    ## Open the syslog and say we're read out stuff.
189    $log->openlog(
190        $Conf::Conf{'syslog'},
191        $Conf::Conf{'log_socket_type'},
192        service => 'sympa/health_check'
193    );
194
195    ## Setting log_level using conf unless it is set by calling option
196    if ($main::options{'log_level'}) {
197        $log->{level} = $main::options{'log_level'};
198        $log->syslog(
199            'info',
200            'Configuration file read, log level set using options: %s',
201            $main::options{'log_level'}
202        );
203    } else {
204        $log->{level} = $Conf::Conf{'log_level'};
205        $log->syslog(
206            'info',
207            'Configuration file read, default log level %s',
208            $Conf::Conf{'log_level'}
209        );
210    }
211
212    ## Check if db_type is not the boilerplate one
213    if ($Conf::Conf{'db_type'} eq '(You must define this parameter)') {
214        die sprintf
215            "Database type \"%s\" defined in sympa.conf is the boilerplate one and obviously incorrect. Verify db_xxx parameters in sympa.conf\n",
216            $Conf::Conf{'db_type'};
217    }
218
219    ## Preliminary check of db_type
220    unless ($Conf::Conf{'db_type'} and $Conf::Conf{'db_type'} =~ /\A\w+\z/) {
221        die sprintf
222            "Database type \"%s\" defined in sympa.conf seems incorrect. Verify db_xxx parameters in sympa.conf\n",
223            $Conf::Conf{'db_type'};
224    }
225
226    ## Check database connectivity and probe database
227    unless (Sympa::DatabaseManager::probe_db()) {
228        die sprintf
229            "Database %s defined in sympa.conf has not the right structure or is unreachable. Verify db_xxx parameters in sympa.conf\n",
230            $Conf::Conf{'db_name'};
231    }
232
233    ## Now trying to load full config (including database)
234    unless (Conf::load()) {    #FIXME: load Site, then robot cache
235        die sprintf
236            "Unable to load Sympa configuration, file %s or any of the virtual host robot.conf files contain errors. Exiting.\n",
237            Conf::get_sympa_conf();
238    }
239
240    ## Change working directory.
241    if (!chdir($Conf::Conf{'home'})) {
242        printf STDERR "Can't chdir to %s: %s\n", $Conf::Conf{'home'}, $ERRNO;
243        exit 1;
244    }
245
246    ## Check for several files.
247    unless (Conf::checkfiles_as_root()) {
248        printf STDERR "Missing files.\n";
249        exit 1;
250    }
251
252    ## Check that the data structure is uptodate
253    unless (Conf::data_structure_uptodate()) {
254        printf STDOUT
255            "Data structure was not updated; you should run sympa.pl --upgrade to run the upgrade process.\n";
256    }
257
258    exit 0;
259}
260
261my $default_lang;
262
263my $language = Sympa::Language->instance;
264my $mailer   = Sympa::Mailer->instance;
265
266_load();
267
268$log->openlog($Conf::Conf{'syslog'}, $Conf::Conf{'log_socket_type'});
269
270# Set the User ID & Group ID for the process
271$GID = $EGID = (getgrnam(Sympa::Constants::GROUP))[2];
272$UID = $EUID = (getpwnam(Sympa::Constants::USER))[2];
273
274## Required on FreeBSD to change ALL IDs
275## (effective UID + real UID + saved UID)
276POSIX::setuid((getpwnam(Sympa::Constants::USER))[2]);
277POSIX::setgid((getgrnam(Sympa::Constants::GROUP))[2]);
278
279## Check if the UID has correctly been set (useful on OS X)
280unless (($GID == (getgrnam(Sympa::Constants::GROUP))[2])
281    && ($UID == (getpwnam(Sympa::Constants::USER))[2])) {
282    die
283        "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";
284}
285
286# Sets the UMASK
287umask(oct($Conf::Conf{'umask'}));
288
289## Most initializations have now been done.
290$log->syslog('notice', 'Sympa %s Started', Sympa::Constants::VERSION());
291
292# Check for several files.
293#FIXME: This would be done in --health_check mode.
294unless (Conf::checkfiles()) {
295    die "Missing files.\n";
296    ## No return.
297}
298
299# Daemon called for dumping subscribers list
300if ($main::options{'dump'} or $main::options{'dump_users'}) {
301    my $all_lists;
302
303    # Compat. for old style "--dump=LIST".
304    my $list_id = $main::options{'dump'} || $main::options{'list'};
305
306    if (defined $list_id and $list_id eq 'ALL') {
307        $all_lists =
308            Sympa::List::get_lists('*', filter => [status => 'open']);
309    } elsif (defined $list_id and length $list_id) {
310        # The parameter is list ID and list have to be open.
311        unless (0 < index $list_id, '@') {
312            $log->syslog('err', 'Incorrect list address %s', $list_id);
313            exit 1;
314        }
315        my $list = Sympa::List->new($list_id);
316        unless (defined $list) {
317            $log->syslog('err', 'Unknown list %s', $list_id);
318            exit 1;
319        }
320        unless ($list->{'admin'}{'status'} eq 'open') {
321            $log->syslog('err', 'List is not open: %s', $list);
322            exit 1;
323        }
324
325        $all_lists = [$list];
326    } else {
327        $log->syslog('err', 'No lists specified');
328        exit 1;
329    }
330
331    my @roles = qw(member);
332    if ($main::options{'role'}) {
333        my %roles = map { ($_ => 1) }
334            ($main::options{'role'} =~ /\b(member|owner|editor)\b/g);
335        @roles = sort keys %roles;
336        unless (@roles) {
337            $log->syslog('err', 'Unknown role %s', $main::options{'role'});
338            exit 1;
339        }
340    }
341
342    foreach my $list (@$all_lists) {
343        foreach my $role (@roles) {
344            unless ($list->dump_users($role)) {
345                printf STDERR "%s: Could not dump list users (%s)\n",
346                    $list->get_id, $role;
347            } else {
348                printf STDERR "%s: Dumped list users (%s)\n",
349                    $list->get_id, $role;
350            }
351        }
352    }
353
354    exit 0;
355} elsif ($main::options{'restore_users'}) {
356    my $all_lists;
357
358    my $list_id = $main::options{'list'};
359
360    if (defined $list_id and $list_id eq 'ALL') {
361        $all_lists =
362            Sympa::List::get_lists('*', filter => [status => 'open']);
363    } elsif (defined $list_id and length $list_id) {
364        # The parameter is list ID and list have to be open.
365        unless (0 < index $list_id, '@') {
366            $log->syslog('err', 'Incorrect list address %s', $list_id);
367            exit 1;
368        }
369        my $list = Sympa::List->new($list_id);
370        unless (defined $list) {
371            $log->syslog('err', 'Unknown list %s', $list_id);
372            exit 1;
373        }
374        unless ($list->{'admin'}{'status'} eq 'open') {
375            $log->syslog('err', 'List is not open: %s', $list);
376            exit 1;
377        }
378
379        $all_lists = [$list];
380    } else {
381        $log->syslog('err', 'No lists specified');
382        exit 1;
383    }
384
385    my @roles = qw(member);
386    if ($main::options{'role'}) {
387        my %roles = map { ($_ => 1) }
388            ($main::options{'role'} =~ /\b(member|owner|editor)\b/g);
389        @roles = sort keys %roles;
390        unless (@roles) {
391            $log->syslog('err', 'Unknown role %s', $main::options{'role'});
392            exit 1;
393        }
394    }
395
396    foreach my $list (@$all_lists) {
397        foreach my $role (@roles) {
398            unless ($list->restore_users($role)) {
399                printf STDERR "%s: Could not restore list users (%s)\n",
400                    $list->get_id, $role;
401            } else {
402                printf STDERR "%s: Restored list users (%s)\n",
403                    $list->get_id, $role;
404            }
405        }
406    }
407
408    exit 0;
409} elsif ($main::options{'make_alias_file'}) {
410    my $robots = $main::options{'robot'} || '*';
411    my @robots;
412    if ($robots eq '*') {
413        @robots = Sympa::List::get_robots();
414    } else {
415        for my $name (split /[\s,]+/, $robots) {
416            next unless length($name);
417            if (Conf::valid_robot($name)) {
418                push @robots, $name;
419            } else {
420                printf STDERR "Invalid robot %s\n", $name;
421            }
422        }
423    }
424    exit 0 unless @robots;
425
426    # There may be multiple aliases files.  Give each of them suffixed
427    # name.
428    my ($basename, %robots_of, %sympa_aliases);
429    $basename = sprintf '%s/sympa_aliases.%s', $Conf::Conf{'tmpdir'}, $PID;
430
431    foreach my $robot (@robots) {
432        my $file = Conf::get_robot_conf($robot, 'sendmail_aliases');
433        next if $file eq 'none';
434
435        $robots_of{$file} ||= [];
436        push @{$robots_of{$file}}, $robot;
437    }
438    if (1 < scalar(keys %robots_of)) {
439        my $i = 0;
440        %sympa_aliases = map {
441            $i++;
442            map { $_ => sprintf('%s.%03d', $basename, $i) } @{$robots_of{$_}}
443        } sort keys %robots_of;
444    } else {
445        %sympa_aliases = map { $_ => $basename } @robots;
446    }
447
448    # Create files.
449    foreach my $sympa_aliases (values %sympa_aliases) {
450        my $fh;
451        unless (open $fh, '>', $sympa_aliases) {    # truncate if exists
452            printf STDERR "Unable to create %s: %s\n", $sympa_aliases, $ERRNO;
453            exit 1;
454        }
455        close $fh;
456    }
457
458    # Write files.
459    foreach my $robot (sort @robots) {
460        my $alias_manager = Conf::get_robot_conf($robot, 'alias_manager');
461        my $sympa_aliases = $sympa_aliases{$robot};
462
463        my $aliases =
464            Sympa::Aliases->new($alias_manager, file => $sympa_aliases);
465        next
466            unless $aliases and $aliases->isa('Sympa::Aliases::Template');
467
468        my $fh;
469        unless (open $fh, '>>', $sympa_aliases) {    # append
470            printf STDERR "Unable to create %s: %s\n", $sympa_aliases, $ERRNO;
471            exit 1;
472        }
473        printf $fh "#\n#\tAliases for all Sympa lists open on %s\n#\n",
474            $robot;
475        close $fh;
476
477        my $all_lists = Sympa::List::get_lists($robot);
478        foreach my $list (@{$all_lists || []}) {
479            next unless $list->{'admin'}{'status'} eq 'open';
480
481            $aliases->add($list);
482        }
483    }
484
485    if (1 < scalar(keys %robots_of)) {
486        printf
487            "Sympa aliases files %s.??? were made.  You probably need to install them in your SMTP engine.\n",
488            $basename;
489    } else {
490        printf
491            "Sympa aliases file %s was made.  You probably need to install it in your SMTP engine.\n",
492            $basename;
493    }
494    exit 0;
495} elsif ($main::options{'md5_digest'}) {
496    my $md5 = Digest::MD5::md5_hex($main::options{'md5_digest'});
497    printf "md5 digest : %s \n", $md5;
498
499    exit 0;
500} elsif ($main::options{'import'}) {
501    #FIXME The parameter should be a list address.
502    unless ($main::options{'import'} =~ /\@/) {
503        printf STDERR "Incorrect list address %s\n", $main::options{'import'};
504        exit 1;
505    }
506    my $list;
507    unless ($list = Sympa::List->new($main::options{'import'})) {
508        printf STDERR "Unknown list name %s\n", $main::options{'import'};
509        exit 1;
510    }
511    my $dump = do { local $RS; <STDIN> };
512
513    my $spindle = Sympa::Spindle::ProcessRequest->new(
514        context          => $list,
515        action           => 'import',
516        dump             => $dump,
517        force            => 1,
518        sender           => Sympa::get_address($list, 'listmaster'),
519        scenario_context => {skip => 1},
520        quiet            => $main::options{quiet},
521    );
522    unless ($spindle and $spindle->spin) {
523        printf STDERR "Failed to add email addresses to %s\n", $list;
524        exit 1;
525    }
526    my $status = _report($spindle);
527    printf STDERR "Total imported subscribers: %d\n",
528        scalar(grep { $_->[1] eq 'notice' and $_->[2] eq 'now_subscriber' }
529            @{$spindle->{stash} || []});
530    exit($status ? 0 : 1);
531
532} elsif ($main::options{'md5_encode_password'}) {
533    print STDERR "Obsoleted.  Use upgrade_sympa_password.pl.\n";
534
535    exit 0;
536} elsif ($main::options{'lowercase'}) {
537    print STDERR "Working on user_table...\n";
538    my $total = _lowercase_field('user_table', 'email_user');
539
540    if (defined $total) {
541        print STDERR "Working on subscriber_table...\n";
542        my $total_sub =
543            _lowercase_field('subscriber_table', 'user_subscriber');
544        if (defined $total_sub) {
545            $total += $total_sub;
546        }
547    }
548
549    unless (defined $total) {
550        print STDERR "Could not work on dabatase.\n";
551        exit 1;
552    }
553
554    printf STDERR "Total lowercased rows: %d\n", $total;
555
556    exit 0;
557} elsif ($main::options{'close_list'}) {
558    my ($listname, $robot_id) = split /\@/, $main::options{'close_list'}, 2;
559    my $current_list = Sympa::List->new($listname, $robot_id);
560    unless ($current_list) {
561        printf STDERR "Incorrect list name %s.\n",
562            $main::options{'close_list'};
563        exit 1;
564    }
565
566    my $spindle = Sympa::Spindle::ProcessRequest->new(
567        context          => $robot_id,
568        action           => 'close_list',
569        current_list     => $current_list,
570        sender           => Sympa::get_address($robot_id, 'listmaster'),
571        scenario_context => {skip => 1},
572    );
573    unless ($spindle and $spindle->spin and _report($spindle)) {
574        printf STDERR "Could not close list %s\n", $current_list->get_id;
575        exit 1;
576    }
577    exit 0;
578
579} elsif ($main::options{'change_user_email'}) {
580    unless ($main::options{'current_email'} and $main::options{'new_email'}) {
581        print STDERR "Missing current_email or new_email parameter\n";
582        exit 1;
583    }
584
585    my $spindle = Sympa::Spindle::ProcessRequest->new(
586        context          => [Sympa::List::get_robots()],
587        action           => 'move_user',
588        current_email    => $main::options{'current_email'},
589        email            => $main::options{'new_email'},
590        sender           => Sympa::get_address('*', 'listmaster'),
591        scenario_context => {skip => 1},
592    );
593    unless ($spindle and $spindle->spin and _report($spindle)) {
594        printf STDERR "Failed to change user email address %s to %s\n",
595            $main::options{'current_email'}, $main::options{'new_email'};
596        exit 1;
597    }
598    exit 0;
599
600} elsif ($main::options{'purge_list'}) {
601    my ($listname, $robot_id) = split /\@/, $main::options{'purge_list'}, 2;
602    my $current_list = Sympa::List->new($listname, $robot_id);
603    unless ($current_list) {
604        printf STDERR "Incorrect list name %s\n",
605            $main::options{'purge_list'};
606        exit 1;
607    }
608
609    my $spindle = Sympa::Spindle::ProcessRequest->new(
610        context          => $robot_id,
611        action           => 'close_list',
612        current_list     => $current_list,
613        mode             => 'purge',
614        scenario_context => {skip => 1},
615    );
616    unless ($spindle and $spindle->spin and _report($spindle)) {
617        printf STDERR "Could not purge list %s\n", $current_list->get_id;
618        exit 1;
619    }
620    exit 0;
621
622} elsif ($main::options{'rename_list'}) {
623    my $current_list =
624        Sympa::List->new(split(/\@/, $main::options{'rename_list'}, 2),
625        {just_try => 1});
626    unless ($current_list) {
627        printf STDERR "Incorrect list name %s\n",
628            $main::options{'rename_list'};
629        exit 1;
630    }
631
632    my $listname = $main::options{'new_listname'};
633    unless (defined $listname and length $listname) {
634        print STDERR "Missing parameter new_listname\n";
635        exit 1;
636    }
637
638    my $robot_id = $main::options{'new_listrobot'};
639    unless (defined $robot_id) {
640        $robot_id = $current_list->{'domain'};
641    } else {
642        unless (length $robot_id and Conf::valid_robot($robot_id)) {
643            printf STDERR "Unknown robot \"%s\"\n", $robot_id;
644            exit 1;
645        }
646    }
647
648    my $spindle = Sympa::Spindle::ProcessRequest->new(
649        context          => $robot_id,
650        action           => 'move_list',
651        current_list     => $current_list,
652        listname         => $listname,
653        sender           => Sympa::get_address($robot_id, 'listmaster'),
654        scenario_context => {skip => 1},
655    );
656    unless ($spindle and $spindle->spin and _report($spindle)) {
657        printf STDERR "Could not rename list %s to %s\@%s\n",
658            $current_list->get_id, $listname, $robot_id;
659        exit 1;
660    }
661    exit 0;
662
663} elsif ($main::options{'copy_list'}) {
664    my $current_list =
665        Sympa::List->new(split(/\@/, $main::options{'copy_list'}, 2),
666        {just_try => 1});
667    unless ($current_list) {
668        printf STDERR "Incorrect list name %s\n", $main::options{'copy_list'};
669        exit 1;
670    }
671
672    my $listname = $main::options{'new_listname'};
673    unless (defined $listname and length $listname) {
674        print STDERR "Missing parameter new_listname\n";
675        exit 1;
676    }
677
678    my $robot_id = $main::options{'new_listrobot'};
679    unless (defined $robot_id) {
680        $robot_id = $current_list->{'domain'};
681    } else {
682        unless (length $robot_id and Conf::valid_robot($robot_id)) {
683            printf STDERR "Unknown robot \"%s\"\n", $robot_id;
684            exit 1;
685        }
686    }
687
688    my $spindle = Sympa::Spindle::ProcessRequest->new(
689        context          => $robot_id,
690        action           => 'move_list',
691        current_list     => $current_list,
692        listname         => $listname,
693        mode             => 'copy',
694        sender           => Sympa::get_address($robot_id, 'listmaster'),
695        scenario_context => {skip => 1},
696    );
697    unless ($spindle and $spindle->spin and _report($spindle)) {
698        printf STDERR "Could not copy list %s to %s\@%s\n",
699            $current_list->get_id, $listname, $robot_id;
700        exit 1;
701    }
702    exit 0;
703
704} elsif ($main::options{'test_database_message_buffer'}) {
705    print
706        "Deprecated.  Size of messages no longer limited by database packet size.\n";
707    exit 1;
708} elsif ($main::options{'conf_2_db'}) {
709
710    printf
711        "Sympa is going to store %s in database conf_table. This operation do NOT remove original files\n",
712        Conf::get_sympa_conf();
713    if (Conf::conf_2_db()) {
714        printf "Done";
715    } else {
716        printf "an error occur";
717    }
718    exit 1;
719
720} elsif ($main::options{'create_list'}) {
721    my $robot = $main::options{'robot'} || $Conf::Conf{'domain'};
722
723    unless ($main::options{'input_file'}) {
724        print STDERR "Error : missing 'input_file' parameter\n";
725        exit 1;
726    }
727
728    my $spindle = Sympa::Spindle::ProcessRequest->new(
729        context          => $robot,
730        action           => 'create_list',
731        parameters       => {file => $main::options{'input_file'}},
732        sender           => Sympa::get_address($robot, 'listmaster'),
733        scenario_context => {skip => 1}
734    );
735    unless ($spindle and $spindle->spin and _report($spindle)) {
736        print STDERR "Could not create list\n";
737        exit 1;
738    }
739    exit 0;
740
741} elsif ($main::options{'instantiate_family'}) {
742    my $robot = $main::options{'robot'} || $Conf::Conf{'domain'};
743
744    my $family_name;
745    unless ($family_name = $main::options{'instantiate_family'}) {
746        print STDERR "Error : missing family parameter\n";
747        exit 1;
748    }
749    my $family;
750    unless ($family = Sympa::Family->new($family_name, $robot)) {
751        printf STDERR
752            "The family %s does not exist, impossible instantiation\n",
753            $family_name;
754        exit 1;
755    }
756
757    unless ($main::options{'input_file'}) {
758        print STDERR "Error : missing input_file parameter\n";
759        exit 1;
760    }
761
762    unless (-r $main::options{'input_file'}) {
763        printf STDERR "Unable to read %s file\n",
764            $main::options{'input_file'};
765        exit 1;
766    }
767
768    unless (
769        instantiate(
770            $family,
771            $main::options{'input_file'},
772            close_unknown => $main::options{'close_unknown'},
773            quiet         => $main::options{quiet},
774        )
775    ) {
776        print STDERR "\nImpossible family instantiation : action stopped \n";
777        exit 1;
778    }
779
780    my %result;
781    my $err = get_instantiation_results($family, \%result);
782
783    unless ($main::options{'quiet'}) {
784        print STDOUT "@{$result{'info'}}";
785        print STDOUT "@{$result{'warn'}}";
786    }
787    if ($err >= 0) {
788        print STDERR "@{$result{'errors'}}";
789        exit 1;
790    }
791
792    exit 0;
793} elsif ($main::options{'add_list'}) {
794    my $robot = $main::options{'robot'} || $Conf::Conf{'domain'};
795
796    my $family_name;
797    unless ($family_name = $main::options{'add_list'}) {
798        print STDERR "Error : missing family parameter\n";
799        exit 1;
800    }
801
802    my $family;
803    unless ($family = Sympa::Family->new($family_name, $robot)) {
804        printf STDERR
805            "The family %s does not exist, impossible to add a list\n",
806            $family_name;
807        exit 1;
808    }
809
810    unless ($main::options{'input_file'}) {
811        print STDERR "Error : missing 'input_file' parameter\n";
812        exit 1;
813    }
814
815    my $spindle = Sympa::Spindle::ProcessRequest->new(
816        context          => $family,
817        action           => 'create_automatic_list',
818        parameters       => {file => $main::options{'input_file'}},
819        sender           => Sympa::get_address($family, 'listmaster'),
820        scenario_context => {skip => 1},
821    );
822    unless ($spindle and $spindle->spin and _report($spindle)) {
823        printf STDERR "Impossible to add a list to the family %s\n",
824            $family_name;
825        exit 1;
826    }
827
828    exit 0;
829
830} elsif ($main::options{'sync_include'}) {
831    my $list = Sympa::List->new($main::options{'sync_include'});
832    my $role = $main::options{'role'} || 'member';    # Compat. <= 6.2.54
833
834    unless (defined $list) {
835        printf STDERR "Incorrect list name %s\n",
836            $main::options{'sync_include'};
837        exit 1;
838    }
839    unless (grep { $role eq $_ } qw(member owner editor)) {
840        printf STDERR "Unknown role %s\n", $role;
841        exit 1;
842    }
843
844    my $spindle = Sympa::Spindle::ProcessRequest->new(
845        context          => $list,
846        action           => 'include',
847        role             => $role,
848        sender           => Sympa::get_address($list, 'listmaster'),
849        scenario_context => {skip => 1},
850    );
851    unless ($spindle and $spindle->spin and _report($spindle)) {
852        printf STDERR "Could not sync role %s of list %s with data sources\n",
853            $role, $list->get_id;
854        exit 1;
855    }
856    exit 0;
857## Migration from one version to another
858} elsif ($main::options{'upgrade'}) {
859
860    $log->syslog('notice', "Upgrade process...");
861
862    $main::options{'from'} ||= Sympa::Upgrade::get_previous_version();
863    $main::options{'to'}   ||= Sympa::Constants::VERSION;
864
865    if ($main::options{'from'} eq $main::options{'to'}) {
866        $log->syslog('notice', 'Current version: %s; no upgrade is required',
867            $main::options{'to'});
868        exit 0;
869    } else {
870        $log->syslog('notice', "Upgrading from %s to %s...",
871            $main::options{'from'}, $main::options{'to'});
872    }
873
874    unless (
875        Sympa::Upgrade::upgrade($main::options{'from'}, $main::options{'to'}))
876    {
877        $log->syslog('err', "Migration from %s to %s failed",
878            $main::options{'from'}, $main::options{'to'});
879        exit 1;
880    }
881
882    $log->syslog('notice', 'Upgrade process finished');
883    Sympa::Upgrade::update_version();
884
885    exit 0;
886
887} elsif ($main::options{'upgrade_shared'}) {
888    print STDERR "Obsoleted.  Use upgrade_shared_repository.pl.\n";
889
890    exit 0;
891} elsif ($main::options{'reload_list_config'}) {
892    if ($main::options{'list'}) {
893        $log->syslog('notice', 'Loading list %s...', $main::options{'list'});
894        my $list =
895            Sympa::List->new($main::options{'list'}, '',
896            {reload_config => 1});
897        unless (defined $list) {
898            printf STDERR "Error : incorrect list name '%s'\n",
899                $main::options{'list'};
900            exit 1;
901        }
902    } else {
903        $log->syslog('notice', "Loading ALL lists...");
904        my $all_lists = Sympa::List::get_lists('*', reload_config => 1);
905    }
906    $log->syslog('notice', '...Done.');
907
908    exit 0;
909}
910
911##########################################
912elsif ($main::options{'modify_list'}) {
913    my $robot = $main::options{'robot'} || $Conf::Conf{'domain'};
914
915    my $family;
916    unless ($main::options{'modify_list'}) {
917        print STDERR "Error : missing family parameter\n";
918        exit 1;
919    }
920    unless ($family =
921        Sympa::Family->new($main::options{'modify_list'}, $robot)) {
922        printf STDERR
923            "The family %s does not exist, impossible to modify the list.\n",
924            $main::options{'modify_list'};
925        exit 1;
926    }
927    unless ($main::options{'input_file'}) {
928        print STDERR "Error : missing input_file parameter\n";
929        exit 1;
930    }
931
932    # list config family updating
933    my $spindle = Sympa::Spindle::ProcessRequest->new(
934        context          => $family,
935        action           => 'update_automatic_list',
936        parameters       => {file => $main::options{'input_file'}},
937        sender           => Sympa::get_address($family, 'listmaster'),
938        scenario_context => {skip => 1},
939    );
940    unless ($spindle and $spindle->spin and _report($spindle)) {
941        print STDERR "No object list resulting from updating\n";
942        exit 1;
943    }
944
945    exit 0;
946}
947
948##########################################
949elsif ($main::options{'close_family'}) {
950    my $robot = $main::options{'robot'} || $Conf::Conf{'domain'};
951
952    my $family_name;
953    unless ($family_name = $main::options{'close_family'}) {
954        pod2usage(-exitval => 1, -output => \*STDERR);
955    }
956    my $family;
957    unless ($family = Sympa::Family->new($family_name, $robot)) {
958        printf STDERR
959            "The family %s does not exist, impossible family closure\n",
960            $family_name;
961        exit 1;
962    }
963
964    my $lists = Sympa::List::get_lists($family);
965    my @impossible_close;
966    my @close_ok;
967
968    foreach my $list (@{$lists || []}) {
969        my $listname = $list->{'name'};
970
971        my $spindle = Sympa::Spindle::ProcessRequest->new(
972            context          => $family->{'domain'},
973            action           => 'close_list',
974            current_list     => $list,
975            sender           => Sympa::get_address($family, 'listmaster'),
976            scenario_context => {skip => 1},
977        );
978        unless ($spindle and $spindle->spin and _report($spindle)) {
979            push @impossible_close, $listname;
980            next;
981        }
982        push(@close_ok, $listname);
983    }
984
985    if (@impossible_close) {
986        print "\nImpossible list closure for : \n  "
987            . join(", ", @impossible_close) . "\n";
988    }
989    if (@close_ok) {
990        print "\nThese lists are closed : \n  "
991            . join(", ", @close_ok) . "\n";
992    }
993
994    exit 0;
995}
996##########################################
997elsif ($main::options{'sync_list_db'}) {
998    my $listname = $main::options{'list'} || '';
999    if (length($listname) > 1) {
1000        my $list = Sympa::List->new($listname);
1001        unless (defined $list) {
1002            printf STDOUT "\nList '%s' does not exist. \n", $listname;
1003            exit 1;
1004        }
1005        $list->_update_list_db;
1006    } else {
1007        Sympa::List::_flush_list_db();
1008        my $all_lists = Sympa::List::get_lists('*', 'reload_config' => 1);
1009        foreach my $list (@$all_lists) {
1010            $list->_update_list_db;
1011        }
1012    }
1013    exit 0;
1014} elsif ($main::options{'export_list'}) {
1015    my $robot_id = $main::options{'robot'} || '*';
1016    my $all_lists = Sympa::List::get_lists($robot_id);
1017    exit 1 unless defined $all_lists;
1018    foreach my $list (@$all_lists) {
1019        printf "%s\n", $list->{'name'};
1020    }
1021    exit 0;
1022} elsif ($main::options{'send_digest'}) {
1023    Sympa::Spindle::ProcessDigest->new(
1024        send_now    => 1,
1025        keep_digest => $main::options{'keep_digest'},
1026    )->spin;
1027    exit 0;
1028} elsif ($main::options{'open_list'}) {
1029    my ($listname, $robot_id) = split /\@/, $main::options{'open_list'}, 2;
1030    my $current_list = Sympa::List->new($listname, $robot_id);
1031    unless ($current_list) {
1032        printf STDERR "Incorrect list name %s.\n",
1033            $main::options{'open_list'};
1034        exit 1;
1035    }
1036
1037    my $mode = 'open';
1038    $mode = 'install' if $current_list->{'admin'}{'status'} eq 'pending';
1039    my $notify = $main::options{'notify'} // 0;
1040
1041    my $spindle = Sympa::Spindle::ProcessRequest->new(
1042        context          => $robot_id,
1043        action           => 'open_list',
1044        mode             => $mode,
1045        notify           => $notify,
1046        current_list     => $current_list,
1047        sender           => Sympa::get_address($robot_id, 'listmaster'),
1048        scenario_context => {skip => 1},
1049    );
1050    unless ($spindle and $spindle->spin and _report($spindle)) {
1051        printf STDERR "Could not open list %s\n", $current_list->get_id;
1052        exit 1;
1053    }
1054    exit 0;
1055} elsif ($main::options{'show_pending_lists'}) {
1056    my $all_lists = Sympa::List::get_lists(
1057        $main::options{'show_pending_lists'},
1058        'filter' => ['status' => 'pending']
1059    );
1060
1061    if (@{$all_lists}) {
1062        print "Pending lists:\n";
1063        foreach my $list (@$all_lists) {
1064            printf "%s@%s\n  subject: %s\n  creator: %s\n  date: %s\n",
1065                $list->{'name'},
1066                $main::options{'show_pending_lists'},
1067                $list->{'admin'}{'subject'},
1068                $list->{'admin'}{'creation'}{'email'},
1069                $list->{'admin'}{'creation'}{'date_epoch'};
1070        }
1071    } else {
1072        printf "No pending list for robot %s\n",
1073            $main::options{'show_pending_lists'};
1074    }
1075    exit 0;
1076} elsif ($main::options{'rebuildarc'}) {
1077    my ($listname, $robot_id) = split /\@/, $main::options{'rebuildarc'}, 2;
1078    my $current_list = Sympa::List->new($listname, $robot_id);
1079    unless ($current_list) {
1080        printf STDERR "Incorrect list name %s.\n",
1081            $main::options{'rebuildarc'};
1082        exit 1;
1083    }
1084
1085    my $arc_message = Sympa::Message->new(
1086        sprintf("\nrebuildarc %s *\n\n", $listname),
1087        context => $robot_id,
1088        sender  => Sympa::get_address($robot_id, 'listmaster'),
1089        date    => time
1090    );
1091    my $marshalled = Sympa::Spool::Archive->new->store($arc_message);
1092    unless ($marshalled) {
1093        printf STDERR "Cannot store command to rebuild archive of list %s.\n",
1094            $main::options{'rebuildarc'};
1095        exit 1;
1096    }
1097    printf "Archive rebuild scheduled for %s.\n",
1098        $main::options{'rebuildarc'};
1099    exit 0;
1100}
1101
1102die 'Unknown option';
1103
1104exit(0);
1105
1106# Load configuration.
1107sub _load {
1108    ## Load sympa.conf.
1109    unless (Conf::load(Conf::get_sympa_conf(), 'no_db')) {    #Site and Robot
1110        die sprintf
1111            "Unable to load sympa configuration, file %s or one of the vhost robot.conf files contain errors. Exiting.\n",
1112            Conf::get_sympa_conf();
1113    }
1114
1115    ## Open the syslog and say we're read out stuff.
1116    $log->openlog($Conf::Conf{'syslog'}, $Conf::Conf{'log_socket_type'});
1117
1118    # Enable SMTP logging if required
1119    $mailer->{log_smtp} = $main::options{'mail'}
1120        || Sympa::Tools::Data::smart_eq($Conf::Conf{'log_smtp'}, 'on');
1121
1122    # setting log_level using conf unless it is set by calling option
1123    if (defined $main::options{'log_level'}) {
1124        $log->{level} = $main::options{'log_level'};
1125        $log->syslog(
1126            'info',
1127            'Configuration file read, log level set using options: %s',
1128            $main::options{'log_level'}
1129        );
1130    } else {
1131        $log->{level} = $Conf::Conf{'log_level'};
1132        $log->syslog(
1133            'info',
1134            'Configuration file read, default log level %s',
1135            $Conf::Conf{'log_level'}
1136        );
1137    }
1138
1139    # Check database connectivity.
1140    unless (Sympa::DatabaseManager->instance) {
1141        die sprintf
1142            "Database %s defined in sympa.conf is unreachable. verify db_xxx parameters in sympa.conf\n",
1143            $Conf::Conf{'db_name'};
1144    }
1145
1146    # Now trying to load full config (including database)
1147    unless (Conf::load()) {    #FIXME: load Site, then robot cache
1148        die sprintf
1149            "Unable to load Sympa configuration, file %s or any of the virtual host robot.conf files contain errors. Exiting.\n",
1150            Conf::get_sympa_conf();
1151    }
1152
1153    ## Set locale configuration
1154    ## Compatibility with version < 2.3.3
1155    $main::options{'lang'} =~ s/\.cat$//
1156        if defined $main::options{'lang'};
1157    $default_lang =
1158        $language->set_lang($main::options{'lang'}, $Conf::Conf{'lang'},
1159        'en');
1160
1161    ## Main program
1162    if (!chdir($Conf::Conf{'home'})) {
1163        die sprintf 'Can\'t chdir to %s: %s', $Conf::Conf{'home'}, $ERRNO;
1164        ## Function never returns.
1165    }
1166
1167    ## Check for several files.
1168    unless (Conf::checkfiles_as_root()) {
1169        die "Missing files\n";
1170    }
1171}
1172
1173sub _report {
1174    my $spindle = shift;
1175
1176    my @reports = @{$spindle->{stash} || []};
1177    @reports = ([undef, 'notice', 'performed']) unless @reports;
1178
1179    my $template = Sympa::Template->new('*', subdir => 'mail_tt2');
1180    foreach my $report (@reports) {
1181        my ($request, $report_type, $report_entry, $report_param) = @$report;
1182        my $action = $request ? $request->{action} : 'sympa';
1183        my $message = '';
1184        $template->parse(
1185            {   report_type  => $report_type,
1186                report_entry => $report_entry,
1187                report_param => ($report_param || {}),
1188            },
1189            'report.tt2',
1190            \$message
1191        );
1192        $message ||= $report_entry;
1193        $message =~ s/\n/ /g;
1194
1195        printf STDERR "%s [%s] %s\n", $action, $report_type, $message;
1196    }
1197
1198    return $spindle->success ? 1 : undef;
1199}
1200
1201# DEPRECATED.  Use Sympa::Spindle::ProcessDigest class.
1202#sub SendDigest;
1203
1204# Lowercase field from database.
1205# Old names: List::lowercase_field(), Sympa::List::lowercase_field().
1206sub _lowercase_field {
1207    my ($table, $field) = @_;
1208
1209    my $sth;
1210    my $sdm   = Sympa::DatabaseManager->instance;
1211    my $total = 0;
1212
1213    unless ($sdm
1214        and $sth = $sdm->do_query(q{SELECT %s FROM %s}, $field, $table)) {
1215        $log->syslog('err', 'Unable to get values of field %s for table %s',
1216            $field, $table);
1217        return undef;
1218    }
1219
1220    while (my $user = $sth->fetchrow_hashref('NAME_lc')) {
1221        my $lower_cased = lc($user->{$field});
1222        next if $lower_cased eq $user->{$field};
1223
1224        $total++;
1225
1226        ## Updating database.
1227        unless (
1228            $sth = $sdm->do_prepared_query(
1229                sprintf(
1230                    q{UPDATE %s SET %s = ? WHERE %s = ?},
1231                    $table, $field, $field
1232                ),
1233                $lower_cased,
1234                $user->{$field}
1235            )
1236        ) {
1237            $log->syslog('err',
1238                'Unable to set field % from table %s to value %s',
1239                $field, $lower_cased, $table);
1240            next;
1241        }
1242    }
1243    $sth->finish();
1244
1245    return $total;
1246}
1247
1248#### Subroutines for family
1249
1250use Term::ProgressBar;
1251use XML::LibXML;
1252
1253# instantiate family action :
1254#  - create family lists if they are not
1255#  - update family lists if they already exist
1256#
1257# IN : -$family
1258#      -$xml_fh : file handle on the xml file
1259#      -%options
1260#        - close_unknown : true if must close old lists undefined in new
1261#                          instantiation
1262# OUT : -1 or undef
1263# Old name: Sympa::Family::instantiate().
1264sub instantiate {
1265    $log->syslog('debug2', '(%s, %s, ...)', @_);
1266    my $family   = shift;
1267    my $xml_file = shift;
1268    my %options  = @_;
1269
1270    ## all the description variables are emptied.
1271    _initialize_instantiation($family);
1272
1273    ## get the currently existing lists in the family
1274    my $previous_family_lists = {
1275        (   map { $_->{name} => $_ }
1276                @{Sympa::List::get_lists($family, no_check_family => 1) || []}
1277        )
1278    };
1279
1280    ## Splits the family description XML file into a set of list description
1281    ## xml files
1282    ## and collects lists to be created in $list_to_generate.
1283    my $list_to_generate = _split_xml_file($family, $xml_file);
1284    unless ($list_to_generate) {
1285        $log->syslog('err', 'Errors during the parsing of family xml file');
1286        return undef;
1287    }
1288
1289    my $created = 0;
1290    my $total;
1291    my $progress;
1292    unless (@$list_to_generate) {
1293        $log->syslog('err', 'No list found in XML file %s.', $xml_file);
1294        $total = 0;
1295    } else {
1296        $total    = scalar @$list_to_generate;
1297        $progress = Term::ProgressBar->new(
1298            {   name  => 'Creating lists',
1299                count => $total,
1300                ETA   => 'linear'
1301            }
1302        );
1303        $progress->max_update_rate(1);
1304    }
1305    my $next_update = 0;
1306
1307    # EACH FAMILY LIST
1308    foreach my $listname (@$list_to_generate) {
1309        my $path = $family->{'dir'} . '/' . $listname . '.xml';
1310        my $list = Sympa::List->new($listname, $family->{'domain'},
1311            {no_check_family => 1});
1312
1313        if ($list) {
1314            ## LIST ALREADY EXISTING
1315            delete $previous_family_lists->{$list->{'name'}};
1316
1317            # Update list config.
1318            my $spindle = Sympa::Spindle::ProcessRequest->new(
1319                context          => $family,
1320                action           => 'update_automatic_list',
1321                parameters       => {file => $path},
1322                sender           => Sympa::get_address($family, 'listmaster'),
1323                scenario_context => {skip => 1},
1324            );
1325            unless ($spindle and $spindle->spin and $spindle->success) {
1326                push(@{$family->{'errors'}{'update_list'}}, $list->{'name'});
1327                $list->set_status_error_config('instantiation_family',
1328                    $family->{'name'});
1329                next;
1330            }
1331        } else {
1332            # FIRST LIST CREATION
1333
1334            ## Create the list
1335            my $spindle = Sympa::Spindle::ProcessRequest->new(
1336                context          => $family,
1337                action           => 'create_automatic_list',
1338                listname         => $listname,
1339                parameters       => {file => $path},
1340                sender           => Sympa::get_address($family, 'listmaster'),
1341                scenario_context => {skip => 1},
1342            );
1343            unless ($spindle and $spindle->spin and $spindle->success) {
1344                push @{$family->{'errors'}{'create_list'}}, $listname;
1345                next;
1346            }
1347
1348            $list = Sympa::List->new($listname, $family->{'domain'},
1349                {no_check_family => 1});
1350
1351            ## aliases
1352            if (grep { $_->[1] eq 'notice' and $_->[2] eq 'auto_aliases' }
1353                @{$spindle->{stash} || []}) {
1354                push(
1355                    @{$family->{'created_lists'}{'with_aliases'}},
1356                    $list->{'name'}
1357                );
1358            } else {
1359                $family->{'created_lists'}{'without_aliases'}{$list->{'name'}}
1360                    = $list->{'name'};
1361            }
1362        }
1363
1364        $created++;
1365        $progress->message(
1366            sprintf(
1367                "List \"%s\" (%i/%i) created/updated",
1368                $list->{'name'}, $created, $total
1369            )
1370        );
1371        $next_update = $progress->update($created)
1372            if ($created > $next_update);
1373    }
1374
1375    $progress->update($total) if $progress;
1376
1377    ## PREVIOUS LIST LEFT
1378    foreach my $l (keys %{$previous_family_lists}) {
1379        my $list;
1380        unless ($list =
1381            Sympa::List->new($l, $family->{'domain'}, {no_check_family => 1}))
1382        {
1383            push(@{$family->{'errors'}{'previous_list'}}, $l);
1384            next;
1385        }
1386
1387        my $answer;
1388        unless ($options{close_unknown}) {
1389            #while ($answer ne 'y' and $answer ne 'n') {
1390            print STDOUT
1391                "The list $l isn't defined in the new instantiation family, do you want to close it ? (y or n)";
1392            $answer = <STDIN>;
1393            chomp($answer);
1394            #######################
1395            $answer ||= 'y';
1396            #}
1397        }
1398        if ($options{close_unknown} or $answer eq 'y') {
1399            my $spindle = Sympa::Spindle::ProcessRequest->new(
1400                context          => $family->{'domain'},
1401                action           => 'close_list',
1402                current_list     => $list,
1403                sender           => Sympa::get_address($family, 'listmaster'),
1404                scenario_context => {skip => 1},
1405            );
1406            unless ($spindle and $spindle->spin and $spindle->success) {
1407                push @{$family->{'family_closed'}{'impossible'}},
1408                    $list->{'name'};
1409            }
1410            push(@{$family->{'family_closed'}{'ok'}}, $list->{'name'});
1411
1412        } elsif (lc($answer) eq 'n') {
1413            next;
1414        } else {
1415            my $spindle = Sympa::Spindle::ProcessRequest->new(
1416                context      => $family,
1417                action       => 'update_automatic_list',
1418                current_list => $list,
1419                parameters   => {file => $list->{'dir'} . '/instance.xml'},
1420                sender       => Sympa::get_address($family, 'listmaster'),
1421                scenario_context => {skip => 1},
1422            );
1423            unless ($spindle and $spindle->spin and $spindle->success) {
1424                push(@{$family->{'errors'}{'update_list'}}, $list->{'name'});
1425                $list->set_status_error_config('instantiation_family',
1426                    $family->{'name'});
1427                next;
1428            }
1429        }
1430    }
1431
1432    return 1;
1433}
1434
1435# return a string of instantiation results
1436#
1437# IN : -$family
1438#
1439# OUT : -$string
1440# Old name: Sympa::Family::get_instantiation_results().
1441sub get_instantiation_results {
1442    my ($family, $result) = @_;
1443    $log->syslog('debug3', '(%s)', $family->{'name'});
1444
1445    $result->{'errors'} = ();
1446    $result->{'warn'}   = ();
1447    $result->{'info'}   = ();
1448    my $string;
1449
1450    unless ($#{$family->{'errors'}{'create_hash'}} < 0) {
1451        push(
1452            @{$result->{'errors'}},
1453            "\nImpossible list generation because errors in xml file for : \n  "
1454                . join(", ", @{$family->{'errors'}{'create_hash'}}) . "\n"
1455        );
1456    }
1457
1458    unless ($#{$family->{'errors'}{'create_list'}} < 0) {
1459        push(
1460            @{$result->{'errors'}},
1461            "\nImpossible list creation for : \n  "
1462                . join(", ", @{$family->{'errors'}{'create_list'}}) . "\n"
1463        );
1464    }
1465
1466    unless ($#{$family->{'errors'}{'listname_already_used'}} < 0) {
1467        push(
1468            @{$result->{'errors'}},
1469            "\nImpossible list creation because listname is already used (orphelan list or in another family) for : \n  "
1470                . join(", ", @{$family->{'errors'}{'listname_already_used'}})
1471                . "\n"
1472        );
1473    }
1474
1475    unless ($#{$family->{'errors'}{'update_list'}} < 0) {
1476        push(
1477            @{$result->{'errors'}},
1478            "\nImpossible list updating for : \n  "
1479                . join(", ", @{$family->{'errors'}{'update_list'}}) . "\n"
1480        );
1481    }
1482
1483    unless ($#{$family->{'errors'}{'previous_list'}} < 0) {
1484        push(
1485            @{$result->{'errors'}},
1486            "\nExisted lists from the lastest instantiation impossible to get and not anymore defined in the new instantiation : \n  "
1487                . join(", ", @{$family->{'errors'}{'previous_list'}}) . "\n"
1488        );
1489    }
1490
1491    # $string .= "\n****************************************\n";
1492
1493    unless ($#{$family->{'created_lists'}{'with_aliases'}} < 0) {
1494        push(
1495            @{$result->{'info'}},
1496            "\nThese lists have been created and aliases are ok :\n  "
1497                . join(", ", @{$family->{'created_lists'}{'with_aliases'}})
1498                . "\n"
1499        );
1500    }
1501
1502    my $without_aliases = $family->{'created_lists'}{'without_aliases'};
1503    if (ref $without_aliases) {
1504        if (scalar %{$without_aliases}) {
1505            $string =
1506                "\nThese lists have been created but aliases need to be installed : \n";
1507            foreach my $l (keys %{$without_aliases}) {
1508                $string .= " $without_aliases->{$l}";
1509            }
1510            push(@{$result->{'warn'}}, $string . "\n");
1511        }
1512    }
1513
1514    unless ($#{$family->{'updated_lists'}{'aliases_ok'}} < 0) {
1515        push(
1516            @{$result->{'info'}},
1517            "\nThese lists have been updated and aliases are ok :\n  "
1518                . join(", ", @{$family->{'updated_lists'}{'aliases_ok'}})
1519                . "\n"
1520        );
1521    }
1522
1523    my $aliases_to_install = $family->{'updated_lists'}{'aliases_to_install'};
1524    if (ref $aliases_to_install) {
1525        if (scalar %{$aliases_to_install}) {
1526            $string =
1527                "\nThese lists have been updated but aliases need to be installed : \n";
1528            foreach my $l (keys %{$aliases_to_install}) {
1529                $string .= " $aliases_to_install->{$l}";
1530            }
1531            push(@{$result->{'warn'}}, $string . "\n");
1532        }
1533    }
1534
1535    my $aliases_to_remove = $family->{'updated_lists'}{'aliases_to_remove'};
1536    if (ref $aliases_to_remove) {
1537        if (scalar %{$aliases_to_remove}) {
1538            $string =
1539                "\nThese lists have been updated but aliases need to be removed : \n";
1540            foreach my $l (keys %{$aliases_to_remove}) {
1541                $string .= " $aliases_to_remove->{$l}";
1542            }
1543            push(@{$result->{'warn'}}, $string . "\n");
1544        }
1545    }
1546
1547    # $string .= "\n****************************************\n";
1548
1549    unless ($#{$family->{'generated_lists'}{'file_error'}} < 0) {
1550        push(
1551            @{$result->{'errors'}},
1552            "\nThese lists have been generated but they are in status error_config because of errors while creating list config files :\n  "
1553                . join(", ", @{$family->{'generated_lists'}{'file_error'}})
1554                . "\n"
1555        );
1556    }
1557
1558    my $constraint_error = $family->{'generated_lists'}{'constraint_error'};
1559    if (ref $constraint_error) {
1560        if (scalar %{$constraint_error}) {
1561            $string =
1562                "\nThese lists have been generated but there are in status error_config because of errors on parameter constraint :\n";
1563            foreach my $l (keys %{$constraint_error}) {
1564                $string .= " $l : " . $constraint_error->{$l} . "\n";
1565            }
1566            push(@{$result->{'errors'}}, $string);
1567        }
1568    }
1569
1570    # $string .= "\n****************************************\n";
1571
1572    unless ($#{$family->{'family_closed'}{'ok'}} < 0) {
1573        push(
1574            @{$result->{'info'}},
1575            "\nThese lists don't belong anymore to the family, they are in status family_closed :\n  "
1576                . join(", ", @{$family->{'family_closed'}{'ok'}}) . "\n"
1577        );
1578    }
1579
1580    unless ($#{$family->{'family_closed'}{'impossible'}} < 0) {
1581        push(
1582            @{$result->{'warn'}},
1583            "\nThese lists don't belong anymore to the family, but they can't be set in status family_closed :\n  "
1584                . join(", ", @{$family->{'family_closed'}{'impossible'}})
1585                . "\n"
1586        );
1587    }
1588
1589    unshift @{$result->{'errors'}},
1590        "\n********** ERRORS IN INSTANTIATION of $family->{'name'} FAMILY ********************\n"
1591        if ($#{$result->{'errors'}} > 0);
1592    unshift @{$result->{'warn'}},
1593        "\n********** WARNINGS IN INSTANTIATION of $family->{'name'} FAMILY ********************\n"
1594        if ($#{$result->{'warn'}} > 0);
1595    unshift @{$result->{'info'}},
1596        "\n\n******************************************************************************\n"
1597        . "\n******************** INSTANTIATION of $family->{'name'} FAMILY ********************\n"
1598        . "\n******************************************************************************\n\n";
1599
1600    return $#{$result->{'errors'}};
1601
1602}
1603
1604# initialize vars for instantiation and result
1605# then to make a string result
1606#
1607# IN  : -$family
1608# OUT : -1
1609# Old name: Sympa::Family::_initialize_instantiation().
1610sub _initialize_instantiation {
1611    my $family = shift;
1612    $log->syslog('debug3', '(%s)', $family->{'name'});
1613
1614    ### info vars for instantiate  ###
1615    ### returned by                ###
1616    ### get_instantiation_results  ###
1617
1618    ## lists in error during creation or updating : LIST FATAL ERROR
1619    # array of xml file name  : error during xml data extraction
1620    $family->{'errors'}{'create_hash'} = ();
1621    ## array of list name : error during list creation
1622    $family->{'errors'}{'create_list'} = ();
1623    ## array of list name : error during list updating
1624    $family->{'errors'}{'update_list'} = ();
1625    ## array of list name : listname already used (in another family)
1626    $family->{'errors'}{'listname_already_used'} = ();
1627    ## array of list name : previous list impossible to get
1628    $family->{'errors'}{'previous_list'} = ();
1629
1630    ## created or updated lists
1631    ## array of list name : aliases are OK (installed or not, according to
1632    ## status)
1633    $family->{'created_lists'}{'with_aliases'} = ();
1634    ## hash of (list name -> aliases) : aliases needed to be installed
1635    $family->{'created_lists'}{'without_aliases'} = {};
1636    ## array of list name : aliases are OK (installed or not, according to
1637    ## status)
1638    $family->{'updated_lists'}{'aliases_ok'} = ();
1639    ## hash of (list name -> aliases) : aliases needed to be installed
1640    $family->{'updated_lists'}{'aliases_to_install'} = {};
1641    ## hash of (list name -> aliases) : aliases needed to be removed
1642    $family->{'updated_lists'}{'aliases_to_remove'} = {};
1643
1644    ## generated (created or updated) lists in error : no fatal error for the
1645    ## list
1646    ## array of list name : error during copying files
1647    $family->{'generated_lists'}{'file_error'} = ();
1648    ## hash of (list name -> array of param) : family constraint error
1649    $family->{'generated_lists'}{'constraint_error'} = {};
1650
1651    ## lists isn't anymore in the family
1652    ## array of list name : lists in status family_closed
1653    $family->{'family_closed'}{'ok'} = ();
1654    ## array of list name : lists that must be in status family_closed but
1655    ## they aren't
1656    $family->{'family_closed'}{'impossible'} = ();
1657
1658    return 1;
1659}
1660
1661# split the xml family file into xml list files. New
1662# list names are put in the array reference
1663# and new files are put in
1664# the family directory
1665#
1666# IN : -$family
1667#      -$xml_fh : file handle on xml file containing description
1668#               of the family lists
1669# OUT : -1 (if OK) or undef
1670# Old name: Sympa::Family::_split_xml_file().
1671sub _split_xml_file {
1672    my $family   = shift;
1673    my $xml_file = shift;
1674    my $root;
1675    $log->syslog('debug2', '(%s)', $family->{'name'});
1676
1677    ## parse file
1678    my $parser = XML::LibXML->new();
1679    $parser->line_numbers(1);
1680    my $doc;
1681
1682    unless ($doc = $parser->parse_file($xml_file)) {
1683        $log->syslog('err', 'Failed to parse XML file');
1684        return undef;
1685    }
1686
1687    ## the family document
1688    $root = $doc->documentElement();
1689    unless ($root->nodeName eq 'family') {
1690        $log->syslog('err', 'The root element must be called "family"');
1691        return undef;
1692    }
1693
1694    # Lists: Family's elements.
1695    my @list_to_generate;
1696    foreach my $list_elt ($root->childNodes()) {
1697
1698        if ($list_elt->nodeType == 1) {    # ELEMENT_NODE
1699            unless ($list_elt->nodeName eq 'list') {
1700                $log->syslog(
1701                    'err',
1702                    'Elements contained in the root element must be called "list", line %s',
1703                    $list_elt->line_number()
1704                );
1705                return undef;
1706            }
1707        } else {
1708            next;
1709        }
1710
1711        ## listname
1712        my @children = $list_elt->getChildrenByTagName('listname');
1713
1714        if ($#children < 0) {
1715            $log->syslog(
1716                'err',
1717                '"listname" element is required in "list" element, line: %s',
1718                $list_elt->line_number()
1719            );
1720            return undef;
1721        }
1722        if ($#children > 0) {
1723            my @error;
1724            foreach my $i (@children) {
1725                push(@error, $i->line_number());
1726            }
1727            $log->syslog(
1728                'err',
1729                'Only one "listname" element is allowed for "list" element, lines: %s',
1730                join(", ", @error)
1731            );
1732            return undef;
1733        }
1734        my $listname_elt = shift @children;
1735        my $listname     = $listname_elt->textContent();
1736        $listname =~ s/^\s*//;
1737        $listname =~ s/\s*$//;
1738        $listname = lc $listname;
1739        my $filename = $listname . ".xml";
1740
1741        ## creating list XML document
1742        my $list_doc =
1743            XML::LibXML::Document->createDocument($doc->version(),
1744            $doc->encoding());
1745        $list_doc->setDocumentElement($list_elt);
1746
1747        ## creating the list xml file
1748        unless ($list_doc->toFile("$family->{'dir'}/$filename", 0)) {
1749            $log->syslog(
1750                'err',
1751                'Cannot create list file %s',
1752                $family->{'dir'} . '/' . $filename,
1753                $list_elt->line_number()
1754            );
1755            return undef;
1756        }
1757
1758        push @list_to_generate, $listname;
1759    }
1760    return [@list_to_generate];
1761}
1762
1763__END__
1764
1765=encoding utf-8
1766
1767=head1 NAME
1768
1769sympa, sympa.pl - Command line utility to manage Sympa
1770
1771=head1 SYNOPSIS
1772
1773C<sympa.pl> S<[ C<-d, --debug> ]> S<[ C<-f, --file>=I<another.sympa.conf> ]>
1774S<[ C<-l, --lang>=I<lang> ]> S<[ C<-m, --mail> ]>
1775S<[ C<-h, --help> ]> S<[ C<-v, --version> ]>
1776S<>
1777S<[ C<--import>=I<listname> ]>
1778S<[ C<--open_list>=I<list>[I<@robot>] [--notify] ]>
1779S<[ C<--close_list>=I<list>[I<@robot>] ]>
1780S<[ C<--purge_list>=I<list>[I<@robot>] ]>
1781S<[ C<--lowercase> ]> S<[ C<--make_alias_file> ]>
1782S<[ C<--dump_users> C<--list>=I<list>@I<domain>|ALL [ C<--role>=I<roles> ] ]>
1783S<[ C<--restore_users> C<--list>=I<list>@I<domain>|ALL [ C<--role>=I<roles> ] ]>
1784S<[ C<--show_pending_lists>=I<robot> ]>
1785S<[ C<--rebuildarc>=I<list>[I<@robot>] ]>
1786
1787=head1 DESCRIPTION
1788
1789NOTE:
1790On overview of Sympa documentation see L<sympa_toc(1)>.
1791
1792Sympa.pl is invoked from command line then performs various administration
1793tasks.
1794
1795=head1 OPTIONS
1796
1797F<sympa.pl> may run with following options in general.
1798
1799=over 4
1800
1801=item C<-d>, C<--debug>
1802
1803Enable debug mode.
1804
1805=item C<-f>, C<--config=>I<file>
1806
1807Force Sympa to use an alternative configuration file instead
1808of F<--CONFIG-->.
1809
1810=item C<-l>, C<--lang=>I<lang>
1811
1812Set this option to use a language for Sympa. The corresponding
1813gettext catalog file must be located in F<$LOCALEDIR>
1814directory.
1815
1816=item C<--log_level=>I<level>
1817
1818Sets Sympa log level.
1819
1820=back
1821
1822With the following options F<sympa.pl> will run in batch mode:
1823
1824=over 4
1825
1826=item C<--add_list=>I<family_name> C<--robot=>I<robot_name>
1827C<--input_file=>I</path/to/file.xml>
1828
1829Add the list described by the file.xml under robot_name, to the family
1830family_name.
1831
1832=item C<--change_user_email> C<--current_email=>I<xx> C<--new_email=>I<xx>
1833
1834Changes a user email address in all Sympa  databases (subscriber_table,
1835list config, etc) for all virtual robots.
1836
1837=item C<--close_family=>I<family_name> C<--robot=>I<robot_name>
1838
1839Close lists of family_name family under robot_name.
1840
1841=item C<--close_list=>I<list>[I<@robot>]
1842
1843Close the list (changing its status to closed), remove aliases and remove
1844subscribers from DB (a dump is created in the list directory to allow
1845restoring the list)
1846
1847=item C<--conf_2_db>
1848
1849Load sympa.conf and each robot.conf into database.
1850
1851=item C<--copy_list=>I<listname>@I<robot>
1852C<--new_listname=>I<newlistname> C<--new_listrobot=>I<newrobot>
1853
1854Copy a list.
1855
1856=item C<--create_list> C<--robot=>I<robot_name>
1857C<--input_file=>I</path/to/file.xml >
1858
1859Create a list with the XML file under robot robot_name.
1860
1861=item C<--dump=>I<list>@I<domain>|C<ALL>
1862
1863Obsoleted option.  Use C<--dump_users>.
1864
1865=item C<--dump_users> C<--list=>I<list>@I<domain>|C<ALL> [ C<--role=>I<roles> ]
1866
1867Dumps users of a list or all lists.
1868
1869C<--role> may specify C<member>, C<owner>, C<editor> or any of them separated
1870by comma (C<,>). Only C<member> is chosen by default.
1871
1872Users are dumped in files I<role>C<.dump> in each list directory.
1873
1874Note: On Sympa prior to 6.2.31b.1, subscribers were dumped in
1875F<subscribers.db.dump> file, and owners and moderators could not be dumped.
1876
1877See also C<--restore_users>.
1878
1879Note: This option replaced C<--dump> on Sympa 6.2.34.
1880
1881=begin comment
1882
1883=item C<--export_list> [ C<--robot=>I<robot_name> ]
1884
1885B<Not fully implemented>.
1886
1887=end comment
1888
1889=item C<--health_check>
1890
1891Check if F<sympa.conf>, F<robot.conf> of virtual robots and database structure
1892are correct.  If any errors occur, exits with non-zero status.
1893
1894=item C<--import=>I<list>@I<dom>
1895
1896Import subscribers in the list. Data are read from standard input.
1897The imported data should contain one entry per line : the first field
1898is an email address, the second (optional) field is the free form name.
1899Fields are spaces-separated.
1900
1901Use C<--quiet> to prevent welcome emails.
1902
1903Sample:
1904
1905    ## Data to be imported
1906    ## email        gecos
1907    john.steward@some.company.com           John - accountant
1908    mary.blacksmith@another.company.com     Mary - secretary
1909
1910=item C<--instantiate_family=>I<family_name> C<--robot=>I<robot_name>
1911C<--input_file=>I</path/to/file.xml> [ C<--close_unknown> ] [ C<--quiet> ]
1912
1913Instantiate family_name lists described in the file.xml under robot_name.
1914The family directory must exist; automatically close undefined lists in a
1915new instantiation if --close_unknown is specified; do not print report if
1916C<--quiet> is specified.
1917
1918=item C<--lowercase>
1919
1920Lowercases email addresses in database.
1921
1922=item C<--make_alias_file> [ C<--robot> robot ]
1923
1924Create an aliases file in /tmp/ with all list aliases. It uses the
1925F<list_aliases.tt2> template  (useful when list_aliases.tt2 was changed).
1926
1927=item C<--md5_encode_password>
1928
1929Rewrite password in C<user_table> of database using MD5 fingerprint.
1930YOU CAN'T UNDO unless you save this table first.
1931
1932B<Note> that this option was obsoleted.
1933Use L<upgrade_sympa_password(1)>.
1934
1935=item C<--modify_list=>I<family_name> C<--robot=>I<robot_name>
1936C<--input_file=>I</path/to/file.xml>
1937
1938Modify the existing list installed under the robot robot_name and that
1939belongs to the family family_name. The new description is in the C<file.xml>.
1940
1941=item C<--open_list=>I<list>[I<@robot>] [--notify]
1942
1943Restore the closed list (changing its status to open), add aliases and restore
1944users to DB (dump files in the list directory are imported).
1945
1946The C<--notify> is optional. If present, the owner(s) of the list will be notified.
1947
1948=item C<--purge_list>=I<list>[@I<robot>]
1949
1950Remove the list (remove archive, configuration files, users and owners in admin table. Restore is not possible after this operation.
1951
1952=item C<--show_pending_lists>=I<robot>
1953
1954Print all pending lists for the robot, with informations.
1955
1956=item C<--rebuildarc>=I<list>[I<@robot>]
1957
1958Rebuild the archives of the list.
1959
1960=item C<--reload_list_config>
1961[ C<--list=>I<mylist>@I<mydom> ] [ C<--robot=>I<mydom> ]
1962
1963Recreates all F<config.bin> files or cache in C<list_table>.
1964You should run this command if you edit authorization scenarios.
1965The list and robot parameters are optional.
1966
1967=item C<--rename_list=>I<listname>@I<robot>
1968C<--new_listname=>I<newlistname> C<--new_listrobot=>I<newrobot>
1969
1970Renames a list or move it to another virtual robot.
1971
1972=item C<--send_digest> [ C<--keep_digest> ]
1973
1974Send digest right now.
1975If C<--keep_digest> is specified, stocked digest will not be removed.
1976
1977=item C<--restore_users> C<--list=>I<list>@I<domain>|C<ALL> [ C<--role=>I<roles> ]
1978
1979Restore users from files dumped by C<--dump_users>.
1980
1981Note: This option was added on Sympa 6.2.34.
1982
1983=item C<--sync_include=>I<listname>@I<robot> [ C<--role=>I<role> ]
1984
1985Trigger update of the list users included from data sources.
1986
1987=item C<--sync_list_db> [ C<--list=>I<listname>@I<robot> ]
1988
1989Syncs filesystem list configs to the database cache of list configs,
1990optionally syncs an individual list if specified.
1991
1992=item C<--test_database_message_buffer>
1993
1994B<Note>:
1995This option was deprecated.
1996
1997Test the database message buffer size.
1998
1999=item C<--upgrade> [ C<--from=>I<X> ] [ C<--to=>I<Y> ]
2000
2001Runs Sympa maintenance script to upgrade from version I<X> to version I<Y>.
2002
2003=item C<--upgrade_shared> [ C<--list=>I<X> ] [ C<--robot=>I<Y> ]
2004
2005B<Note>:
2006This option was deprecated.
2007See upgrade_shared_repository(1).
2008
2009Rename files in shared.
2010
2011=back
2012
2013With following options F<sympa.pl> will print some information and exit.
2014
2015=over 4
2016
2017=item C<-h>, C<--help>
2018
2019Print this help message.
2020
2021=item C<--md5_digest=>I<password>
2022
2023Output a MD5 digest of a password (useful for SOAP client trusted
2024application).
2025
2026=item C<-v>, C<--version>
2027
2028Print the version number.
2029
2030=back
2031
2032=head1 FILES
2033
2034F<--CONFIG--> main configuration file.
2035
2036=head1 SEE ALSO
2037
2038L<sympa_toc(1)>.
2039
2040=head1 HISTORY
2041
2042This program was originally written by:
2043
2044=over 4
2045
2046=item Serge Aumont
2047
2048ComitE<233> RE<233>seau des UniversitE<233>s
2049
2050=item Olivier SalaE<252>n
2051
2052ComitE<233> RE<233>seau des UniversitE<233>s
2053
2054=back
2055
2056As of Sympa 6.2b.4, it was split into three programs:
2057F<sympa.pl> command line utility, F<sympa_automatic.pl> daemon and
2058F<sympa_msg.pl> daemon.
2059
2060=cut
2061