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 2017, 2019, 2020 The Sympa Community. See the AUTHORS.md 12# file at 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::ProcessAutomatic; 29 30use strict; 31use warnings; 32use English qw(-no_match_vars); 33use File::Copy qw(); 34 35use Sympa; 36use Conf; 37use Sympa::Family; 38use Sympa::List; 39use Sympa::Log; 40use Sympa::Mailer; 41use Sympa::Spindle::ProcessRequest; 42use Sympa::Spool::Incoming; 43use Sympa::Spool::Listmaster; 44use Sympa::Tools::Data; 45 46use base qw(Sympa::Spindle); 47 48my $log = Sympa::Log->instance; 49 50use constant _distaff => 'Sympa::Spool::Automatic'; 51 52sub _init { 53 my $self = shift; 54 my $state = shift; 55 56 if ($state == 1) { 57 # Process grouped notifications. 58 Sympa::Spool::Listmaster->instance->flush; 59 } 60 61 1; 62} 63 64sub _on_success { 65 my $self = shift; 66 my $message = shift; 67 my $handle = shift; 68 69 if ($self->{keepcopy}) { 70 unless ( 71 File::Copy::copy( 72 $self->{distaff}->{directory} . '/' . $handle->basename, 73 $self->{keepcopy} . '/' . $handle->basename 74 ) 75 ) { 76 $log->syslog( 77 'notice', 78 'Could not rename %s/%s to %s/%s: %m', 79 $self->{distaff}->{directory}, 80 $handle->basename, 81 $self->{keepcopy}, 82 $handle->basename 83 ); 84 } 85 } 86 87 $self->SUPER::_on_success($message, $handle); 88} 89 90# Old name: process_message() in sympa_automatic.pl. 91sub _twist { 92 my $self = shift; 93 my $message = shift; 94 95 unless (defined $message->{'message_id'} 96 and length $message->{'message_id'}) { 97 $log->syslog('err', 'Message %s has no message ID', $message); 98 $log->db_log( 99 #'robot' => $robot, 100 #'list' => $listname, 101 'action' => 'process_message', 102 'parameters' => $message->get_id, 103 'target_email' => "", 104 'msg_id' => "", 105 'status' => 'error', 106 'error_type' => 'no_message_id', 107 'user_email' => $message->{'sender'} 108 ); 109 return undef; 110 } 111 112 my $msg_id = $message->{message_id}; 113 114 $log->syslog( 115 'notice', 116 'Processing %s; envelope_sender=%s; message_id=%s; sender=%s', 117 $message, 118 $message->{envelope_sender}, 119 $message->{message_id}, 120 $message->{sender} 121 ); 122 123 my $robot; 124 my $listname; 125 126 $robot = 127 (ref $message->{context} eq 'Sympa::List') 128 ? $message->{context}->{'domain'} 129 : $message->{context}; 130 $listname = $message->{'listname'}; 131 132 ## Ignoring messages with no sender 133 my $sender = $message->{'sender'}; 134 unless ($message->{'md5_check'} or $sender) { 135 $log->syslog('err', 'No sender found in message %s', $message); 136 $log->db_log( 137 'robot' => $robot, 138 'list' => $listname, 139 'action' => 'process_message', 140 'parameters' => "", 141 'target_email' => "", 142 'msg_id' => $msg_id, 143 'status' => 'error', 144 'error_type' => 'no_sender', 145 'user_email' => $sender 146 ); 147 return undef; 148 } 149 150 # Unknown robot. 151 unless ($message->{'md5_check'} or Conf::valid_robot($robot)) { 152 $log->syslog('err', 'Robot %s does not exist', $robot); 153 Sympa::send_dsn('*', $message, {}, '5.1.2'); 154 $log->db_log( 155 'robot' => $robot, 156 'list' => $listname, 157 'action' => 'process_message', 158 'parameters' => "", 159 'target_email' => "", 160 'msg_id' => $msg_id, 161 'status' => 'error', 162 'error_type' => 'unknown_robot', 163 'user_email' => $sender 164 ); 165 return undef; 166 } 167 168 # Load spam status. 169 $message->check_spam_status; 170 # Check DKIM signatures. 171 $message->check_dkim_signature; 172 # Check S/MIME signature. 173 $message->check_smime_signature; 174 # Decrypt message. On success, check nested S/MIME signature. 175 if ($message->smime_decrypt and not $message->{'smime_signed'}) { 176 $message->check_smime_signature; 177 } 178 179 # *** Now message content may be altered. *** 180 181 # Enable SMTP logging if required. 182 Sympa::Mailer->instance->{log_smtp} = $self->{log_smtp} 183 || Sympa::Tools::Data::smart_eq( 184 Conf::get_robot_conf($robot, 'log_smtp'), 'on'); 185 # setting log_level using conf unless it is set by calling option 186 $log->{level} = 187 (defined $self->{log_level}) 188 ? $self->{log_level} 189 : Conf::get_robot_conf($robot, 'log_level'); 190 191 ## Strip of the initial X-Sympa-To and X-Sympa-Checksum internal headers 192 delete $message->{'rcpt'}; 193 delete $message->{'checksum'}; 194 195 my $list = 196 (ref $message->{context} eq 'Sympa::List') 197 ? $message->{context} 198 : undef; 199 200 # Maybe we are an automatic list 201 #_amr ici on ne doit prendre que la première ligne ! 202 my ($dyn_list_family, $dyn_just_created); 203 # we care of fake headers. If we put it, it's the 1st one. 204 $dyn_list_family = $message->{'family'}; 205 206 unless (defined $dyn_list_family and length $dyn_list_family) { 207 $log->syslog( 208 'err', 209 'Internal server error: Automatic lists creation daemon should never proceed message %s without X-Sympa-Family header', 210 $message 211 ); 212 Sympa::send_notify_to_listmaster( 213 '*', 214 'intern_error', 215 { 'error' => 216 sprintf( 217 'Internal server error: Automatic lists creation daemon should never proceed message %s without X-Sympa-Family header', 218 $message) 219 } 220 ); 221 return undef; 222 } 223 delete $message->{'family'}; 224 225 unless (ref $list eq 'Sympa::List') { 226 ## Automatic creation of a mailing list, based on a family 227 my $dyn_family; 228 unless ($dyn_family = Sympa::Family->new($dyn_list_family, $robot)) { 229 $log->syslog( 230 'err', 231 'Failed to process message %s: family %s does not exist, impossible to create the dynamic list', 232 $message, 233 $dyn_list_family 234 ); 235 Sympa::send_notify_to_listmaster( 236 $robot, 237 'automatic_list_creation_failed', 238 { 'family' => $dyn_list_family, 239 'robot' => $robot, 240 'msg_id' => $msg_id, 241 } 242 ); 243 Sympa::send_dsn($robot, $message, {}, '5.3.5'); 244 return undef; 245 } 246 247 my $spindle_req = Sympa::Spindle::ProcessRequest->new( 248 context => $dyn_family, 249 action => 'create_automatic_list', 250 parameters => {listname => $listname}, 251 sender => $sender, 252 smime_signed => $message->{'smime_signed'}, 253 md5_check => $message->{'md5_check'}, 254 dkim_pass => $message->{'dkim_pass'}, 255 scenario_context => { 256 sender => $sender, 257 message => $message, 258 family => $dyn_family, 259 automatic_listname => $listname, 260 }, 261 ); 262 unless ($spindle_req and $spindle_req->spin) { 263 $log->syslog('err', 'Cannot create dynamic list %s', $listname); 264 return undef; 265 } elsif ( 266 not($spindle_req->success 267 and $list = Sympa::List->new( 268 $listname, 269 $dyn_family->{'domain'}, 270 {just_try => 1} 271 ) 272 ) 273 ) { 274 $log->syslog('err', 275 'Unable to create list %s. Message %s ignored', 276 $listname, $message); 277 Sympa::send_notify_to_listmaster( 278 $dyn_family->{'domain'}, 279 'automatic_list_creation_failed', 280 { 'listname' => $listname, 281 'family' => $dyn_list_family, 282 'robot' => $robot, 283 'msg_id' => $msg_id, 284 } 285 ); 286 Sympa::send_dsn($robot, $message, {}, '5.3.5'); 287 $log->db_log( 288 'robot' => $dyn_family->{'domain'}, 289 'list' => $listname, 290 'action' => 'process_message', 291 'parameters' => $msg_id . "," . $dyn_family->{'domain'}, 292 'target_email' => '', 293 'msg_id' => $msg_id, 294 'status' => 'error', 295 'error_type' => 'internal', 296 'user_email' => $sender 297 ); 298 return undef; 299 } else { 300 # Overwrite context of the message. 301 $message->{context} = $list; 302 $dyn_just_created = 1; 303 } 304 } 305 306 if ($dyn_just_created) { 307 unless (defined $list->sync_include('member')) { 308 $log->syslog( 309 'err', 310 'Failed to synchronize list members of dynamic list %s from %s family', 311 $list, 312 $dyn_list_family 313 ); 314 # As list may be purged, use robot context. 315 Sympa::send_dsn($robot, $message, {}, '4.2.1'); 316 $log->db_log( 317 'robot' => $robot, 318 'list' => $list->{'name'}, 319 'action' => 'process_message', 320 'parameters' => "", 321 'target_email' => "", 322 'msg_id' => $msg_id, 323 'status' => 'error', 324 'error_type' => 'dyn_cant_sync', 325 'user_email' => $sender 326 ); 327 # purge the unwanted empty automatic list 328 if ($Conf::Conf{'automatic_list_removal'} =~ /if_empty/i) { 329 Sympa::Spindle::ProcessRequest->new( 330 context => $robot, 331 action => 'close_list', 332 current_list => $list, 333 mode => 'purge', 334 scenario_context => {skip => 1}, 335 )->spin; 336 } 337 return undef; 338 } 339 unless ($list->get_total) { 340 $log->syslog('err', 341 'Dynamic list %s from %s family has ZERO subscribers', 342 $list, $dyn_list_family); 343 # As list may be purged, use robot context. 344 Sympa::send_dsn($robot, $message, {}, '4.2.4'); 345 $log->db_log( 346 'robot' => $robot, 347 'list' => $list->{'name'}, 348 'action' => 'process_message', 349 'parameters' => "", 350 'target_email' => "", 351 'msg_id' => $msg_id, 352 'status' => 'error', 353 'error_type' => 'list_unknown', 354 'user_email' => $sender 355 ); 356 # purge the unwanted empty automatic list 357 if ($Conf::Conf{'automatic_list_removal'} =~ /if_empty/i) { 358 Sympa::Spindle::ProcessRequest->new( 359 context => $robot, 360 action => 'close_list', 361 current_list => $list, 362 mode => 'purge', 363 scenario_context => {skip => 1}, 364 )->spin; 365 } 366 return undef; 367 } 368 $log->syslog('info', 369 'Successfully create list %s with %s subscribers', 370 $list, $list->get_total()); 371 } 372 373 # Do not process messages in list creation. Move them to main spool. 374 my $marshalled = 375 Sympa::Spool::Incoming->new->store($message, original => 1); 376 if ($marshalled) { 377 $log->syslog('notice', 378 'Message %s is stored into incoming spool as <%s>', 379 $message, $marshalled); 380 } else { 381 $log->syslog( 382 'err', 383 'Unable to move in spool for processing message %s to list %s (daemon_usage = creation)', 384 $message, 385 $list 386 ); 387 Sympa::send_notify_to_listmaster($list, 'mail_intern_error', 388 {error => '', who => $sender, msg_id => $msg_id,}); 389 Sympa::send_dsn($list, $message, {}, '5.3.0'); 390 return undef; 391 } 392 393 return 1; 394} 395 3961; 397__END__ 398 399=encoding utf-8 400 401=head1 NAME 402 403Sympa::Spindle::ProcessAutomatic - Workflow of automatic list creation 404 405=head1 SYNOPSIS 406 407 use Sympa::Spindle::ProcessAutomatic; 408 409 my $spindle = Sympa::Spindle::ProcessAutomatic->new; 410 $spindle->spin; 411 412=head1 DESCRIPTION 413 414L<Sympa::Spindle::ProcessAutomatic> defines workflow to process messages 415for automatic list creation. 416 417When spin() method is invoked, it reads the messages in automatic spool. 418If the list a message is bound for has not been there and list creation is 419authorized, it will be created. Then the message is stored into incoming 420message spool again and waits for processing by 421L<Sympa::Spindle::ProcessIncoming>. 422 423Order to process messages in source spool are controlled by modification time 424of files and delivery date. 425Some messages are skipped according to these priorities 426(See L<Sympa::Spool::Automatic>): 427 428=over 429 430=item * 431 432Messages with lowest priority (C<z> or C<Z>) are skipped. 433 434=item * 435 436Messages with possibly higher priority are chosen. 437This is done by skipping messages with lower priority than those already 438found. 439 440=back 441 442=head2 Public methods 443 444See also L<Sympa::Spindle/"Public methods">. 445 446=over 447 448=item new ( [ keepcopy =E<gt> $directory ], 449[ log_level =E<gt> $level ], 450[ log_smtp =E<gt> 0|1 ] ) 451 452=item spin ( ) 453 454new() may take following options: 455 456=over 457 458=item keepcopy =E<gt> $directory 459 460spin() keeps copy of successfully processed messages in $directory. 461 462=item log_level =E<gt> $level 463 464Overwrites log_level parameter in configuration. 465 466=item log_smtp =E<gt> 0|1 467 468Overwrites log_smtp parameter in configuration. 469 470=back 471 472=back 473 474=head2 Properties 475 476See also L<Sympa::Spindle/"Properties">. 477 478=over 479 480=item {distaff} 481 482Instance of L<Sympa::Spool::Automatic> class. 483 484=back 485 486=head1 SEE ALSO 487 488L<Sympa::Message>, 489L<Sympa::Spindle>, L<Sympa::Spool::Automatic>, L<Sympa::Spool::Incoming>. 490 491=head1 HISTORY 492 493L<Sympa::Spindle::ProcessAutomatic> appeared on Sympa 6.2.10. 494 495=cut 496