1# -*- indent-tabs-mode: nil; -*-
2# vim:ft=perl:et:sw=4
3# $Id$
4
5# Sympa - SYsteme de Multi-Postage Automatique
6#
7# Copyright (c) 1997, 1998, 1999 Institut Pasteur & Christophe Wolfhugel
8# Copyright (c) 1997, 1998, 1999, 2000, 2001, 2002, 2003, 2004, 2005,
9# 2006, 2007, 2008, 2009, 2010, 2011 Comite Reseau des Universites
10# Copyright (c) 2011, 2012, 2013, 2014, 2015, 2016, 2017 GIP RENATER
11# Copyright 2018, 2019 The Sympa Community. See the AUTHORS.md file at
12# the top-level directory of this distribution and at
13# <https://github.com/sympa-community/sympa.git>.
14#
15# This program is free software; you can redistribute it and/or modify
16# it under the terms of the GNU General Public License as published by
17# the Free Software Foundation; either version 2 of the License, or
18# (at your option) any later version.
19#
20# This program is distributed in the hope that it will be useful,
21# but WITHOUT ANY WARRANTY; without even the implied warranty of
22# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
23# GNU General Public License for more details.
24#
25# You should have received a copy of the GNU General Public License
26# along with this program.  If not, see <http://www.gnu.org/licenses/>.
27
28package Sympa::Spindle::AuthorizeMessage;
29
30use strict;
31use warnings;
32
33use Sympa;
34use Sympa::List;
35use Sympa::Log;
36use Sympa::Scenario;
37use Sympa::Spool::Topic;
38use Sympa::Tools::Data;
39
40use base qw(Sympa::Spindle);
41
42my $log = Sympa::Log->instance;
43
44# Old name: (part of) DoMessage() in sympa_msg.pl.
45sub _twist {
46    my $self    = shift;
47    my $message = shift;
48
49    my $list      = $message->{context};
50    my $messageid = $message->{message_id};
51
52    # Now check if the sender is an authorized address.
53    my $sender = $self->{confirmed_by} || $message->{sender};
54
55    my $context = {
56        'sender'  => $sender,
57        'message' => $message
58    };
59
60    # List msg topic.
61    if (not $self->{confirmed_by}    # Not in ProcessHeld spindle.
62        and $list->is_there_msg_topic
63    ) {
64        my $topic;
65        if ($topic = Sympa::Spool::Topic->load($message)) {
66            # Is message already tagged?
67            ;
68        } elsif ($topic =
69            Sympa::Spool::Topic->load($message, in_reply_to => 1)) {
70            # Is message in-reply-to already tagged?
71            $topic = Sympa::Spool::Topic->new(
72                topic  => $topic->{topic},
73                method => 'auto'
74            );
75            $topic->store($message);
76        } elsif (my $topic_list = $message->compute_topic) {
77            # Not already tagged.
78            $topic = Sympa::Spool::Topic->new(
79                topic  => $topic_list,
80                method => 'auto'
81            );
82            $topic->store($message);
83        }
84
85        if ($topic) {
86            $context->{'topic'} = $context->{'topic_' . $topic->{method}} =
87                $topic->{topic};
88        }
89        $context->{'topic_needed'} =
90            (!$context->{'topic'} && $list->is_msg_topic_tagging_required);
91    }
92
93    # Call scenario: auth_method MD5 do not have any sense in "send"
94    # scenario because auth is performed by distribute or reject command.
95
96    my $action;
97    my $result;
98
99    # The order of the following 3 lines is important! SMIME > DKIM > SMTP.
100    my $auth_method =
101          $message->{'smime_signed'} ? 'smime'
102        : $message->{'md5_check'}    ? 'md5'
103        : $message->{'dkim_pass'}    ? 'dkim'
104        :                              'smtp';
105
106    $result =
107        Sympa::Scenario->new($list, 'send')->authz($auth_method, $context);
108    $action = $result->{'action'} if (ref($result) eq 'HASH');
109
110    unless (defined $action) {
111        $log->syslog(
112            'err',
113            'Message %s ignored because unable to evaluate scenario "send" for list %s',
114            $message,
115            $list
116        );
117        Sympa::send_notify_to_listmaster(
118            $list,
119            'mail_intern_error',
120            {   error =>
121                    'Message ignored because scenario "send" cannot be evaluated',
122                who    => $sender,
123                msg_id => $messageid,
124            }
125        );
126        Sympa::send_dsn($list, $message, {}, '5.3.0');
127        $log->db_log(
128            'robot'        => $list->{'domain'},
129            'list'         => $list->{'name'},
130            'action'       => 'DoMessage',
131            'parameters'   => $message->get_id,
132            'target_email' => '',
133            'msg_id'       => $messageid,
134            'status'       => 'error',
135            'error_type'   => 'internal',
136            'user_email'   => $sender
137        );
138        return undef;
139    }
140
141    # Message topic context.
142    if ($action =~ /^do_it\b/ and $context->{'topic_needed'}) {
143        if ($list->{'admin'}{'msg_topic_tagging'} eq 'required_sender') {
144            $action = 'request_auth';
145        } elsif (
146            $list->{'admin'}{'msg_topic_tagging'} eq 'required_moderator') {
147            $action = 'editorkey';
148        }
149    }
150
151    # Check TT2 syntax for personalization feature.
152    if ($action !~ /\Areject\b/
153        and not $self->{confirmed_by}    # Not in ProcessHeld spindle.
154        and $message->{shelved}{merge}
155        and $message->{shelved}{merge} ne 'footer'    # 'all' or '1'(<=6.2.58)
156        and not _test_personalize($message, $list)
157    ) {
158        $log->syslog('err',
159            'Failed to personalize. Message %s for list %s was rejected',
160            $message, $list);
161        Sympa::send_dsn($list, $message, {}, '5.6.5');
162        return undef;
163    }
164
165    if ($action =~ /^do_it\b/) {
166        $self->{quiet} ||= ($action =~ /,\s*quiet\b/);    # Overwrite.
167
168        unless ($self->{confirmed_by}) {    # Not in ProcessHeld spindle.
169            $message->{shelved}{dkim_sign} = 1
170                if Sympa::Tools::Data::is_in_array(
171                $list->{'admin'}{'dkim_signature_apply_on'}, 'any')
172                or (
173                Sympa::Tools::Data::is_in_array(
174                    $list->{'admin'}{'dkim_signature_apply_on'},
175                    'smime_authenticated_messages')
176                and $message->{'smime_signed'}
177                )
178                or (
179                Sympa::Tools::Data::is_in_array(
180                    $list->{'admin'}{'dkim_signature_apply_on'},
181                    'dkim_authenticated_messages')
182                and $message->{'dkim_pass'}
183                );
184        } else {
185            $message->add_header('X-Validation-by', $self->{confirmed_by});
186
187            $message->{shelved}{dkim_sign} = 1
188                if Sympa::Tools::Data::is_in_array(
189                $list->{'admin'}{'dkim_signature_apply_on'}, 'any')
190                or Sympa::Tools::Data::is_in_array(
191                $list->{'admin'}{'dkim_signature_apply_on'},
192                'md5_authenticated_messages');
193        }
194
195        # Keep track of known message IDs...if any.
196        $self->{_msgid}{$list->get_id}{$messageid} = time
197            unless $self->{confirmed_by};
198
199        return ['Sympa::Spindle::DistributeMessage'];
200    } elsif (
201        not $self->{confirmed_by}    # Not in ProcessHeld spindle.
202        and $action =~ /^request_auth\b/
203    ) {
204        return ['Sympa::Spindle::ToHeld'];
205    } elsif ($action =~ /^editorkey\b/) {
206        $self->{quiet} ||= ($action =~ /,\s*quiet\b/);    # Overwrite
207
208        return ['Sympa::Spindle::ToModeration'];
209    } elsif ($action =~ /^editor\b/) {
210        $self->{quiet} ||= ($action =~ /,\s*quiet\b/);    # Overwrite
211
212        return ['Sympa::Spindle::ToEditor'];
213    } elsif ($action =~ /^reject\b/) {
214        my $quiet = $self->{quiet} || ($action =~ /,\s*quiet\b/);
215
216        $log->syslog(
217            'notice',
218            'Message %s for %s from %s rejected(%s) because sender not allowed',
219            $message,
220            $list,
221            $sender,
222            $result->{'tt2'}
223        );
224
225        # Do not report to the sender if the message was tagged as a spam.
226        unless ($quiet or $message->{'spam_status'} eq 'spam') {
227            if (defined $result->{'tt2'}) {
228                unless (
229                    Sympa::send_file(
230                        $list, $result->{'tt2'},
231                        $sender, {auto_submitted => 'auto-replied'}
232                    )
233                ) {
234                    $log->syslog('notice',
235                        'Unable to send template "%s" to %s',
236                        $result->{'tt2'}, $sender);
237                    Sympa::send_dsn($list, $message,
238                        {reason => $result->{'reason'}}, '5.7.1');
239                }
240            } else {
241                Sympa::send_dsn($list, $message,
242                    {reason => $result->{'reason'}}, '5.7.1');
243            }
244        }
245        $log->db_log(
246            'robot'        => $list->{'domain'},
247            'list'         => $list->{'name'},
248            'action'       => 'DoMessage',
249            'parameters'   => $message->get_id,
250            'target_email' => '',
251            'msg_id'       => $messageid,
252            'status'       => 'error',
253            'error_type'   => 'rejected_authorization',
254            'user_email'   => $sender
255        );
256        return undef;
257    } else {
258        $log->syslog('err',
259            'Unknown action %s returned by the scenario "send"', $action);
260        Sympa::send_notify_to_listmaster(
261            $list,
262            'mail_intern_error',
263            {   error  => 'Unknown action returned by the scenario "send"',
264                who    => $sender,
265                msg_id => $messageid,
266            }
267        );
268        Sympa::send_dsn($list, $message, {}, '5.3.0');
269        $log->db_log(
270            'robot'        => $list->{'domain'},
271            'list'         => $list->{'name'},
272            'action'       => 'DoMessage',
273            'parameters'   => $message->get_id,
274            'target_email' => '',
275            'msg_id'       => $messageid,
276            'status'       => 'error',
277            'error_type'   => 'internal',
278            'user_email'   => $sender
279        );
280        return undef;
281    }
282}
283
284# Private subroutine.
285
286# Tests if personalization can be performed successfully over all subscribers
287# of list.
288# Returns: 1 if succeed, or undef.
289# Old name: Sympa::Message::test_personalize().
290sub _test_personalize {
291    my $message = shift;
292    my $list    = shift;
293
294    # Get available recipients to test.
295    my $available_recipients = $list->get_recipients_per_mode($message) || {};
296    # Always test all available reception modes using sender.
297    foreach my $mode ('mail',
298        grep { $_ and $_ ne 'nomail' and $_ ne 'not_me' }
299        @{$list->{'admin'}{'available_user_options'}->{'reception'} || []}) {
300        push @{$available_recipients->{$mode}{'verp'}}, $message->{'sender'};
301    }
302
303    foreach my $mode (sort keys %$available_recipients) {
304        my $new_message = $message->dup;
305        $new_message->prepare_message_according_to_mode($mode, $list);
306
307        foreach my $rcpt (
308            @{$available_recipients->{$mode}{'verp'}   || []},
309            @{$available_recipients->{$mode}{'noverp'} || []}
310        ) {
311            unless ($new_message->personalize($list, $rcpt)) {
312                return undef;
313            }
314        }
315    }
316    return 1;
317}
318
3191;
320__END__
321
322=encoding utf-8
323
324=head1 NAME
325
326Sympa::Spindle::AuthorizeMessage -
327Workflow to authorize messages bound for lists
328
329=head1 DESCRIPTION
330
331L<Sympa::Spindle::AuthorizeMessage> authorizes messages and stores them
332into confirmation spool, moderation spool or the lists.
333
334=over
335
336=item *
337
338Messages fetched from incoming (C<msg>) spool or held (C<auth>) spool may be
339passed to this class (messages fetched from moderation spool won't be passed
340to this class).
341
342=item *
343
344Then this class checks the message with C<send> scenario.
345
346=item *
347
348According to the results of scenario processing, each message is passed
349to any of classes for succeeding processing:
350L<Sympa::Spindle::DistributeMessage> for C<do_it> (except if tagging topics
351is required or when personalization failed);
352L<Sympa::Spindle::ToHeld> for C<request_auth> (except if personalization
353failed);
354L<Sympa::Spindle::ToModeration> for C<editorkey> (except if personalization
355failed);
356L<Sympa::Spindle::ToEditor> for C<editor>;
357otherwise reject it.
358
359=back
360
361If the message was confirmed, i.e. it has been fetched from held spool and
362at last decided to be distributed, C<X-Validation-By> header field is added.
363If the message at last will be distributed, C<{shelved}> attribute (see
364L<Sympa::Message>) is added as necessity.
365
366=head2 Public methods
367
368See also L<Sympa::Spindle/"Public methods">.
369
370=over
371
372=item new ( key =E<gt> value, ... )
373
374In most cases, L<Sympa::Spindle::DoMessage>
375splices messages to this class.  This method is not used in ordinal case.
376
377=item spin ( )
378
379Not implemented.
380
381=back
382
383=head1 SEE ALSO
384
385L<Sympa::Internals::Workflow>.
386
387L<Sympa::Message>, L<Sympa::Scenario>, L<Sympa::Spindle::DistributeMessage>,
388L<Sympa::Spindle::DoMessage>, L<Sympa::Spindle::ProcessHeld>,
389L<Sympa::Spindle::ToEditor>, L<Sympa::Spindle::ToHeld>,
390L<Sympa::Spindle::ToModeration>,
391L<Sympa::Spool::Topic>.
392
393=head1 HISTORY
394
395L<Sympa::Spindle::AuthorizeMessage> appeared on Sympa 6.2.13.
396
397=cut
398