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