Spool.pm 24.8 KB
Newer Older
1
2
3
4
5
6
7
8
9
# -*- indent-tabs-mode: nil; -*-
# vim:ft=perl:et:sw=4
# $Id$

# Sympa - SYsteme de Multi-Postage Automatique
#
# 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, 2017 GIP RENATER
11
12
13
# Copyright 2017 The Sympa Community. See the AUTHORS.md file at the top-level
# directory of this distribution and at
# <https://github.com/sympa-community/sympa.git>.
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#
# 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.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

package Sympa::Spool;

use strict;
use warnings;
32
use Cwd qw();
33
34
35
36
37
38
use Digest::MD5;
use English qw(-no_match_vars);
use POSIX qw();
use Sys::Hostname qw();
use Time::HiRes qw();

39
use Sympa;
40
use Conf;
41
use Sympa::Constants;
42
43
44
use Sympa::List;
use Sympa::LockedFile;
use Sympa::Log;
45
use Sympa::Tools::File;
46
47
48

my $log = Sympa::Log->instance;

49
50
51
# Methods.

sub new {
52
53
    my $class   = shift;
    my %options = @_;
54
55
56

    die $EVAL_ERROR unless eval sprintf 'require %s', $class->_generator;

57
58
59
60
61
    my $self = bless {
        %options,
        %{$class->_directories(%options) || {}},
        _metadatas => undef,
    } => $class;
62
63

    $self->_create;
64
    $self->_init(0) or return undef;
65
66
67
68
69
70
71
72
73
74
75
76

    $self;
}

sub _create {
    my $self = shift;

    my $umask = umask oct $Conf::Conf{'umask'};
    foreach my $directory (sort values %{$self->_directories}) {
        unless (-d $directory) {
            $log->syslog('info', 'Creating directory %s of %s',
                $directory, $self);
77
78
79
            unless (mkdir $directory, 0775 or -d $directory) {
                die sprintf 'Cannot create %s: %s', $directory, $ERRNO;
            }
80
            unless (
81
                Sympa::Tools::File::set_file_rights(
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
                    file  => $directory,
                    user  => Sympa::Constants::USER(),
                    group => Sympa::Constants::GROUP()
                )
                ) {
                die sprintf 'Cannot create %s: %s', $directory, $ERRNO;
            }
        }
    }
    umask $umask;
}

sub _init {1}

sub next {
97
98
    my $self    = shift;
    my %options = @_;
99
100
101
102
103
104
105
106

    return unless $self->{directory};

    unless ($self->{_metadatas}) {
        $self->{_metadatas} = $self->_load;
    }
    unless ($self->{_metadatas} and @{$self->{_metadatas}}) {
        undef $self->{_metadatas};
107
        $self->_init(1);
108
109
110
111
112
113
114
115
        return;
    }

    while (my $marshalled = shift @{$self->{_metadatas}}) {
        my ($handle, $metadata, $message);

        # Try locking message.  Those locked or removed by other process will
        # be skipped.
116
117
118
119
120
121
122
123
124
125
        if ($options{no_lock}) {
            next
                unless open $handle, '<',
                $self->{directory} . '/' . $marshalled;
        } else {
            $handle =
                Sympa::LockedFile->new($self->{directory} . '/' . $marshalled,
                -1, $self->_is_collection ? '+' : '+<');
            next unless $handle;
        }
126
127
128
129
130
131
132
133
134

        $metadata = Sympa::Spool::unmarshal_metadata(
            $self->{directory},     $marshalled,
            $self->_marshal_regexp, $self->_marshal_keys
        );

        if ($metadata) {
            next unless $self->_filter($metadata);

135
136
137
138
139
140
            if ($self->_is_collection) {
                $message = $self->_generator->new(%$metadata);
            } else {
                my $msg_string = do { local $RS; <$handle> };
                $message = $self->_generator->new($msg_string, %$metadata);
            }
141
142
143
        }

        # Though message might not be deserialized, anyway return the result.
144
145
146
147
148
149
        if ($options{no_lock}) {
            close $handle;
            return ($message, 1);
        } else {
            return ($message, $handle);
        }
150
151
152
153
    }
    return;
}

154
155
sub _filter {1}

156
157
158
sub _load {
    my $self = shift;

159
160
161
162
163
164
165
166
167
168
169
170
171
    my @entries;
    if ($self->_glob_pattern) {
        my $cwd = Cwd::getcwd();
        die sprintf 'Cannot chdir to %s: %s', $self->{directory}, $ERRNO
            unless chdir $self->{directory};
        @entries = glob $self->_glob_pattern;
        chdir $cwd;
    } else {
        my $dh;
        die sprintf 'Cannot open dir %s: %s', $self->{directory}, $ERRNO
            unless opendir $dh, $self->{directory};
        @entries = readdir $dh;
        closedir $dh;
172
    }
173
174

    my $iscol     = $self->_is_collection;
175
176
177
178
    my $metadatas = [
        sort grep {
                    !/,lock/
                and !m{(?:\A|/)(?:\.|T\.|BAD-)}
179
180
                and ((not $iscol and -f ($self->{directory} . '/' . $_))
                or ($iscol and -d ($self->{directory} . '/' . $_)))
181
        } @entries
182
183
184
185
186
    ];

    return $metadatas;
}

187
sub _glob_pattern {undef}
188

189
190
sub _is_collection {0}

191
192
193
194
195
sub quarantine {
    my $self   = shift;
    my $handle = shift;

    return undef unless $self->{bad_directory};
196
    die 'bug in logic. Ask developer' unless ref $handle;
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212

    my $bad_file;

    $bad_file = $self->{bad_directory} . '/' . $handle->basename;
    unless (-d $self->{bad_directory} and $handle->rename($bad_file)) {
        $bad_file = $self->{directory} . '/BAD-' . $handle->basename;
        return undef unless $handle->rename($bad_file);
    }

    return 1;
}

sub remove {
    my $self   = shift;
    my $handle = shift;

213
    die 'bug in logic.  Ask developer' unless ref $handle;
214

215
216
217
218
219
220
221
    if ($self->_is_collection) {
        return undef
            unless rmdir($self->{directory} . '/' . $handle->basename);
        return $handle->close;
    } else {
        return $handle->unlink;
    }
222
223
}

224
225
226
227
sub size {
    scalar @{shift->_load || []};
}

228
229
230
231
232
sub store {
    my $self    = shift;
    my $message = shift->dup;
    my %options = @_;

233
234
    return if $self->_is_collection;

235
236
237
238
    $message->{date} = time unless defined $message->{date};

    my $marshalled =
        Sympa::Spool::store_spool($self->{directory}, $message,
239
240
        $self->_marshal_format, $self->_marshal_keys, %options,
        _filter_pre => sub { $self->_filter_pre(shift) },);
241
242
243
244
    return unless $marshalled;

    $log->syslog('notice', '%s is stored into %s as <%s>',
        $message, $self, $marshalled);
245
246
247
248
249
250
251
252

    if ($self->_store_key) {
        my $metadata = Sympa::Spool::unmarshal_metadata(
            $self->{directory},     $marshalled,
            $self->_marshal_regexp, $self->_marshal_keys
        );
        return $metadata ? $metadata->{$self->_store_key} : undef;
    }
253
254
255
    return $marshalled;
}

256
257
sub _filter_pre {1}

258
259
sub _store_key {undef}

260
261
# Low-level functions.

262
263
264
265
266
267
268
269
270
sub build_glob_pattern {
    my $format  = shift;
    my $keys    = shift;
    my %options = @_;

    if (exists $options{context}) {
        my $context = $options{context};
        if (ref $context eq 'Sympa::List') {
            @options{qw(localpart domainpart)} =
271
                split /\@/, Sympa::get_address($context);
272
273
274
275
276
        } else {
            $options{domainpart} = $context;
        }
    }

277
    $format =~ s/(%%|%[-#+.\d ]*[l]*\w)/$1 eq '%%' ? '%%' : '%s'/eg;
278
279
280
281
    my @args =
        map {
        if (exists $options{$_} and defined $options{$_}) {
            my $val = $options{$_};
282
            $val =~ s/([^0-9A-Za-z\x80-\xFF])/\\$1/g;
283
284
285
286
287
288
289
290
291
292
            $val;
        } else {
            '*';
        }
        } map {
        lc $_
        } @{$keys || []};
    my $pattern = sprintf $format, @args;
    $pattern =~ s/[*][*]+/*/g;

293
294
295
296
297
    # Eliminate patterns contains only punctuations:
    # ',', '.', '_', wildcard etc.
    return ($pattern =~ /[0-9A-Za-z\x80-\xFF]|\\[^0-9A-Za-z\x80-\xFF]/)
        ? $pattern
        : undef;
298
299
}

300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
sub split_listname {
    my $robot_id = shift || '*';
    my $mailbox = shift;
    return unless defined $mailbox and length $mailbox;

    my $return_path_suffix =
        Conf::get_robot_conf($robot_id, 'return_path_suffix');
    my $regexp = join(
        '|',
        map { quotemeta $_ }
            grep { $_ and length $_ }
            split(
            /[\s,]+/, Conf::get_robot_conf($robot_id, 'list_check_suffixes')
            )
    );

    if (    $mailbox eq 'sympa'
        and $robot_id eq $Conf::Conf{'domain'}) {    # compat.
        return (undef, 'sympa');
    } elsif ($mailbox eq Conf::get_robot_conf($robot_id, 'email')
        or $robot_id eq $Conf::Conf{'domain'}
        and $mailbox eq $Conf::Conf{'email'}) {
        return (undef, 'sympa');
    } elsif ($mailbox eq Conf::get_robot_conf($robot_id, 'listmaster_email')
        or $robot_id eq $Conf::Conf{'domain'}
        and $mailbox eq $Conf::Conf{'listmaster_email'}) {
        return (undef, 'listmaster');
    } elsif ($mailbox =~ /^(\S+)$return_path_suffix$/) {    # -owner
        return ($1, 'return_path');
    } elsif (!$regexp) {
        return ($mailbox);
    } elsif ($mailbox =~ /^(\S+)-($regexp)$/) {
        my ($name, $suffix) = ($1, $2);
        my $type;

        if ($suffix eq 'request') {                         # -request
            $type = 'owner';
        } elsif ($suffix eq 'editor') {
            $type = 'editor';
        } elsif ($suffix eq 'subscribe') {
            $type = 'subscribe';
        } elsif ($suffix eq 'unsubscribe') {
            $type = 'unsubscribe';
        } else {
            $name = $mailbox;
            $type = 'UNKNOWN';
        }
        return ($name, $type);
    } else {
        return ($mailbox);
    }
}

# Old name: SympaspoolClassic::analyze_file_name().
sub unmarshal_metadata {
    $log->syslog('debug3', '(%s, %s, %s)', @_);
356
357
    my $spool_dir      = shift;
    my $marshalled     = shift;
358
359
    my $marshal_regexp = shift;
    my $marshal_keys   = shift;
360
361
362

    my $data;
    my @matches;
363
    unless (@matches = ($marshalled =~ /$marshal_regexp/)) {
364
365
        $log->syslog('debug',
            'File name %s does not have the proper format: %s',
366
            $marshalled, $marshal_regexp);
367
368
369
370
371
372
        return undef;
    }
    $data = {
        messagekey => $marshalled,
        map {
            my $value = shift @matches;
373
            (defined $value and length $value) ? (lc($_) => $value) : ();
374
        } @{$marshal_keys}
375
376
377
378
379
380
    };

    my ($robot_id, $listname, $type, $list, $priority);

    $robot_id = lc($data->{'domainpart'})
        if defined $data->{'domainpart'}
381
382
        and length $data->{'domainpart'}
        and Conf::valid_robot($data->{'domainpart'}, {just_try => 1});
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
    ($listname, $type) =
        Sympa::Spool::split_listname($robot_id || '*', $data->{'localpart'});

    $list = Sympa::List->new($listname, $robot_id || '*', {'just_try' => 1})
        if defined $listname;

    ## Get priority
    #FIXME: is this always needed?
    if (exists $data->{'priority'}) {
        # Priority was given by metadata.
        ;
    } elsif ($type and $type eq 'listmaster') {
        ## highest priority
        $priority = 0;
    } elsif ($type and $type eq 'owner') {    # -request
        $priority = Conf::get_robot_conf($robot_id, 'request_priority');
    } elsif ($type and $type eq 'return_path') {    # -owner
        $priority = Conf::get_robot_conf($robot_id, 'owner_priority');
    } elsif ($type and $type eq 'sympa') {
        $priority = Conf::get_robot_conf($robot_id, 'sympa_priority');
    } elsif (ref $list eq 'Sympa::List') {
        $priority = $list->{'admin'}{'priority'};
    } else {
        $priority = Conf::get_robot_conf($robot_id, 'default_list_priority');
    }

    $data->{context} = $list || $robot_id || '*';
    $data->{'listname'} = $listname if $listname;
    $data->{'listtype'} = $type     if defined $type;
    $data->{'priority'} = $priority if defined $priority;

    $log->syslog('debug3', 'messagekey=%s, context=%s, priority=%s',
        $marshalled, $data->{context}, $data->{'priority'});

    return $data;
}

sub marshal_metadata {
421
    my $message        = shift;
422
423
    my $marshal_format = shift;
    my $marshal_keys   = shift;
424
425
426
427
428
429

    #FIXME: Currently only "sympa@DOMAIN" and "LISTNAME(-TYPE)@DOMAIN" are
    # supported.
    my ($localpart, $domainpart);
    if (ref $message->{context} eq 'Sympa::List') {
        ($localpart) = split /\@/,
430
            Sympa::get_address($message->{context}, $message->{listtype});
431
432
433
434
435
436
437
438
439
440
441
442
443
444
        $domainpart = $message->{context}->{'domain'};
    } else {
        my $robot_id = $message->{context} || '*';
        $localpart  = Conf::get_robot_conf($robot_id, 'email');
        $domainpart = Conf::get_robot_conf($robot_id, 'domain');
    }

    my @args = map {
        if ($_ eq 'localpart') {
            $localpart;
        } elsif ($_ eq 'domainpart') {
            $domainpart;
        } elsif ($_ eq 'AUTHKEY') {
            Digest::MD5::md5_hex(time . (int rand 46656) . $domainpart);
445
446
447
448
449
450
        } elsif ($_ eq 'KEYAUTH') {
            substr
                Digest::MD5::md5_hex(time . (int rand 46656) . $domainpart),
                0, 16;
        } elsif ($_ eq 'PID') {
            $PID;
451
452
453
454
455
456
457
458
459
460
461
        } elsif ($_ eq 'RAND') {
            int rand 10000;
        } elsif ($_ eq 'TIME') {
            Time::HiRes::time();
        } elsif (exists $message->{$_}
            and defined $message->{$_}
            and !ref($message->{$_})) {
            $message->{$_};
        } else {
            '';
        }
462
    } @{$marshal_keys};
463
464
465
466

    # Set "C" locale so that decimal point for "%f" will be ".".
    my $locale_numeric = POSIX::setlocale(POSIX::LC_NUMERIC());
    POSIX::setlocale(POSIX::LC_NUMERIC(), 'C');
467
    my $marshalled = sprintf $marshal_format, @args;
468
469
470
471
472
    POSIX::setlocale(POSIX::LC_NUMERIC(), $locale_numeric);
    return $marshalled;
}

sub store_spool {
473
474
    my $spool_dir      = shift;
    my $message        = shift;
475
476
    my $marshal_format = shift;
    my $marshal_keys   = shift;
477
    my %options        = @_;
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493

    # At first content is stored into temporary file that has unique name and
    # is referred only by this function.
    my $tmppath = sprintf '%s/T.sympa@_tempfile.%s.%ld.%ld',
        $spool_dir, Sys::Hostname::hostname(), time, $PID;
    my $fh;
    unless (open $fh, '>', $tmppath) {
        die sprintf 'Cannot create %s: %s', $tmppath, $ERRNO;
    }
    print $fh $message->to_string(original => $options{original});
    close $fh;

    # Rename temporary path to the file name including metadata.
    # Will retry up to five times.
    my $tries;
    for ($tries = 0; $tries < 5; $tries++) {
494
495
496
497
498
        my $metadata = {%$message};
        if (ref $options{_filter_pre} eq 'CODE') {
            next unless $options{_filter_pre}->($metadata);
        }

sikeda's avatar
sikeda committed
499
        my $marshalled =
500
            Sympa::Spool::marshal_metadata($metadata, $marshal_format,
501
            $marshal_keys);
502
        next unless defined $marshalled and length $marshalled;
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
        my $path = $spool_dir . '/' . $marshalled;

        my $lock;
        unless ($lock = Sympa::LockedFile->new($path, -1, '+')) {
            next;
        }
        if (-e $path) {
            $lock->close;
            next;
        }

        unless (rename $tmppath, $path) {
            die sprintf 'Cannot create %s: %s', $path, $ERRNO;
        }
        $lock->close;

        # Set mtime to be {date} in metadata of the message.
        my $mtime =
              defined $message->{date} ? $message->{date}
            : defined $message->{time} ? $message->{time}
            :                            time;
        utime $mtime, $mtime, $path;

        return $marshalled;
    }

    unlink $tmppath;
    return undef;
}

1;
__END__

=encoding utf-8

=head1 NAME

540
Sympa::Spool - Base class of spool classes
541
542
543

=head1 SYNOPSIS

544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
  package Sympa::Spool::FOO;
  
  use base qw(Sympa::Spool);
  
  sub _directories {
      return {
          directory     => '/path/to/spool',
          bad_directory => '/path/to/spool/bad',
      };
  }
  use constant _generator      => 'Sympa::Message';
  use constant _marshal_format => '%s@%s.%ld.%ld,%d';
  use constant _marshal_keys   => [qw(localpart domainpart date PID RAND)];
  use constant _marshal_regexp =>
      qr{\A([^\s\@]+)(?:\@([\w\.\-]+))?\.(\d+)\.(\w+)(?:,.*)?\z};
  
  1;
561
562
563

=head1 DESCRIPTION

564
565
566
567
568
569
570
571
572
573
574
This module is the base class for spool subclasses of Sympa.

=head2 Public methods

=over

=item new ( [ options... ] )

I<Constructor>.
Creates new instance of the class.

575
=item next ( [ no_lock =E<gt> 1 ] )
576
577

I<Instance method>.
578
Gets next message to process, order is controlled by name of spool file and
579
580
581
582
583
so on.
Message will be locked to prevent multiple proccessing of a single message.

Parameters:

584
585
586
587
588
589
590
=over

=item no_lock =E<gt> 1

Won't lock messages.

=back
591
592
593
594
595

Returns:

Two-elements list of message instance and filehandle locking
a message.
596
597
598
599
600
If parsing message fails, list of C<undef> and filehandle.
If no more message found, empty array.

If C<no_lock> is set,
true scalar value will be returned in place of filehandle.
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623

=item quarantine ( $handle )

I<Instance method>.
Quarantines a message.
On filesystem spool,
message will be moved into C<{bad_directory}> of the spool using rename().

Parameter:

=over

=item $handle

Filehandle, L<Sympa::LockedFile> instance, locking message.

=back

Returns:

True value if message could be quarantined.
Otherwise false value.

624
625
If $handle was not a filehandle, this method will die.

626
627
628
629
630
631
=item remove ( $handle )

I<Instance method>.
Removes a message.

Parameter:
632

633
634
635
636
637
638
639
640
641
642
643
644
645
=over

=item $handle

Filehandle, L<Sympa::LockedFile> instance, locking message.

=back

Returns:

True value if message could be removed.
Otherwise false value.

646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
If $handle was not a filehandle, this method will die.

=item size ( )

I<Instance method>.
Gets the number of messages the spool contains.

Parameters:

None.

Returns:

Number of messages.

Note:
This method returns the number of messages _load() returns,
not applying _filter().

665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
=item store ( $message, [ original =E<gt> $original ] )

I<Instance method>.
Stores the message into spool.

Parameters:

=over

=item $message

Message to be stored.

=item original =E<gt> $original

If true value is specified and $message was decrypted,
Stores original encrypted form.

=back

Returns:
686

687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
If storing succeeded, marshalled metadata (file name) of the message.
Otherwise C<undef>.

=back

=head2 Properties

Instance of L<Sympa::Spool> may have following properties.

=over

=item Directories

Directories _directories() method returns:
C<{directory}>, C<{bad_directory}> and so on.

=back
704
705
706
707
708

=head2 Low level functions

=over

709
710
711
712
713
714
715
716
=item build_glob_pattern ( $marshal_format, $marshal_keys,
[ key =E<gt> value, ... ] )

I<Function>.
Builds a glob pattern from parameters and returns it.
If built pattern is empty or contains only punctuations,
i.e. C<[^0-9A-Za-z\x80-\xFF]>, will return C<undef>.

717
=item split_listname ( $robot, $localpart )
718
719

I<Function>.
720
721
722
723
724
725
726
727
Split local part of e-mail to list name and type.
Returns an array C<(name, type)>.
Note that the list with returned name may or may not exist.

If local part looks like listmaster or sympa address, name is C<undef> and
type is either C<'listmaster'> or C<'sympa'>.
Otherwise, type is either C<'editor'>, C<'owner'>, C<'return_path'>,
C<'subscribe'>, C<'unsubscribe'>, C<'UNKNOWN'> or C<undef>.
728
729
730

Note:
For C<-request> and C<-owner> suffix, this function returns
731
C<owner> and C<return_path> types, respectively.
732

733
734
735
736
737
738
739
=item store_spool ( $spool_dir, $message, $marshal_format, $marshal_keys,
[ key => value, ... ] )

I<Function>.
Store $message into directory $spool_dir as a file with name as
marshalled metadata using $marshal_format and $marshal_keys.

740
=item unmarshal_metadata ( $spool_dir, $marshalled,
741
$marshal_regexp, $marshal_keys )
742
743

I<Function>.
744
745
746
747
748
749
750
751
752
Unmarshals metadata.
Returns hashref with keys in arrayref $marshal_keys
and values with substrings captured by regexp $marshal_regexp.
If $marshalled did not match against $marshal_regexp,
returns C<undef>.

The keys C<localpart> and C<domainpart> are special.
Following keys are derived from them:
C<context>, C<listname>, C<listtype>, C<priority>.
753

754
=item marshal_metadata ( $message, $marshal_format, $marshal_keys )
755
756

I<Function>.
757
758
759
760
761
Marshals metadata.
Returns formatted string by sprintf() using $marshal_format
and metadatas indexed by keys in arrayref $marshal_keys.

If key is uppercase, it means auto-generated value:
762
C<'AUTHKEY'>, C<'KEYAUTH'>, C<'PID'>, C<'RAND'>, C<'TIME'>.
763
764
765
766
Otherwise it means metadata or property of $message.

sprintf() is executed under C<C> locale:
Full stop (C<.>) is always used for decimal point in floating point number.
767

768
769
770
771
772
773
774
775
776
777
778
779
=back

=head2 Methods subclass should implement

=over

=item _create ( )

I<Instance method>, I<overridable>.
Creates spool.
By default, creates directories returned by _directories().

780
=item _directories ( [ options, ... ] )
781
782
783
784
785
786
787
788
789

I<Class or instance method>, I<mandatory for filesystem spool>.
Returns hashref with directory paths related to the spool as values.
It must have keys at least C<directory> and
(if you wish to implement quarantine() method) C<bad_directory>.

=item _filter ( $metadata )

I<Instance method>, I<overridable>.
790
If it returned false value, processing of $metadata by next() will be skipped.
791
792
By default, always returns true value.

793
794
795
796
797
798
799
800
801
802
803
804
805
This method may modify unmarshalled metadata _and_ deserialized messages
include it.

=item _filter_pre ( $metadata )

I<Instance method>, I<overridable>.
If it returned false value, processing of $metadata by store() will be
skipped.
By default, always returns true value.

This method may modify marshalled metadata _but_ stored messages are not
affected.

806
807
808
809
=item _generator ( )

I<Class or instance method>, I<mandatory>.
Returns name of the class to serialize and deserialize messages in the spool.
810
811
812
813
If spool subclass is the collection (see _is_collection),
generator class must implement new().
Otherwise,
generator class must implement dup(), new() and to_string().
814

815
816
817
818
819
820
821
=item _glob_pattern ( )

I<Instance method>.
If implemented and returns non-empty string,
glob() is used to search entries in the spool.
Otherwise readdir() is used for filesystem spool to get all entries.

822
=item _init ( $state )
823
824

I<Instance method>.
825
826
827
828
829
830
831
832
833
834
835
Additional processing when _load() returns no contents ($state is 1) or
when the spool class is instantiated ($state is 0).

=item _is_collection ( )

I<Instance method>, I<overridable>.
If the class is collection of spool class, returns true value.
By default returns false value.

Collection class does not have store() method.
Its content is the set of spool instances.
836
837
838
839
840
841
842
843
844
845
846
847
848
849

=item _load ( )

I<Instance method>, I<overridable>.
Loads sorted content of spool.
Returns arrayref of marshalled metadatas.
By default, returns content of C<{directory}> directory sorted by file name.

=item _marshal_format ( )

=item _marshal_keys ( )

=item _marshal_regexp ( )

850
I<Instance methods>, I<mandatory for filesystem spool>.
851
852
853
854
_marshal_format() and _marshal_keys() are used to marshal metadata.
_marshal_keys() and _marshal_regexp() are used to unmarshal metadata.
See also marshal_metadata() and unmarshal_metadata().

855
856
857
858
859
860
861
862
=item _store_key ( )

I<Instance method>.
If implemented and returns non-empty string,
store() returns an item in metadata specified by this method.
By default store() returns marshalled metadata
(file name on filesystem spool).

863
864
=back

sikeda's avatar
sikeda committed
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
=head2 Marshaling and unmarshaling metadata

Spool class gives generator class the B<metadata> to instantiate it.
On spool based on filesystem, it is typically encoded into file names.
For example a file name in incoming spool (L<Sympa::Spool::Incoming>)

  listname-owner@domain.name.143599229.12345

encodes the metadata

  localpart  => 'listname-owner',
  listname   => 'listname',
  listtype   => 'return_path',
  domainpart => 'domain.name',
  date       => 143599229,

Metadata always includes information of B<context>: List, Robot or
Site.  For example:

- Message in incoming spool bound for E<lt>listname@domain.nameE<gt>:

  context    => Sympa::List <listname@domain.name>,

- Command message in incoming spool bound for E<lt>sympa@domain.nameE<gt>:

  context    => 'domain.name',

- Message sent from Sympa to super-listmaster(s):

  context    => '*'

Context is determined when the generator class is instantiated, and
generally never changed through lifetime of instance.
Thus, constructor of generator class should take context object as an
argument.

901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
=head1 CONFIGURATION PARAMETERS

Following site configuration parameters in sympa.conf will be referred.

=over

=item default_list_priority

=item email

=item owner_priority

=item list_check_suffix

=item listmaster_email

=item request_priority

=item return_path_suffix

=item sympa_priority

Used to extract metadata from marshalled data (file name).

=item umask

The umask to make directories of spool.
928
929
930
931
932
933
934

=back

=head1 SEE ALSO

L<Sympa::Message>, especially L<Serialization|Sympa::Message/"Serialization">.

935
936
937
938
939
=head1 HISTORY

L<Sympa::Spool> appeared on Sympa 6.2.
It as the base class appeared on Sympa 6.2.6.

940
build_glob_pattern(), size(), _glob_pattern() and _store_key()
941
942
were introduced on Sympa 6.2.8.
_filter_pre() was introduced on Sympa 6.2.10.
943

944
=cut