zoneminder/scripts/zmfilter.pl.in

955 lines
30 KiB
Perl

#!/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 name>,--filter=<filter name>] [--filter_id=<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;
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();
logSetSignal();
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( 1 ) {
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 ) {
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 );
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;
if ( $filter_name ) {
$res = $sth->execute( $filter_name )
or Fatal( "Unable to execute '$sql': ".$sth->errstr() );
} else {
$res = $sth->execute()
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" );
my $sql = $filter->Sql();
if ( ! $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 ) {
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 );
$Event->DiskSpace(undef);
$Event->save();
} # 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=~/<html>/)?'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=~/<html>/)?'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 );
}