List.pm 311 KB
Newer Older
1
2
3
4
# -*- indent-tabs-mode: nil; -*-
# vim:ft=perl:et:sw=4
# $Id$

5
# Sympa - SYsteme de Multi-Postage Automatique
6
7
8
9
#
# Copyright (c) 1997, 1998, 1999 Institut Pasteur & Christophe Wolfhugel
# Copyright (c) 1997, 1998, 1999, 2000, 2001, 2002, 2003, 2004, 2005,
# 2006, 2007, 2008, 2009, 2010, 2011 Comite Reseau des Universites
10
# Copyright (c) 2011, 2012, 2013, 2014, 2015, 2016 GIP RENATER
11
12
13
14
15
16
17
18
19
20
21
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
22
23
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
root's avatar
root committed
24

25
package Sympa::List;
root's avatar
root committed
26
27

use strict;
28
use warnings;
29
use Digest::MD5 qw();
30
use English qw(-no_match_vars);
31
use File::Path qw();
32
use HTTP::Request;
33
use IO::Scalar;
34
use LWP::UserAgent;
sikeda's avatar
sikeda committed
35
use POSIX qw();
36
use Storable qw();
37

38
use Sympa;
39
use Conf;
40
use Sympa::ConfDef;
41
use Sympa::Constants;
42
use Sympa::Database;
43
use Sympa::DatabaseDescription;
44
use Sympa::DatabaseManager;
45
46
47
use Sympa::Datasource;
use Sympa::Family;
use Sympa::Fetch;
48
use Sympa::Language;
49
use Sympa::ListDef;
50
use Sympa::LockedFile;
51
use Sympa::Log;
52
use Sympa::Process;
53
use Sympa::Regexps;
54
55
use Sympa::Robot;
use Sympa::Scenario;
56
use Sympa::Spindle::ProcessTemplate;
57
use Sympa::Task;
58
use Sympa::Template;
59
use Sympa::Ticket;
60
61
use Sympa::Tools::Data;
use Sympa::Tools::File;
62
use Sympa::Tools::Password;
63
use Sympa::Tools::SMIME;
64
use Sympa::Tools::Text;
65
use Sympa::User;
66

67
my @sources_providing_listmembers = qw/
68
69
70
71
72
73
74
75
76
77
    include_file
    include_ldap_2level_query
    include_ldap_query
    include_list
    include_remote_file
    include_remote_sympa_list
    include_sql_query
    /;

#XXX include_admin
78
my @more_data_sources = qw/
79
80
    editor_include
    owner_include
81
    member_include
82
    /;
83
84

# All non-pluggable sources are in the admin user file
85
# NO LONGER USED.
86
87
my %config_in_admin_user_file = map +($_ => 1),
    @sources_providing_listmembers;
88

89
my $language = Sympa::Language->instance;
90
my $log      = Sympa::Log->instance;
91
92
93

=encoding utf-8

94
95
96
#=head1 NAME
#
#List - Mailing list
97

root's avatar
root committed
98
99
=head1 CONSTRUCTOR

100
101
=over

root's avatar
root committed
102
103
=item new( [PHRASE] )

104
 Sympa::List->new();
root's avatar
root committed
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132

Creates a new object which will be used for a list and
eventually loads the list if a name is given. Returns
a List object.

=back

=head1 METHODS

=over 4

=item load ( LIST )

Loads the indicated list into the object.

=item save ( LIST )

Saves the indicated list object to the disk files.

=item savestats ()

Saves updates the statistics file on disk.

=item update_stats( BYTES )

Updates the stats, argument is number of bytes, returns the next
sequence number. Does nothing if no stats.

133
134
This method was DEPRECATED.

135
=item delete_list_member ( ARRAY )
root's avatar
root committed
136
137

Delete the indicated users from the list.
138

139
=item delete_list_admin ( ROLE, ARRAY )
140
141
142

Delete the indicated admin user with the predefined role from the list.

root's avatar
root committed
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
=item get_cookie ()

Returns the cookie for a list, if available.

=item get_max_size ()

Returns the maximum allowed size for a message.

=item get_reply_to ()

Returns an array with the Reply-To values.

=item get_default_user_options ()

Returns a default option of the list for subscription.

=item get_total ()

Returns the number of subscribers to the list.

163
=item get_global_user ( USER )
root's avatar
root committed
164

165
Returns a hash with the information regarding the indicated
root's avatar
root committed
166
167
user.

168
=item get_list_member ( USER )
169
170

Returns a subscriber of the list.
david.verdin's avatar
david.verdin committed
171

172
=item get_list_admin ( ROLE, USER)
173
174
175

Return an admin user of the list with predefined role

176
177
178
OBSOLETED.
Use get_admins().

179
=item get_first_list_member ()
root's avatar
root committed
180
181
182

Returns a hash to the first user on the list.

183
=item get_first_list_admin ( ROLE )
184

185
186
OBSOLETED.
Use get_admins().
187

188
=item get_next_list_member ()
root's avatar
root committed
189
190
191
192

Returns a hash to the next users, until we reach the end of
the list.

193
=item get_next_list_admin ()
194

195
196
OBSOLETED.
Use get_admins().
197

sikeda's avatar
sikeda committed
198
=item update_list_member ( $email, key =E<gt> value, ... )
root's avatar
root committed
199

sikeda's avatar
sikeda committed
200
201
I<Instance method>.
Sets the new values given in the pairs for the user.
root's avatar
root committed
202

203
=item update_list_admin ( USER, ROLE, HASHPTR )
204
205
206

Sets the new values given in the hash for the admin user.

207
=item add_list_member ( USER, HASHPTR )
root's avatar
root committed
208
209
210
211

Adds a new user to the list. May overwrite existing
entries.

212
213
214
215
216
=item add_admin_user ( USER, ROLE, HASHPTR )

Adds a new admin user to the list. May overwrite existing
entries.

217
=item is_list_member ( USER )
root's avatar
root committed
218
219

Returns true if the indicated user is member of the list.
220

221
=item am_i ( ROLE, USER )
root's avatar
root committed
222

223
DEPRECATED. Use is_admin().
root's avatar
root committed
224
225
226
227
228
229
230

=item get_state ( FLAG )

Returns the value for a flag : sig or sub.

=item may_do ( ACTION, USER )

231
232
233
B<Note>:
This method was obsoleted.

root's avatar
root committed
234
235
236
237
238
239
240
241
242
243
Chcks is USER may do the ACTION for the list. ACTION can be
one of following : send, review, index, getm add, del,
reconfirm, purge.

=item is_moderated ()

Returns true if the list is moderated.

=item archive_exist ( FILE )

244
DEPRECATED.
root's avatar
root committed
245
246
247
248
Returns true if the indicated file exists.

=item archive_send ( WHO, FILE )

249
DEPRECATED.
root's avatar
root committed
250
251
252
253
Send the indicated archive file to the user, if it exists.

=item archive_ls ()

254
DEPRECATED.
root's avatar
root committed
255
256
257
258
Returns the list of available files, if any.

=item archive_msg ( MSG )

259
DEPRECATED.
root's avatar
root committed
260
261
262
263
264
265
266
Archives the Mail::Internet message given as argument.

=item is_archived ()

Returns true is the list is configured to keep archives of
its messages.

267
268
269
270
271
=item is_archiving_enabled ( )

Returns true is the list is configured to keep archives of
its messages, i.e. process_archive parameter is set to "on".

272
273
274
275
=item is_included ( )

Returns true value if the list is included in another list(s).

root's avatar
root committed
276
277
278
279
280
281
282
=item get_stats ( OPTION )

Returns either a formatted printable strings or an array whith
the statistics. OPTION can be 'text' or 'array'.

=item print_info ( FDNAME )

283
Print the list information to the given file descriptor, or the
root's avatar
root committed
284
285
currently selected descriptor.

286
287
=back

root's avatar
root committed
288
289
290
=cut

## Database and SQL statement handlers
291
my ($sth, @sth_stack);
292
293
294

my %list_cache;

sikeda's avatar
sikeda committed
295
296
297
298
299
300
301
302
303
304
305
306
# DB fields with numeric type.
# We should not do quote() for these while inserting data.
my %db_struct = Sympa::DatabaseDescription::full_db_struct();
my %numeric_field;
foreach my $t (qw(subscriber_table admin_table)) {
    foreach my $k (keys %{$db_struct{$t}->{fields}}) {
        if ($db_struct{$t}->{fields}{$k}{struct} =~ /\A(tiny|small|big)?int/)
        {
            $numeric_field{$k} = 1;
        }
    }
}
root's avatar
root committed
307

308
# This is the generic hash which keeps all lists in memory.
309
my %list_of_lists  = ();
salaun's avatar
salaun committed
310
my %list_of_robots = ();
salaun's avatar
salaun committed
311
312
my %edit_list_conf = ();

root's avatar
root committed
313
314
## Creates an object.
sub new {
315
316
    my ($pkg, $name, $robot, $options) = @_;
    my $list = {};
317
    $log->syslog('debug2', '(%s, %s, %s)', $name, $robot,
318
        join('/', keys %$options));
319

320
    $name = lc($name);
321
322
    ## Allow robot in the name
    if ($name =~ /\@/) {
323
324
325
        my @parts = split /\@/, $name;
        $robot ||= $parts[1];
        $name = $parts[0];
326
    }
root's avatar
root committed
327

328
329
330
331
332
    # Look for the list if no robot was provided.
    if (not $robot or $robot eq '*') {
        #FIXME: Default robot would be used instead of oppotunistic search.
        $robot = search_list_among_robots($name);
    }
333

334
    unless ($robot) {
335
        $log->syslog('err',
336
337
338
339
            'Missing robot parameter, cannot create list object for %s',
            $name)
            unless ($options->{'just_try'});
        return undef;
340
341
    }

342
343
    $options = {} unless (defined $options);

root's avatar
root committed
344
    ## Only process the list if the name is valid.
345
    my $listname_regexp = Sympa::Regexps::listname();
346
    unless ($name and ($name =~ /^($listname_regexp)$/io)) {
347
        $log->syslog('err', 'Incorrect listname "%s"', $name)
348
349
            unless ($options->{'just_try'});
        return undef;
root's avatar
root committed
350
351
    }
    ## Lowercase the list name.
352
    $name = $1;
root's avatar
root committed
353
    $name =~ tr/A-Z/a-z/;
354

355
    ## Reject listnames with reserved list suffixes
356
357
358
    my $regx = Conf::get_robot_conf($robot, 'list_check_regexp');
    if ($regx) {
        if ($name =~ /^(\S+)-($regx)$/) {
359
            $log->syslog(
360
361
362
363
364
365
                'err',
                'Incorrect name: listname "%s" matches one of service aliases',
                $name
            ) unless ($options->{'just_try'});
            return undef;
        }
366
367
    }

368
    my $status;
369
    ## If list already in memory and not previously purged by another process
370
371
372
373
374
375
376
377
378
379
380
381
382
    if ($list_of_lists{$robot}{$name}
        and -d $list_of_lists{$robot}{$name}{'dir'}) {
        # use the current list in memory and update it
        $list = $list_of_lists{$robot}{$name};

        $status = $list->load($name, $robot, $options);
    } else {
        # create a new object list
        bless $list, $pkg;

        $options->{'first_access'} = 1;
        $status = $list->load($name, $robot, $options);
    }
383
    unless (defined $status) {
384
        return undef;
385
386
387
    }

    ## Config file was loaded or reloaded
388
389
    my $pertinent_ttl = $list->{'admin'}{'distribution_ttl'}
        || $list->{'admin'}{'ttl'};
390
    if ($status
sikeda's avatar
sikeda committed
391
392
393
        && ((     !$options->{'skip_sync_admin'}
                && $list->{'last_sync'} < time - $pertinent_ttl
            )
394
            || $options->{'force_sync_admin'}
sikeda's avatar
sikeda committed
395
        )
396
397
398
        ) {
        ## Update admin_table
        unless (defined $list->sync_include_admin()) {
399
            $log->syslog('err', '')
400
401
                unless ($options->{'just_try'});
        }
402
403
        if (not @{$list->get_admins('owner') || []}
            and $list->{'admin'}{'status'} ne 'error_config') {
404
            $log->syslog('err', 'The list "%s" has got no owner defined',
405
                $list->{'name'});
406
            $list->set_status_error_config('no_owner_defined');
407
        }
root's avatar
root committed
408
409
    }

410
411
412
    return $list;
}

413
414
415
## When no robot is specified, look for a list among robots
sub search_list_among_robots {
    my $listname = shift;
416

417
    unless ($listname) {
418
        $log->syslog('err', 'Missing list parameter');
419
        return undef;
420
    }
421

422
    ## Search in default robot
423
424
    if (-d $Conf::Conf{'home'} . '/' . $listname) {
        return $Conf::Conf{'domain'};
425
    }
426
427
428
429
430
431
432
433

    foreach my $r (keys %{$Conf::Conf{'robots'}}) {
        if (-d $Conf::Conf{'home'} . '/' . $r . '/' . $listname) {
            return $r;
        }
    }

    return 0;
434
435
}

436
437
## set the list in status error_config and send a notify to listmaster
sub set_status_error_config {
438
    $log->syslog('debug2', '(%s, %s, ...)', @_);
439
    my ($self, $msg, @param) = @_;
440

441
442
    unless ($self->{'admin'}
        and $self->{'admin'}{'status'} eq 'error_config') {
443
444
        $self->{'admin'}{'status'} = 'error_config';

445
446
447
448
        # No more save config in error...
        # $self->save_config(tools::get_address($self->{'domain'},
        #     'listmaster'));
        # $self->savestats();
449
        $log->syslog('err',
450
451
            'The list %s is set in status error_config: %s(%s)',
            $self, $msg, join(', ', @param));
452
        Sympa::send_notify_to_listmaster($self, $msg,
453
            [$self->{'name'}, @param]);
454
455
456
457
458
    }
}

## set the list in status family_closed and send a notify to owners
sub set_status_family_closed {
459
460
461
462
    $log->syslog('debug2', '(%s, %s, %s)', @_);
    my $self    = shift;
    my $message = shift;    # 'close_list', 'purge_list': Currently unused.
    my @param   = @_;       # No longer used.
463
464

    unless ($self->{'admin'}{'status'} eq 'family_closed') {
465
466
467
        my $updater =
            Conf::get_robot_conf($self->{'domain'}, 'listmaster_email') . '@'
            . Conf::get_robot_conf($self->{'domain'}, 'host');
468

469
        unless ($self->close_list($updater, 'family_closed')) {
470
            $log->syslog('err',
471
472
473
                'Impossible to set the list %s in status family_closed');
            return undef;
        }
474
        $log->syslog('info', 'The list "%s" is set in status family_closed',
475
            $self->{'name'});
476
        $self->send_notify_to_owner('list_closed_family', {});
477
478
    }
    return 1;
root's avatar
root committed
479
480
481
482
}

## Saves the statistics data to disk.
sub savestats {
483
    $log->syslog('debug2', '(%s)', @_);
root's avatar
root committed
484
    my $self = shift;
485

486
487
488
    # Be sure the list has been loaded.
    my $dir = $self->{'dir'};
    return undef unless $list_of_lists{$self->{'domain'}}{$self->{'name'}};
489
490

    unless (ref($self->{'stats'}) eq 'ARRAY') {
491
        $log->syslog('err', 'Incorrect parameter %s', $self->{'stats'});
492
        return undef;
493
    }
494
495
496
497

    ## Lock file
    my $lock_fh = Sympa::LockedFile->new($dir . '/stats', 2, '>');
    unless ($lock_fh) {
498
        $log->syslog('err', 'Could not create new lock');
499
500
        return undef;
    }
olivier.salaun's avatar
olivier.salaun committed
501

502
    printf $lock_fh "%d %.0f %.0f %.0f %d %d %d\n",
503
504
        @{$self->{'stats'}}, $self->{'total'}, $self->{'last_sync'},
        $self->{'last_sync_admin_user'};
505

olivier.salaun's avatar
olivier.salaun committed
506
    ## Release the lock
507
    unless ($lock_fh->close) {
508
        return undef;
olivier.salaun's avatar
olivier.salaun committed
509
510
    }

root's avatar
root committed
511
    ## Changed on disk
512
    $self->{'_mtime'}{'stats'} = time;
root's avatar
root committed
513
514
515
516

    return 1;
}

517
## msg count.
518
519
# Old name: increment_msg_count().
sub _increment_msg_count {
520
    $log->syslog('debug2', '(%s)', @_);
521
    my $self = shift;
522

523
    # Be sure the list has been loaded.
524
    my $file = "$self->{'dir'}/msg_count";
525
526
527
528
529
530
531
532
533

    my %count;
    if (open(MSG_COUNT, $file)) {
        while (<MSG_COUNT>) {
            if ($_ =~ /^(\d+)\s(\d+)$/) {
                $count{$1} = $2;
            }
        }
        close MSG_COUNT;
534
535
536
    }
    my $today = int(time / 86400);
    if ($count{$today}) {
537
538
539
        $count{$today}++;
    } else {
        $count{$today} = 1;
540
    }
541

542
    unless (open(MSG_COUNT, ">$file.$PID")) {
543
        $log->syslog('err', 'Unable to create "%s.%s": %m', $file, $PID);
544
        return undef;
545
    }
546
547
    foreach my $key (sort { $a <=> $b } keys %count) {
        printf MSG_COUNT "%d\t%d\n", $key, $count{$key};
548
    }
549
550
    close MSG_COUNT;

551
    unless (rename("$file.$PID", $file)) {
552
        $log->syslog('err', 'Unable to write "%s": %m', $file);
553
        return undef;
554
555
556
557
    }
    return 1;
}

558
559
# Returns the number of messages sent to the list
sub get_msg_count {
560
    $log->syslog('debug2', '(%s)', @_);
561
562
    my $self = shift;

563
    # Be sure the list has been loaded.
564
    my $file = "$self->{'dir'}/stats";
565
566
567
568
569
570
571
572
573

    my $count = 0;
    if (open(MSG_COUNT, $file)) {
        while (<MSG_COUNT>) {
            if ($_ =~ /^(\d+)\s+(.*)$/) {
                $count = $1;
            }
        }
        close MSG_COUNT;
574
575
576
577
    }

    return $count;
}
578
579
## last date of distribution message .
sub get_latest_distribution_date {
580
    $log->syslog('debug2', '(%s)', @_);
581
    my $self = shift;
582

583
    # Be sure the list has been loaded.
584
    my $file = "$self->{'dir'}/msg_count";
585
586

    my $latest_date = 0;
salaun's avatar
salaun committed
587
    unless (open(MSG_COUNT, $file)) {
588
        $log->syslog('debug2', 'Unable to open %s', $file);
589
        return undef;
salaun's avatar
salaun committed
590
    }
591

592
593
594
595
    while (<MSG_COUNT>) {
        if ($_ =~ /^(\d+)\s(\d+)$/) {
            $latest_date = $1 if ($1 > $latest_date);
        }
596
    }
597
    close MSG_COUNT;
598

599
600
    return undef if ($latest_date == 0);
    return $latest_date;
601
602
}

603
## Update the stats struct
root's avatar
root committed
604
605
## Input  : num of bytes of msg
## Output : num of msgs sent
606
607
# Old name: List::update_stats().
sub get_next_sequence {
608
609
    $log->syslog('debug3', '(%s)', @_);
    my $self = shift;
610

root's avatar
root committed
611
612
    my $stats = $self->{'stats'};
    $stats->[0]++;
613
614

    ## Update 'msg_count' file, used for bounces management
615
    $self->_increment_msg_count();
616

root's avatar
root committed
617
618
619
    return $stats->[0];
}

620
# Old name: List::extract_verp_rcpt().
621
622
# Moved to: Sympa::Spindle::DistributeMessage::_extract_verp_rcpt().
#sub _extract_verp_rcpt;
623

root's avatar
root committed
624
625
## Dumps a copy of lists to disk, in text format
sub dump {
626
    my $self = shift;
627
    $log->syslog('debug2', '(%s)', $self->{'name'});
628

629
    unless (defined $self) {
630
        $log->syslog('err', 'Unknown list');
631
        return undef;
632
    }
633

634
    my $user_file_name = "$self->{'dir'}/subscribers.db.dump";
635

636
    unless ($self->_save_list_members_file($user_file_name)) {
637
        $log->syslog('err', 'Failed to save file %s', $user_file_name);
638
        return undef;
root's avatar
root committed
639
    }
640

641
    # Note: "subscribers" file was deprecated.
642
    $self->{'_mtime'} = {
643
644
        'config' => Sympa::Tools::File::get_mtime($self->{'dir'} . '/config'),
        'stats'  => Sympa::Tools::File::get_mtime($self->{'dir'} . '/stats'),
645
    };
646
647

    return 1;
root's avatar
root committed
648
649
650
651
652
}

## Saves the configuration file to disk
sub save_config {
    my ($self, $email) = @_;
653
    $log->syslog('debug3', '(%s, %s)', $self->{'name'}, $email);
root's avatar
root committed
654

655
656
    return undef
        unless ($self);
657
658
659
660

    my $config_file_name = "$self->{'dir'}/config";

    ## Lock file
661
662
    my $lock_fh = Sympa::LockedFile->new($config_file_name, 5, '+<');
    unless ($lock_fh) {
663
        $log->syslog('err', 'Could not create new lock');
664
        return undef;
665
666
    }

667
668
    my $name                 = $self->{'name'};
    my $old_serial           = $self->{'admin'}{'serial'};
salaun's avatar
salaun committed
669
    my $old_config_file_name = "$self->{'dir'}/config.$old_serial";
root's avatar
root committed
670
671
672

    ## Update management info
    $self->{'admin'}{'serial'}++;
673
    $self->{'admin'}{'update'} = {
674
675
676
677
678
679
        'email'      => $email,
        'date_epoch' => time,
        'date'       => $language->gettext_strftime(
            "%d %b %Y at %H:%M:%S",
            localtime time
        ),
680
    };
681

682
683
684
685
686
    unless (
        $self->_save_list_config_file(
            $config_file_name, $old_config_file_name
        )
        ) {
687
        $log->syslog('info', 'Unable to save config file %s',
688
689
690
            $config_file_name);
        $lock_fh->close();
        return undef;
root's avatar
root committed
691
    }
692

693
    ## Also update the binary version of the data structure
694
695
696
    if (Conf::get_robot_conf($self->{'domain'}, 'cache_list_config') eq
        'binary_file') {
        eval {
697
            Storable::store($self->{'admin'}, "$self->{'dir'}/config.bin");
698
699
        };
        if ($@) {
700
            $log->syslog('err',
701
702
703
                'Failed to save the binary config %s. error: %s',
                "$self->{'dir'}/config.bin", $@);
        }
704
705
    }

706
    ## Release the lock
707
    unless ($lock_fh->close()) {
708
        return undef;
709
710
    }

711
    unless ($self->_update_list_db) {
712
        $log->syslog('err', "Unable to update list_table");
713
714
    }

root's avatar
root committed
715
716
717
718
719
    return 1;
}

## Loads the administrative data for a list
sub load {
720
721
722
723
724
725
726
    $log->syslog('debug2', '(%s, %s, %s, ...)', @_);
    my $self    = shift;
    my $name    = shift;
    my $robot   = shift;
    my $options = shift;

    die 'bug in logic. Ask developer' unless $robot;
727

728
729
    ## Set of initializations ; only performed when the config is first loaded
    if ($options->{'first_access'}) {
730
731
732
733
734
        if ($robot && (-d "$Conf::Conf{'home'}/$robot")) {
            $self->{'dir'} = "$Conf::Conf{'home'}/$robot/$name";
        } elsif (lc($robot) eq lc($Conf::Conf{'domain'})) {
            $self->{'dir'} = "$Conf::Conf{'home'}/$name";
        } else {
735
            $log->syslog('err', 'No such robot (virtual domain) %s', $robot)
736
737
738
739
740
741
742
743
744
                unless ($options->{'just_try'});
            return undef;
        }

        $self->{'domain'} = $robot;

        # default list host is robot domain
        $self->{'admin'}{'host'} ||= $self->{'domain'};
        $self->{'name'} = $name;
745
    }
746

747
    unless ((-d $self->{'dir'}) && (-f "$self->{'dir'}/config")) {
748
        $log->syslog('debug2', 'Missing directory (%s) or config file for %s',
749
750
751
            $self->{'dir'}, $name)
            unless ($options->{'just_try'});
        return undef;
752
    }
salaun's avatar
salaun committed
753

754
755
756
757
758
759
760
761
762
763
764
    # Last modification of list config ($last_time_config) and stats
    # ($last_time_stats) on memory cache.
    # Note: "subscribers" file was deprecated.
    my ($last_time_config, $last_time_stats);
    if ($self->{'_mtime'}) {
        $last_time_config = $self->{'_mtime'}{'config'};
        $last_time_stats  = $self->{'_mtime'}{'stats'};
    } else {
        $last_time_config = POSIX::INT_MIN();
        $last_time_stats  = POSIX::INT_MIN();
    }
765

766
767
768
769
770
771
772
    my $time_config = Sympa::Tools::File::get_mtime("$self->{'dir'}/config");
    my $time_config_bin =
        Sympa::Tools::File::get_mtime("$self->{'dir'}/config.bin");
    my $time_stats = Sympa::Tools::File::get_mtime("$self->{'dir'}/stats");
    my $main_config_time =
        Sympa::Tools::File::get_mtime(Sympa::Constants::CONFIG);
    # my $web_config_time  = Sympa::Tools::File::get_mtime(Sympa::Constants::WWSCONFIG);
773
    my $config_reloaded = 0;
root's avatar
root committed
774
    my $admin;
775
776

    if (Conf::get_robot_conf($self->{'domain'}, 'cache_list_config') eq
777
778
            'binary_file'
        and !$options->{'reload_config'}
779
        and $time_config_bin > $last_time_config
780
781
        and $time_config_bin >= $time_config
        and $time_config_bin >= $main_config_time) {
782
783
784
785
        ## Get a shared lock on config file first
        my $lock_fh =
            Sympa::LockedFile->new($self->{'dir'} . '/config', 5, '<');
        unless ($lock_fh) {
786
            $log->syslog('err', 'Could not create new lock');
787
788
789
790
791
            return undef;
        }

        ## Load a binary version of the data structure
        ## unless config is more recent than config.bin
792
        eval { $admin = Storable::retrieve("$self->{'dir'}/config.bin") };
793
        if ($@) {
794
            $log->syslog('err',
795
796
797
798
799
800
                'Failed to load the binary config %s, error: %s',
                "$self->{'dir'}/config.bin", $@);
            $lock_fh->close();
            return undef;
        }

801
802
        $config_reloaded  = 1;
        $last_time_config = $time_config_bin;
803
        $lock_fh->close();
804
    } elsif ($self->{'name'} ne $name
805
        or $time_config > $last_time_config
806
        or $options->{'reload_config'}) {
807
        $admin = $self->_load_list_config_file;
808
809
810
811
812

        ## Get a shared lock on config file first
        my $lock_fh =
            Sympa::LockedFile->new($self->{'dir'} . '/config', 5, '+<');
        unless ($lock_fh) {
813
            $log->syslog('err', 'Could not create new lock');
814
815
816
817
818
819
            return undef;
        }

        ## update the binary version of the data structure
        if (Conf::get_robot_conf($self->{'domain'}, 'cache_list_config') eq
            'binary_file') {
820
            eval { Storable::store($admin, "$self->{'dir'}/config.bin") };
821
            if ($@) {
822
                $log->syslog('err',
823
824
825
826
827
828
829
                    'Failed to save the binary config %s. error: %s',
                    "$self->{'dir'}/config.bin", $@);
            }
        }

        $config_reloaded = 1;
        unless (defined $admin) {
830
            $log->syslog(
831
                'err',
sikeda's avatar
sikeda committed
832
833
                'Impossible to load list config file for list %s set in status error_config',
                $self
834
            );
835
            $self->set_status_error_config('load_admin_file_error');
836
837
838
839
            $lock_fh->close();
            return undef;
        }

840
        $last_time_config = $time_config;
841
        $lock_fh->close();
root's avatar
root committed
842
    }
843

844
845
    ## If config was reloaded...
    if ($admin) {
846
847
848
849
850
851
852
853
        $self->{'admin'} = $admin;

        ## check param_constraint.conf if belongs to a family and the config
        ## has been loaded
        if (defined $admin->{'family_name'}
            && ($admin->{'status'} ne 'error_config')) {
            my $family;
            unless ($family = $self->get_family()) {
854
                $log->syslog(
855
                    'err',
856
                    'Impossible to get list %s family: %s. The list is set in status error_config',
857
                    $self,
858
859
860
                    $self->{'admin'}{'family_name'}
                );
                $self->set_status_error_config('no_list_family',
861
                    $self->{'admin'}{'family_name'});
862
863
864
865
                return undef;
            }
            my $error = $family->check_param_constraint($self);
            unless ($error) {
866
                $log->syslog(
867
868
869
870
871
                    'err',
                    'Impossible to check parameters constraint for list % set in status error_config',
                    $self->{'name'}
                );
                $self->set_status_error_config('no_check_rules_family',
872
                    $family->{'name'});
873
874
            }
            if (ref($error) eq 'ARRAY') {
875
                $log->syslog(
876
877
878
879
880
881
                    'err',
                    'The list "%s" does not respect the rules from its family %s',
                    $self->{'name'},
                    $family->{'name'}
                );
                $self->set_status_error_config('no_respect_rules_family',
882
                    $family->{'name'});
883
884
885
886
887
888
889
890
891
            }
        }
    }

    $self->{'as_x509_cert'} = 1
        if ((-r "$self->{'dir'}/cert.pem")
        || (-r "$self->{'dir'}/cert.pem.enc"));

    ## Load stats file if first new() or stats file changed
892
    my ($stats, $total);
893
894
    my $stats_file = $self->{'dir'} . '/stats';
    if (!-e $stats_file or $time_stats > $last_time_stats) {
895
896
        (   $stats, $total, $self->{'last_sync'},
            $self->{'last_sync_admin_user'}
897
898
        ) = _load_stats_file($stats_file);
        $last_time_stats = $time_stats;
899

900
901
        $self->{'stats'} = $stats if (defined $stats);
        $self->{'total'} = $total if (defined $total);
902
    }
903

904
905
906
907
    $self->{'_mtime'} = {
        'config' => $last_time_config,
        'stats'  => $last_time_stats,
    };
root's avatar
root committed
908

909
    $list_of_lists{$self->{'domain'}}{$name} = $self;
910
    return $config_reloaded;
root's avatar
root committed
911
912
}

913
## Return a list of hash's owners and their param
914
#OBSOLETED.  Use get_admins().
915
sub get_owners {
916
917
    $log->syslog('debug3', '(%s)', @_);
    my $self = shift;
918

919
920
    # owners are in the admin_table ; they might come from an include data
    # source
921
    return [$self->get_admins('owner')];
922
923
}

924
# OBSOLETED: No longer used.
925
sub get_nb_owners {
926
927
    $log->syslog('debug3', '(%s)', @_);
    my $self = shift;
928

929
    return scalar @{$self->get_admins('owner')};
930
931
}

932
933
## Return a hash of list's editors and their param(empty if there isn't any
## editor)
934
#OBSOLETED. Use get_admins().
935
sub get_editors {
936
937
    $log->syslog('debug3', '(%s)', @_);
    my $self = shift;
938

939
940
    # editors are in the admin_table ; they might come from an include data
    # source
941
    return [$self->get_admins('editor')];
942
943
}

944
## Returns an array of owners' email addresses
945
946
#OBSOLETED: Use get_admins_email('receptive_owner') or
#           get_admins_email('owner').
salaun's avatar
salaun committed
947
sub get_owners_email {
948
949
950
    $log->syslog('debug3', '(%s, %s)', @_);
    my $self  = shift;
    my $param = shift;
951

952
    my @rcpt;
953

954
    if ($param->{'ignore_nomail'}) {
955
        @rcpt = map { $_->{'email'} } $self->get_admins('owner');
956
    } else {
957
        @rcpt = map { $_->{'email'} } $self->get_admins('receptive_owner');
958
959
    }
    unless (@rcpt) {
960
        $log->syslog('notice', 'Warning: No owner found for list %s', $self);
961
    }
962
963
964
    return @rcpt;
}

965
## Returns an array of editors' email addresses
966
#  or owners if there isn't any editors' email addresses
967
968
#OBSOLETED: Use get_admins_email('receptive_editor') or
#           get_admins_email('actual_editor').
969
sub get_editors_email {
970
971
972
    $log->syslog('debug3', '(%s, %s)', @_);
    my $self  = shift;
    my $param = shift;
973

974
    my @rcpt;
975

976
    if ($param->{'ignore_nomail'}) {
977
        @rcpt = map { $_->{'email'} } $self->get_admins('actual_editor');
978
    } else {
979
        @rcpt = map { $_->{'email'} } $self->get_admins('receptive_editor');
980
981
982
983
    }
    return @rcpt;
}

984
## Returns an object Sympa::Family if the list belongs to a family or undef
sympa-authors's avatar