#!/usr/bin/perl -wT # # ========================================================================== # # ZoneMinder Event Filter Script, $Date$, $Revision$ # Copyright (C) 2001-2008 Philip Coombes # # 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, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. # # ========================================================================== =head1 NAME zmfilter.pl - ZoneMinder tool to filter events =head1 SYNOPSIS zmfilter.pl [-f ,--filter=] [--filter_id=] | -v, --version =head1 DESCRIPTION This script continuously monitors the recorded events for the given monitor and applies any filters which would delete and/or upload matching events. =head1 OPTIONS -f{filter name}, --filter={filter name} - The name of a specific filter to run --filter_id={filter id} - The id of a specific filter to run -v, --version - Print ZoneMinder version =cut use strict; use bytes; # ========================================================================== # # These are the elements you can edit to suit your installation # # ========================================================================== use constant START_DELAY => 5; # How long to wait before starting # ========================================================================== # # You shouldn't need to change anything from here downwards # # ========================================================================== @EXTRA_PERL_LIB@ use ZoneMinder; use DBI; use POSIX; use Time::HiRes qw/gettimeofday/; #use Date::Manip; use Getopt::Long; use autouse 'Pod::Usage'=>qw(pod2usage); use autouse 'Data::Dumper'=>qw(Dumper); my $filter_name = ''; my $filter_id; my $version = 0; my $zm_terminate = 0; GetOptions( 'filter=s' =>\$filter_name, 'filter_id=s' =>\$filter_id, 'version' =>\$version ) or pod2usage(-exitstatus => -1); if ( $version ) { print ZoneMinder::Base::ZM_VERSION . "\n"; exit(0); } require ZoneMinder::Event; require ZoneMinder::Filter; use constant EVENT_PATH => ($Config{ZM_DIR_EVENTS}=~m|/|) ? $Config{ZM_DIR_EVENTS} : ($Config{ZM_PATH_WEB}.'/'.$Config{ZM_DIR_EVENTS}) ; logInit(); sub HupHandler { Info("Received HUP, reloading"); &ZoneMinder::Logger::logHupHandler(); } sub TermHandler { Info("Received TERM, exiting"); $zm_terminate = 1; } sub Term { exit( 0 ); } $SIG{HUP} = \&HupHandler; $SIG{TERM} = \&TermHandler; $SIG{INT} = \&TermHandler; if ( $Config{ZM_OPT_UPLOAD} ) { # Comment these out if you don't have them and don't want to upload # or don't want to use that format if ( $Config{ZM_UPLOAD_ARCH_FORMAT} eq 'zip' ) { require Archive::Zip; import Archive::Zip qw( :ERROR_CODES :CONSTANTS ); } else { require Archive::Tar; } if ( $Config{ZM_UPLOAD_PROTOCOL} eq 'ftp' ) { require Net::FTP; } else { require Net::SFTP::Foreign; } } if ( $Config{ZM_OPT_EMAIL} ) { if ( $Config{ZM_NEW_MAIL_MODULES} ) { require MIME::Lite; require Net::SMTP; } else { require MIME::Entity; } } if ( $Config{ZM_OPT_MESSAGE} ) { if ( $Config{ZM_NEW_MAIL_MODULES} ) { require MIME::Lite; require Net::SMTP; } else { require MIME::Entity; } } $| = 1; $ENV{PATH} = '/bin:/usr/bin:/usr/local/bin'; $ENV{SHELL} = '/bin/sh' if exists $ENV{SHELL}; delete @ENV{qw(IFS CDPATH ENV BASH_ENV)}; my $delay = $Config{ZM_FILTER_EXECUTE_INTERVAL}; my $event_id = 0; if ( ! EVENT_PATH ) { Error( "No event path defined. Config was $Config{ZM_DIR_EVENTS}\n" ); die; } # In future, should not be neccessary wrt StorageAreas chdir( EVENT_PATH ); # SHould not be neccessary... but nice to get a local var. What if it fails? my $dbh = zmDbConnect(); if ( $filter_name ) { Info( "Scanning for events using filter '$filter_name'\n" ); } elsif ( $filter_id ) { Info( "Scanning for events using filter id '$filter_id'\n" ); } else { Info( "Scanning for events using all filters\n" ); } if ( ! ( $filter_name or $filter_id ) ) { Debug("Sleeping due to start delay: " . START_DELAY . ' seconds...' ); sleep( START_DELAY ); } my @filters; my $last_action = 0; while( ! $zm_terminate ) { my $now = time; if ( ($now - $last_action) > $Config{ZM_FILTER_RELOAD_DELAY} ) { Debug( "Reloading filters\n" ); $last_action = $now; @filters = getFilters( { Name=>$filter_name, Id=>$filter_id } ); } foreach my $filter ( @filters ) { last if $zm_terminate; if ( $$filter{Concurrent} and ! ( $filter_id or $filter_name ) ) { my ( $proc ) = $0 =~ /(\S+)/; my ( $id ) = $$filter{Id} =~ /(\d+)/; Debug("Running concurrent filter process $proc --filter_id $$filter{Id} => $id for $$filter{Name}"); system( qq`$proc --filter "$$filter{Name}" &` ); } else { checkFilter( $filter ); } } last if $filter_name or $filter_id or $zm_terminate; Debug( "Sleeping for $delay seconds\n" ); sleep( $delay ); } sub getFilters { my $sql_filters = @_ ? shift : {}; my @sql_values; my @filters; my $sql = 'SELECT * FROM Filters WHERE'; if ( $$sql_filters{Name} ) { $sql .= ' Name = ? AND'; push @sql_values, $$sql_filters{Name}; } elsif ( $$sql_filters{Id} ) { $sql .= ' Id = ? AND'; push @sql_values, $$sql_filters{Id}; } else { $sql .= ' Background = 1 AND'; } $sql .= '( AutoArchive = 1 or AutoVideo = 1 or AutoUpload = 1 or AutoEmail = 1 or AutoMessage = 1 or AutoExecute = 1 or AutoDelete = 1 or UpdateDiskSpace = 1 or AutoMove = 1 ) ORDER BY Name'; my $sth = $dbh->prepare_cached( $sql ) or Fatal( "Unable to prepare '$sql': ".$dbh->errstr() ); my $res = $sth->execute( @sql_values ) or Fatal( "Unable to execute '$sql': ".$sth->errstr() ); FILTER: while( my $db_filter = $sth->fetchrow_hashref() ) { my $filter = new ZoneMinder::Filter( $$db_filter{Id}, $db_filter ); Debug( "Found filter '$db_filter->{Name}'\n" ); # The undef here is to make sure the Sql gets regenerated because the Filter object may be cached my $filter_sql = $filter->Sql(undef); if ( ! $filter_sql ) { Error( "Error parsing Sql. skipping filter '$db_filter->{Name}'\n" ); next FILTER; } push @filters, $filter; } $sth->finish(); if ( ! @filters ) { Warning("No filter found for $sql with values(@sql_values)"); } else { Debug( "Got " . @filters . " filters" ); } return @filters; } # end sub getFilters sub checkFilter { my $filter = shift; my @Events = $filter->Execute(); Info( join(' ', 'Checking filter', $filter->{Name}, join( ', ', ($filter->{AutoDelete}?'delete':()), ($filter->{AutoArchive}?'archive':()), ($filter->{AutoVideo}?'video':()), ($filter->{AutoUpload}?'upload':()), ($filter->{AutoEmail}?'email':()), ($filter->{AutoMessage}?'message':()), ($filter->{AutoExecute}?'execute':()), ($filter->{AutoMove}?'move':()), ($filter->{UpdateDiskSpace}?'update disk space':()), ), 'returned' , scalar @Events , 'events', "\n", ) ); foreach my $event ( @Events ) { last if $zm_terminate; Debug( "Checking event $event->{Id}" ); my $delete_ok = !undef; $dbh->ping(); if ( $filter->{AutoArchive} ) { Info( "Archiving event $event->{Id}" ); # Do it individually to avoid locking up the table for new events my $sql = 'UPDATE Events SET Archived = 1 WHERE Id = ?'; my $sth = $dbh->prepare_cached( $sql ) or Fatal( "Unable toprepare '$sql': ".$dbh->errstr() ); my $res = $sth->execute( $event->{Id} ) or Error( "Unable toexecute '$sql': ".$sth->errstr() ); } if ( $Config{ZM_OPT_FFMPEG} && $filter->{AutoVideo} ) { if ( !$event->{Videoed} ) { $delete_ok = undef if ( !generateVideo( $filter, $event ) ); } } if ( $Config{ZM_OPT_EMAIL} && $filter->{AutoEmail} ) { if ( !$event->{Emailed} ) { $delete_ok = undef if ( !sendEmail( $filter, $event ) ); } } if ( $Config{ZM_OPT_MESSAGE} && $filter->{AutoMessage} ) { if ( !$event->{Messaged} ) { $delete_ok = undef if ( !sendMessage( $filter, $event ) ); } } if ( $Config{ZM_OPT_UPLOAD} && $filter->{AutoUpload} ) { if ( !$event->{Uploaded} ) { $delete_ok = undef if ( !uploadArchFile( $filter, $event ) ); } } if ( $filter->{AutoExecute} ) { if ( !$event->{Executed} ) { $delete_ok = undef if ( !executeCommand( $filter, $event ) ); } } if ( $filter->{AutoDelete} ) { if ( $delete_ok ) { my $Event = new ZoneMinder::Event( $$event{Id}, $event ); $Event->delete(); } else { Error( "Unable toto delete event $event->{Id} as previous operations failed\n" ); } } # end if AutoDelete if ( $filter->{AutoMove} ) { my $Event = new ZoneMinder::Event( $$event{Id}, $event ); my $NewStorage = new ZoneMinder::Storage( $filter->{AutoMoveTo} ); $_ = $Event->MoveTo( $NewStorage ); Error($_) if $_; } if ( $filter->{UpdateDiskSpace} ) { my $Event = new ZoneMinder::Event( $$event{Id}, $event ); $ZoneMinder::Database::dbh->begin_work(); $Event->lock_and_load(); my $old_diskspace = $$Event{DiskSpace}; if ( $old_diskspace != $Event->DiskSpace(undef) ) { $Event->save(); } $ZoneMinder::Database::dbh->commit(); } # end if UpdateDiskSpace } # end foreach event } sub generateVideo { my $filter = shift; my $event = shift; my $phone = shift; my $rate = $event->{DefaultRate}/100; my $scale = $event->{DefaultScale}/100; my $format; my @ffmpeg_formats = split( /\s+/, $Config{ZM_FFMPEG_FORMATS} ); my $default_video_format; my $default_phone_format; foreach my $ffmpeg_format( @ffmpeg_formats ) { if ( $ffmpeg_format =~ /^(.+)\*\*$/ ) { $default_phone_format = $1; } elsif ( $ffmpeg_format =~ /^(.+)\*$/ ) { $default_video_format = $1; } } if ( $phone && $default_phone_format ) { $format = $default_phone_format; } elsif ( $default_video_format ) { $format = $default_video_format; } else { $format = $ffmpeg_formats[0]; } my $command = join( '', $Config{ZM_PATH_BIN}, '/zmvideo.pl -e ', $event->{Id}, ' -r ', $rate, ' -s ', $scale, ' -f ', $format, ); my $output = qx($command); chomp( $output ); my $status = $? >> 8; if ( $status || logDebugging() ) { Debug( "Output: $output\n" ); } if ( $status ) { Error( "Video generation '$command' failed with status: $status\n" ); if ( wantarray() ) { return( undef, undef ); } return( 0 ); } else { my $sql = 'UPDATE Events SET Videoed = 1 WHERE Id = ?'; my $sth = $dbh->prepare_cached( $sql ) or Fatal( "Unable toprepare '$sql': ".$dbh->errstr() ); my $res = $sth->execute( $event->{Id} ) or Fatal( "Unable toexecute '$sql': ".$sth->errstr() ); if ( wantarray() ) { return( $format, $output ); } } return( 1 ); } # Returns an image absolute path for given event and frame # Optionally an analyse image path may be returned if an analyse image exists # If neither capture nor analyse image exists it will try to extract a frame from .mp4 file if exists # An empty string is returned if no one from methods above works sub generateImage { my $event = shift; my $frame = shift; my $analyse = do { @_ ? shift : 0 }; # don't return analyse image by default my $capture_image_path = sprintf('%s/%0'.$Config{ZM_EVENT_IMAGE_DIGITS}.'d-capture.jpg', getEventPath($event), $frame->{FrameId}); my $analyse_image_path = sprintf('%s/%0'.$Config{ZM_EVENT_IMAGE_DIGITS}.'d-analyse.jpg', getEventPath($event), $frame->{FrameId}) if $analyse; my $video_path = sprintf('%s/%d-video.mp4', getEventPath($event), $event->{Id}); my $image_path = ''; # check if the image file exists. If the file doesn't exist and we use H264 try to extract it from .mp4 video if ( $analyse && -r $analyse_image_path ) { $image_path = $analyse_image_path; } elsif ( -r $capture_image_path ) { $image_path = $capture_image_path; } elsif ( -e $video_path ) { if ( !system('ffmpeg -y -v 0 -i '.$video_path." -vf 'select=gte(n\\,".$frame->{FrameId}."),setpts=PTS-STARTPTS' -vframes 1 -f image2 ".$capture_image_path) ) { $image_path = $capture_image_path; } } return $image_path; } sub uploadArchFile { my $filter = shift; my $event = shift; if ( ! $Config{ZM_UPLOAD_HOST} ) { Error( 'Cannot upload archive as no upload host defined' ); return( 0 ); } my $archFile = $event->{MonitorName}.'-'.$event->{Id}; my $archImagePath = getEventPath( $event ) .'/' .( ( $Config{ZM_UPLOAD_ARCH_ANALYSE} ) ? '{*analyse,*capture}' : '*capture' ) .'.jpg' ; my @archImageFiles = glob($archImagePath); my $archLocPath; my $archError = 0; if ( $Config{ZM_UPLOAD_ARCH_FORMAT} eq 'zip' ) { $archFile .= '.zip'; $archLocPath = $Config{ZM_UPLOAD_LOC_DIR}.'/'.$archFile; my $zip = Archive::Zip->new(); Info( "Creating upload file '$archLocPath', ".int(@archImageFiles)." files\n" ); my $status = &AZ_OK; foreach my $imageFile ( @archImageFiles ) { Debug( "Adding $imageFile\n" ); my $member = $zip->addFile( $imageFile ); if ( !$member ) { Error( "Unable toto add image file $imageFile to zip archive $archLocPath" ); $archError = 1; last; } $member->desiredCompressionMethod( $Config{ZM_UPLOAD_ARCH_COMPRESS} ? &COMPRESSION_DEFLATED : &COMPRESSION_STORED ); } if ( !$archError ) { $status = $zip->writeToFileNamed( $archLocPath ); if ( $archError = ($status != &AZ_OK) ) { Error( "Zip error: $status\n " ); } } else { Error( "Error adding images to zip archive $archLocPath, not writing" ); } } elsif ( $Config{ZM_UPLOAD_ARCH_FORMAT} eq 'tar' ) { if ( $Config{ZM_UPLOAD_ARCH_COMPRESS} ) { $archFile .= '.tar.gz'; } else { $archFile .= '.tar'; } $archLocPath = $Config{ZM_UPLOAD_LOC_DIR}.'/'.$archFile; Info( "Creating upload file '$archLocPath', ".int(@archImageFiles)." files\n" ); if ( $archError = !Archive::Tar->create_archive( $archLocPath, $Config{ZM_UPLOAD_ARCH_COMPRESS}, @archImageFiles ) ) { Error( 'Tar error: '.Archive::Tar->error()."\n " ); } } if ( $archError ) { return( 0 ); } else { if ( $Config{ZM_UPLOAD_PROTOCOL} eq 'ftp' ) { Info( 'Uploading to '.$Config{ZM_UPLOAD_HOST}." using FTP\n" ); my $ftp = Net::FTP->new( $Config{ZM_UPLOAD_HOST}, Timeout=>$Config{ZM_UPLOAD_TIMEOUT}, Passive=>$Config{ZM_UPLOAD_FTP_PASSIVE}, Debug=>$Config{ZM_UPLOAD_DEBUG} ); if ( !$ftp ) { Error( "Unable tocreate FTP connection: $@" ); return( 0 ); } $ftp->login( $Config{ZM_UPLOAD_USER}, $Config{ZM_UPLOAD_PASS} ) or Error( "FTP - Unable tologin" ); $ftp->binary() or Error( "FTP - Unable togo binary" ); $ftp->cwd( $Config{ZM_UPLOAD_REM_DIR} ) or Error( "FTP - Unable tocwd" ) if ( $Config{ZM_UPLOAD_REM_DIR} ); $ftp->put( $archLocPath ) or Error( "FTP - Unable toupload '$archLocPath'" ); $ftp->quit() or Error( "FTP - Unable toquit" ); } else { my $host = $Config{ZM_UPLOAD_HOST}; $host .= ':'.$Config{ZM_UPLOAD_PORT} if $Config{ZM_UPLOAD_PORT}; Info( 'Uploading to '.$host." using SFTP\n" ); my %sftpOptions = ( host=>$Config{ZM_UPLOAD_HOST}, user=>$Config{ZM_UPLOAD_USER} ); $sftpOptions{password} = $Config{ZM_UPLOAD_PASS} if $Config{ZM_UPLOAD_PASS}; $sftpOptions{port} = $Config{ZM_UPLOAD_PORT} if $Config{ZM_UPLOAD_PORT}; $sftpOptions{timeout} = $Config{ZM_UPLOAD_TIMEOUT} if $Config{ZM_UPLOAD_TIMEOUT}; my @more_ssh_args; push @more_ssh_args, '-o'=>'StrictHostKeyChecking=no' if ! $Config{ZM_UPLOAD_STRICT}; push @more_ssh_args, '-v' if $Config{ZM_UPLOAD_DEBUG}; $sftpOptions{more} = [@more_ssh_args]; my $sftp = Net::SFTP::Foreign->new( $Config{ZM_UPLOAD_HOST}, %sftpOptions ); if ( $sftp->error ) { Error( "Unable tocreate SFTP connection: ".$sftp->error ); return( 0 ); } $sftp->setcwd( $Config{ZM_UPLOAD_REM_DIR} ) or Error( "SFTP - Unable tosetcwd: ".$sftp->error ) if $Config{ZM_UPLOAD_REM_DIR}; $sftp->put( $archLocPath, $archFile ) or Error( "SFTP - Unable toupload '$archLocPath': ".$sftp->error ); } unlink( $archLocPath ); my $sql = 'UPDATE Events SET Uploaded = 1 WHERE Id = ?'; my $sth = $dbh->prepare_cached( $sql ) or Fatal( "Unable toprepare '$sql': ".$dbh->errstr() ); my $res = $sth->execute( $event->{Id} ) or Fatal( "Unable toexecute '$sql': ".$sth->errstr() ); } return( 1 ); } sub substituteTags { my $text = shift; my $filter = shift; my $event = shift; my $attachments_ref = shift; # First we'd better check what we need to get # We have a filter and an event, do we need any more # monitor information? my $need_monitor = $text =~ /%(?:MET|MEH|MED|MEW|MEN|MEA)%/; my $monitor = {}; if ( $need_monitor ) { my $db_now = strftime( '%Y-%m-%d %H:%M:%S', localtime() ); my $sql = "SELECT M.Id, count(E.Id) as EventCount, count(if(E.Archived,1,NULL)) as ArchEventCount, count(if(E.StartTime>'$db_now' - INTERVAL 1 HOUR && E.Archived = 0,1,NULL)) as HourEventCount, count(if(E.StartTime>'$db_now' - INTERVAL 1 DAY && E.Archived = 0,1,NULL)) as DayEventCount, count(if(E.StartTime>'$db_now' - INTERVAL 7 DAY && E.Archived = 0,1,NULL)) as WeekEventCount, count(if(E.StartTime>'$db_now' - INTERVAL 1 MONTH && E.Archived = 0,1,NULL)) as MonthEventCount FROM Monitors as M LEFT JOIN Events as E on E.MonitorId = M.Id WHERE MonitorId = ? GROUP BY E.MonitorId ORDER BY Id" ; my $sth = $dbh->prepare_cached( $sql ) or Fatal( "Unable toprepare '$sql': ".$dbh->errstr() ); my $res = $sth->execute( $event->{MonitorId} ) or Fatal( "Unable toexecute '$sql': ".$sth->errstr() ); $monitor = $sth->fetchrow_hashref(); $sth->finish(); return() if ( !$monitor ); } # Do we need the image information too? my $need_images = $text =~ /%(?:EPI1|EPIM|EI1|EIM|EI1A|EIMA)%/; my $first_alarm_frame; my $max_alarm_frame; my $max_alarm_score = 0; if ( $need_images ) { my $sql = "SELECT * FROM Frames WHERE EventId = ? AND Type = 'Alarm' ORDER BY FrameId" ; my $sth = $dbh->prepare_cached( $sql ) or Fatal( "Unable toprepare '$sql': ".$dbh->errstr() ); my $res = $sth->execute( $event->{Id} ) or Fatal( "Unable toexecute '$sql': ".$sth->errstr() ); while( my $frame = $sth->fetchrow_hashref() ) { if ( !$first_alarm_frame ) { $first_alarm_frame = $frame; } if ( $frame->{Score} > $max_alarm_score ) { $max_alarm_frame = $frame; $max_alarm_score = $frame->{Score}; } } $sth->finish(); } my $url = $Config{ZM_URL}; $text =~ s/%ZP%/$url/g; $text =~ s/%MN%/$event->{MonitorName}/g; $text =~ s/%MET%/$monitor->{EventCount}/g; $text =~ s/%MEH%/$monitor->{HourEventCount}/g; $text =~ s/%MED%/$monitor->{DayEventCount}/g; $text =~ s/%MEW%/$monitor->{WeekEventCount}/g; $text =~ s/%MEM%/$monitor->{MonthEventCount}/g; $text =~ s/%MEA%/$monitor->{ArchEventCount}/g; $text =~ s/%MP%/$url?view=watch&mid=$event->{MonitorId}/g; $text =~ s/%MPS%/$url?view=watchfeed&mid=$event->{MonitorId}&mode=stream/g; $text =~ s/%MPI%/$url?view=watchfeed&mid=$event->{MonitorId}&mode=still/g; $text =~ s/%EP%/$url?view=event&mid=$event->{MonitorId}&eid=$event->{Id}/g; $text =~ s/%EPS%/$url?view=event&mode=stream&mid=$event->{MonitorId}&eid=$event->{Id}/g; $text =~ s/%EPI%/$url?view=event&mode=still&mid=$event->{MonitorId}&eid=$event->{Id}/g; $text =~ s/%EI%/$event->{Id}/g; $text =~ s/%EN%/$event->{Name}/g; $text =~ s/%EC%/$event->{Cause}/g; $text =~ s/%ED%/$event->{Notes}/g; $text =~ s/%ET%/$event->{StartTime}/g; $text =~ s/%EL%/$event->{Length}/g; $text =~ s/%EF%/$event->{Frames}/g; $text =~ s/%EFA%/$event->{AlarmFrames}/g; $text =~ s/%EST%/$event->{TotScore}/g; $text =~ s/%ESA%/$event->{AvgScore}/g; $text =~ s/%ESM%/$event->{MaxScore}/g; if ( $first_alarm_frame ) { $text =~ s/%EPI1%/$url?view=frame&mid=$event->{MonitorId}&eid=$event->{Id}&fid=$first_alarm_frame->{FrameId}/g; $text =~ s/%EPIM%/$url?view=frame&mid=$event->{MonitorId}&eid=$event->{Id}&fid=$max_alarm_frame->{FrameId}/g; if ( $attachments_ref && $text =~ s/%EI1%//g ) { my $path = generateImage( $event, $first_alarm_frame ); if ( -e $path ) { push( @$attachments_ref, { type=>'image/jpeg', path=>$path } ); } } if ( $attachments_ref && $text =~ s/%EIM%//g ) { # Don't attach the same image twice if ( !@$attachments_ref || ($first_alarm_frame->{FrameId} != $max_alarm_frame->{FrameId} ) ) { my $path = generateImage( $event, $max_alarm_frame ); if ( -e $path ) { push( @$attachments_ref, { type=>'image/jpeg', path=>$path } ); } } } if ( $attachments_ref && $text =~ s/%EI1A%//g ) { my $path = generateImage( $event, $first_alarm_frame, 'analyse' ); if ( -e $path ) { push( @$attachments_ref, { type=>'image/jpeg', path=>$path } ); } } if ( $attachments_ref && $text =~ s/%EIMA%//g ) { # Don't attach the same image twice if ( !@$attachments_ref || ($first_alarm_frame->{FrameId} != $max_alarm_frame->{FrameId} ) ) { my $path = generateImage( $event, $max_alarm_frame, 'analyse'); if ( -e $path ) { push( @$attachments_ref, { type=>'image/jpeg', path=>$path } ); } } } } # end if $first_alarm_frame if ( $attachments_ref && $Config{ZM_OPT_FFMPEG} ) { if ( $text =~ s/%EV%//g ) { my ( $format, $path ) = generateVideo( $filter, $event ); if ( !$format ) { return( undef ); } push( @$attachments_ref, { type=>"video/$format", path=>$path } ); } if ( $text =~ s/%EVM%//g ) { my ( $format, $path ) = generateVideo( $filter, $event, 1 ); if ( !$format ) { return( undef ); } push( @$attachments_ref, { type=>"video/$format", path=>$path } ); } } $text =~ s/%FN%/$filter->{Name}/g; ( my $filter_name = $filter->{Name} ) =~ s/ /+/g; $text =~ s/%FP%/$url?view=filter&mid=$event->{MonitorId}&filter_name=$filter_name/g; return( $text ); } # end subsitituteTags sub sendEmail { my $filter = shift; my $event = shift; if ( ! $Config{ZM_FROM_EMAIL} ) { Error( "No 'from' email address defined, not sending email" ); return( 0 ); } if ( ! $Config{ZM_EMAIL_ADDRESS} ) { Error( 'No email address defined, not sending email' ); return( 0 ); } Info( "Creating notification email\n" ); my $subject = substituteTags( $Config{ZM_EMAIL_SUBJECT}, $filter, $event ); return( 0 ) if ( !$subject ); my @attachments; my $body = substituteTags( $Config{ZM_EMAIL_BODY}, $filter, $event, \@attachments ); return( 0 ) if ( !$body ); Info( "Sending notification email '$subject'\n" ); eval { if ( $Config{ZM_NEW_MAIL_MODULES} ) { ### Create the multipart container my $mail = MIME::Lite->new ( From => $Config{ZM_FROM_EMAIL}, To => $Config{ZM_EMAIL_ADDRESS}, Subject => $subject, Type => 'multipart/mixed' ); ### Add the text message part $mail->attach ( Type => 'TEXT', Data => $body ); ### Add the attachments foreach my $attachment ( @attachments ) { Info( "Attaching '$attachment->{path}\n" ); $mail->attach( Path => $attachment->{path}, Type => $attachment->{type}, Disposition => 'attachment' ); } ### Send the Message if ( $Config{ZM_SSMTP_MAIL} ) { my $ssmtp_location = $Config{ZM_SSMTP_PATH}; if ( !$ssmtp_location ) { if ( logDebugging() ) { Debug( "which ssmtp: $ssmtp_location - set ssmtp path in options to suppress this message\n" ); } $ssmtp_location = qx('which ssmtp'); } if ( !$ssmtp_location ) { Debug( "Unable tofind ssmtp, trying MIME::Lite->send" ); MIME::Lite->send( 'smtp', $Config{ZM_EMAIL_HOST}, Timeout=>60 ); $mail->send(); } else { ### Send using SSMTP $mail->send( 'sendmail', $ssmtp_location, $Config{ZM_EMAIL_ADDRESS} ); } } else { MIME::Lite->send( 'smtp', $Config{ZM_EMAIL_HOST}, Timeout=>60 ); $mail->send(); } } else { my $mail = MIME::Entity->build( From => $Config{ZM_FROM_EMAIL}, To => $Config{ZM_EMAIL_ADDRESS}, Subject => $subject, Type => (($body=~//)?'text/html':'text/plain'), Data => $body ); foreach my $attachment ( @attachments ) { Info( "Attaching '$attachment->{path}\n" ); $mail->attach( Path => $attachment->{path}, Type => $attachment->{type}, Encoding => 'base64' ); } $mail->smtpsend( Host => $Config{ZM_EMAIL_HOST}, MailFrom => $Config{ZM_FROM_EMAIL} ); } }; if ( $@ ) { Error( "Unable tosend email: $@" ); return( 0 ); } else { Info( "Notification email sent\n" ); } my $sql = 'update Events set Emailed = 1 where Id = ?'; my $sth = $dbh->prepare_cached( $sql ) or Fatal( "Unable toprepare '$sql': ".$dbh->errstr() ); my $res = $sth->execute( $event->{Id} ) or Fatal( "Unable toexecute '$sql': ".$sth->errstr() ); return( 1 ); } sub sendMessage { my $filter = shift; my $event = shift; if ( ! $Config{ZM_FROM_EMAIL} ) { Error( "No 'from' email address defined, not sending message" ); return( 0 ); } if ( ! $Config{ZM_MESSAGE_ADDRESS} ) { Error( 'No message address defined, not sending message' ); return( 0 ); } Info( "Creating notification message\n" ); my $subject = substituteTags( $Config{ZM_MESSAGE_SUBJECT}, $filter, $event ); return( 0 ) if ( !$subject ); my @attachments; my $body = substituteTags( $Config{ZM_MESSAGE_BODY}, $filter, $event, \@attachments ); return( 0 ) if ( !$body ); Info( "Sending notification message '$subject'\n" ); eval { if ( $Config{ZM_NEW_MAIL_MODULES} ) { ### Create the multipart container my $mail = MIME::Lite->new ( From => $Config{ZM_FROM_EMAIL}, To => $Config{ZM_MESSAGE_ADDRESS}, Subject => $subject, Type => 'multipart/mixed' ); ### Add the text message part $mail->attach ( Type => 'TEXT', Data => $body ); ### Add the attachments foreach my $attachment ( @attachments ) { Info( "Attaching '$attachment->{path}\n" ); $mail->attach( Path => $attachment->{path}, Type => $attachment->{type}, Disposition => 'attachment' ); } ### Send the Message if ( $Config{ZM_SSMTP_MAIL} ) { my $ssmtp_location = $Config{ZM_SSMTP_PATH}; if ( !$ssmtp_location ) { if ( logDebugging() ) { Debug( "which ssmtp: $ssmtp_location - set ssmtp path in options to suppress this message\n" ); } $ssmtp_location = qx('which ssmtp'); } if ( !$ssmtp_location ) { Debug( 'Unable tofind ssmtp, trying MIME::Lite->send' ); MIME::Lite->send( 'smtp', $Config{ZM_EMAIL_HOST}, Timeout=>60 ); $mail->send(); } else { ### Send using SSMTP $mail->send( 'sendmail', $ssmtp_location, $Config{ZM_MESSAGE_ADDRESS} ); } } else { MIME::Lite->send( 'smtp', $Config{ZM_EMAIL_HOST}, Timeout=>60 ); $mail->send(); } } else { my $mail = MIME::Entity->build( From => $Config{ZM_FROM_EMAIL}, To => $Config{ZM_MESSAGE_ADDRESS}, Subject => $subject, Type => (($body=~//)?'text/html':'text/plain'), Data => $body ); foreach my $attachment ( @attachments ) { Info( "Attaching '$attachment->{path}\n" ); $mail->attach( Path => $attachment->{path}, Type => $attachment->{type}, Encoding => 'base64' ); } $mail->smtpsend( Host => $Config{ZM_EMAIL_HOST}, MailFrom => $Config{ZM_FROM_EMAIL} ); } }; if ( $@ ) { Error( "Unable tosend email: $@" ); return( 0 ); } else { Info( "Notification message sent\n" ); } my $sql = 'update Events set Messaged = 1 where Id = ?'; my $sth = $dbh->prepare_cached( $sql ) or Fatal( "Unable toprepare '$sql': ".$dbh->errstr() ); my $res = $sth->execute( $event->{Id} ) or Fatal( "Unable toexecute '$sql': ".$sth->errstr() ); return( 1 ); } sub executeCommand { my $filter = shift; my $event = shift; my $event_path = getEventPath( $event ); my $command = $filter->{AutoExecuteCmd}; $command .= " $event_path"; $command = substituteTags( $command, $filter, $event ); Info( "Executing '$command'\n" ); my $output = qx($command); my $status = $? >> 8; if ( $status || logDebugging() ) { chomp( $output ); Debug( "Output: $output\n" ); } if ( $status ) { Error( "Command '$command' exited with status: $status\n" ); return( 0 ); } else { my $sql = 'update Events set Executed = 1 where Id = ?'; my $sth = $dbh->prepare_cached( $sql ) or Fatal( "Unable toprepare '$sql': ".$dbh->errstr() ); my $res = $sth->execute( $event->{Id} ) or Fatal( "Unable toexecute '$sql': ".$sth->errstr() ); } return( 1 ); }