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 2017, 2018 The Sympa Community. See the AUTHORS.md file at the
9# top-level directory of this distribution and at
10# <https://github.com/sympa-community/sympa.git>.
11#
12# This program is free software; you can redistribute it and/or modify
13# it under the terms of the GNU General Public License as published by
14# the Free Software Foundation; either version 2 of the License, or
15# (at your option) any later version.
16#
17# This program is distributed in the hope that it will be useful,
18# but WITHOUT ANY WARRANTY; without even the implied warranty of
19# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
20# GNU General Public License for more details.
21#
22# You should have received a copy of the GNU General Public License
23# along with this program.  If not, see <http://www.gnu.org/licenses/>.
24
25package Sympa::Aliases::Template;
26
27use strict;
28use warnings;
29use English qw(-no_match_vars);
30
31use Conf;
32use Sympa::Constants;
33use Sympa::Language;
34use Sympa::LockedFile;
35use Sympa::Log;
36use Sympa::Template;
37
38use base qw(Sympa::Aliases::CheckSMTP);
39
40my $language = Sympa::Language->instance;
41my $log      = Sympa::Log->instance;
42
43sub _aliases {
44    my $self = shift;
45    my $list = shift;
46
47    my $domain   = $list->{'domain'};
48    my $listname = $list->{'name'};
49
50    my $data = {
51        'date' => $language->gettext_strftime('%d %b %Y', localtime time),
52        'list' => {
53            'name'   => $listname,
54            'domain' => $domain,
55            # Compat. < 6.2.32
56            'host' => $domain,
57        },
58        'is_default_domain' => ($domain eq $Conf::Conf{'domain'}),
59        'return_path_suffix' =>
60            Conf::get_robot_conf($domain, 'return_path_suffix'),
61
62        # No longer used by default.
63        'robot'          => $domain,
64        'default_domain' => $Conf::Conf{'domain'},
65    };
66
67    my $aliases_dump;
68    my $template = Sympa::Template->new($domain);
69    unless ($template->parse($data, 'list_aliases.tt2', \$aliases_dump)) {
70        $log->syslog(
71            'err',
72            'Can\'t parse list_aliases.tt2: %s',
73            $template->{last_error}
74        );
75        return;
76    }
77
78    my @aliases = split /\n/, $aliases_dump;
79    unless (@aliases) {
80        $log->syslog('err', 'No aliases defined');
81        return;
82    }
83    return @aliases;
84}
85
86sub add {
87    my $self = shift;
88    my $list = shift;
89
90    return 0
91        if lc Conf::get_robot_conf($list->{'domain'}, 'sendmail_aliases') eq
92        'none';
93
94    my @aliases = $self->_aliases($list);
95    return undef unless @aliases;
96
97    my $alias_file =
98           $self->{file}
99        || Conf::get_robot_conf($list->{'domain'}, 'sendmail_aliases')
100        || Sympa::Constants::SENDMAIL_ALIASES();
101    # Create a lock
102    my $lock_fh;
103    unless ($lock_fh = Sympa::LockedFile->new($alias_file, 20, '+>>')) {
104        $log->syslog('err', 'Can\'t lock %s', $alias_file);
105        return undef;
106    }
107
108    # Check existing aliases
109    if (_already_defined($lock_fh, @aliases)) {
110        $log->syslog('err', 'Some alias already exist');
111        return undef;
112    }
113
114    # Append new entries.
115    unless (seek $lock_fh, 0, 2) {
116        $log->syslog('err', 'Unable to seek: %m');
117        return undef;
118    }
119    foreach (@aliases) {
120        print $lock_fh "$_\n";
121    }
122    $lock_fh->flush;
123
124    # Newaliases
125    unless ($self->{file}) {
126        system(alias_wrapper($list), '--domain=' . $list->{'domain'});
127        if ($CHILD_ERROR == -1) {
128            $log->syslog('err', 'Failed to execute sympa_newaliases: %m');
129            return undef;
130        } elsif ($CHILD_ERROR & 127) {
131            $log->syslog(
132                'err',
133                'sympa_newaliases was terminated by signal %d',
134                $CHILD_ERROR & 127
135            );
136            return undef;
137        } elsif ($CHILD_ERROR) {
138            $log->syslog(
139                'err',
140                'sympa_newaliases exited with status %d',
141                $CHILD_ERROR >> 8
142            );
143            return undef;
144        }
145    }
146
147    # Unlock
148    $lock_fh->close;
149
150    return 1;
151}
152
153sub del {
154    my $self = shift;
155    my $list = shift;
156
157    return 0
158        if lc Conf::get_robot_conf($list->{'domain'}, 'sendmail_aliases') eq
159        'none';
160
161    my @aliases = $self->_aliases($list);
162    return undef unless @aliases;
163
164    my $alias_file =
165           $self->{file}
166        || Conf::get_robot_conf($list->{'domain'}, 'sendmail_aliases')
167        || Sympa::Constants::SENDMAIL_ALIASES();
168    # Create a lock
169    my $lock_fh;
170    unless ($lock_fh = Sympa::LockedFile->new($alias_file, 20, '+<')) {
171        $log->syslog('err', 'Can\'t lock %s', $alias_file);
172        return undef;
173    }
174
175    # Check existing aliases.
176    my (@deleted_lines, @new_aliases);
177    my @to_be_deleted =
178        grep { defined $_ }
179        map { ($_ and m{^([^\s:]+)[\s:]}) ? $1 : undef } @aliases;
180    while (my $alias = <$lock_fh>) {
181        my $left_side = ($alias =~ /^([^\s:]+)[\s:]/) ? $1 : '';
182        if (grep { $left_side eq $_ } @to_be_deleted) {
183            push @deleted_lines, $alias;
184        } else {
185            push @new_aliases, $alias;
186        }
187    }
188
189    unless (@deleted_lines) {
190        $log->syslog('err', 'No matching line in %s', $alias_file);
191        return 0;
192    }
193
194    # Replace old aliases file.
195    unless (seek $lock_fh, 0, 0) {
196        $log->syslog('err', 'Could not seek: %m');
197        return undef;
198    }
199    print $lock_fh join '', @new_aliases;
200    $lock_fh->flush;
201    truncate $lock_fh, tell $lock_fh;
202
203    # Newaliases
204    unless ($self->{file}) {
205        system(alias_wrapper($list), '--domain=' . $list->{'domain'});
206        if ($CHILD_ERROR == -1) {
207            $log->syslog('err', 'Failed to execute sympa_newaliases: %m');
208            return undef;
209        } elsif ($CHILD_ERROR & 127) {
210            $log->syslog(
211                'err',
212                'sympa_newaliases was terminated by signal %d',
213                $CHILD_ERROR & 127
214            );
215            return undef;
216        } elsif ($CHILD_ERROR) {
217            $log->syslog(
218                'err',
219                'sympa_newaliases exited with status %d',
220                $CHILD_ERROR >> 8
221            );
222            return undef;
223        }
224    }
225
226    # Unlock
227    $lock_fh->close;
228
229    return 1;
230}
231
232sub alias_wrapper {
233    my $list = shift;
234    my $command;
235
236    if (Conf::get_robot_conf($list->{'domain'}, 'aliases_wrapper') eq 'on'
237        and -e Sympa::Constants::LIBEXECDIR . '/sympa_newaliases-wrapper') {
238        return Sympa::Constants::LIBEXECDIR . '/sympa_newaliases-wrapper';
239    }
240
241    return Sympa::Constants::SBINDIR . '/sympa_newaliases.pl';
242}
243
244# Check if an alias is already defined.
245# Old name: already_defined() in alias_manager.pl.
246sub _already_defined {
247    my $fh      = shift;
248    my @aliases = @_;
249
250    unless (seek $fh, 0, 0) {
251        $log->syslog('err', 'Could not seek: %m');
252        return undef;
253    }
254
255    my $ret = 0;
256    while (my $alias = <$fh>) {
257        # skip comment
258        next if $alias =~ /^#/;
259        $alias =~ /^([^\s:]+)[\s:]/;
260        my $left_side = $1;
261        next unless ($left_side);
262        foreach (@aliases) {
263            next unless ($_ =~ /^([^\s:]+)[\s:]/);
264            my $new_left_side = $1;
265            if ($left_side eq $new_left_side) {
266                $log->syslog('info', 'Alias already defined: %s', $left_side);
267                $ret++;
268            }
269        }
270    }
271
272    return $ret;
273}
274
2751;
276__END__
277
278=encoding utf-8
279
280=head1 NAME
281
282Sympa::Aliases::Template -
283Alias management: Aliases file based on template
284
285=head1 SYNOPSIS
286
287  use Sympa::Aliases;
288  my $aliases = Sympa::Aliases->new('Template',
289      [ file => '/path/to/file' ] );
290  $aliases->check('listname', 'domain');
291  $aliases->add($list);
292  $aliases->del($list);
293
294=head1 DESCRIPTION
295
296L<Sympa::Aliases::Template> manages list aliases based on template
297F<list_aliases.tt2>.
298
299=head2 Methods
300
301=over
302
303=item check ( $listname, $domain )
304
305See L<Sympa::Aliases::CheckSMTP>.
306
307=item add ( $list )
308
309=item del ( $list )
310
311Adds or removes aliases of list $list.
312
313If constructor was called with C<file> option, it will be used as aliases
314file and F<sympa_newaliases> utility will not be executed.
315Otherwise, value of C<sendmail_aliases> parameter will be used as aliases
316file and F<sympa_newaliases> utility will be executed to update
317alias database.
318If C<sendmail_aliases> parameter is set to C<none>, aliases will never be
319updated.
320
321=back
322
323=head2 Configuration parameters
324
325=over
326
327=item return_path_suffix
328
329Suffix of list return address.
330
331=item sendmail_aliases
332
333Path of the file that contains all list related aliases.
334
335=item tmpdir
336
337A directory temporary files are placed.
338
339=back
340
341=head1 FILES
342
343=over
344
345=item F<$SYSCONFDIR/I<domain name>/list_aliases.tt2>
346
347=item F<$SYSCONFDIR/list_aliases.tt2>
348
349=item F<$DEFAULTDIR/list_aliases.tt2>
350
351Template of aliases: Specific to a domain, global context and the default.
352
353=item F<$SENDMAIL_ALIASES>
354
355Default location of aliases file.
356
357=item F<$SBINDIR/sympa_newaliases>
358
359Auxiliary program to update alias database.
360
361=back
362
363=head1 SEE ALSO
364
365L<Sympa::Aliases>,
366L<Sympa::Aliases::CheckSMTP>,
367L<sympa_newaliases(1)>.
368
369=head1 HISTORY
370
371F<alias_manager.pl> to manage aliases using template appeared on
372Sympa 3.1b.13.
373
374L<Sympa::Aliases::Template> module appeared on Sympa 6.2.23b,
375and it obsoleted F<alias_manager(8)>.
376
377=cut
378