generate_signature.sh Generate a PGP key pair for signing emails stylesheet.css Stylesheet for HTML format emails test_plugin.pl Aid for testing plugins statusmail.sh Simple shell script for running statusmail.pl statusmailctrl.c Add/Remove statusmail from fcron.hourly
Signed-off-by: Tim FitzGeorge ipfr@tfitzgeorge.me.uk --- src/misc-progs/statusmailctrl.c | 36 +++ src/statusmail/generate_signature.sh | 51 ++++ src/statusmail/statusmail.sh | 3 + src/statusmail/stylesheet.css | 30 ++ src/statusmail/test_plugin.pl | 541 +++++++++++++++++++++++++++++++++++ 5 files changed, 661 insertions(+) create mode 100644 src/misc-progs/statusmailctrl.c create mode 100755 src/statusmail/generate_signature.sh create mode 100755 src/statusmail/statusmail.sh create mode 100755 src/statusmail/stylesheet.css create mode 100755 src/statusmail/test_plugin.pl
diff --git a/src/misc-progs/statusmailctrl.c b/src/misc-progs/statusmailctrl.c new file mode 100644 index 000000000..828aaf223 --- /dev/null +++ b/src/misc-progs/statusmailctrl.c @@ -0,0 +1,36 @@ +/* This file is part of the IPFire Firewall. + * + * This program is distributed under the terms of the GNU General Public + * Licence. See the file COPYING for details. + * + */ + +#include <stdlib.h> +#include <stdio.h> +#include <string.h> +#include <unistd.h> +#include <sys/types.h> +#include <fcntl.h> +#include "setuid.h" + +int main(int argc, char *argv[]) { + + if (!(initsetuid())) + exit(1); + + if (argc < 2) { + fprintf(stderr, "\nNo argument given.\n\nstatusmailctrl (enable|disable)\n\n"); + exit(1); + } + + if (strcmp(argv[1], "enable") == 0) { + safe_system("ln -fs /usr/lib/statusmail/statusmail.sh /etc/fcron.hourly/statusmail >/dev/null 2>&1"); + } else if (strcmp(argv[1], "disable") == 0) { + safe_system("rm -f /etc/fcron.hourly/statusmail >/dev/null 2>&1"); + } else { + fprintf(stderr, "\nBad argument given.\n\nstatusmailctrl (enable|disable)\n\n"); + exit(1); + } + + return 0; +} diff --git a/src/statusmail/generate_signature.sh b/src/statusmail/generate_signature.sh new file mode 100755 index 000000000..267d6a302 --- /dev/null +++ b/src/statusmail/generate_signature.sh @@ -0,0 +1,51 @@ +#!/bin/sh + +############################################################################ +# # +# Send log and status emails for IPFire # +# # +# This 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 3 of the License, or # +# (at your option) any later version. # +# # +# This 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 IPFire; if not, write to the Free Software # +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # +# # +# Copyright (C) 2018 - 2019 The IPFire Team # +# # +############################################################################ +# Generates a PGP key that is used to sign email messages # +############################################################################ + +source /var/ipfire/dma/mail.conf + +# Find the old key if there is one so we can delete it later + +OLDKEY=`gpg --homedir /var/ipfire/statusmail/keys --with-colons --fingerprint --list-keys IPFire | sed -ne '/^fpr/{s/fpr//;s/://g;p}'` 2>/dev/null + +echo Generate new keys + +/usr/bin/gpg --homedir /var/ipfire/statusmail/keys --batch --gen-key <<EOF +Key-Type: rsa +Key-Length: 4096 +Key-Usage: sign +Name-Real: IPFire +Name-Email: $SENDER +Expire-Date: 0 +Passphrase: ipfirestatusemail +%commit +%echo done +EOF + +if [[ $OLDKEY ]]; then + echo Delete old keys + gpg --homedir /var/ipfire/statusmail/keys --batch --yes --delete-secret-keys $OLDKEY + gpg --homedir /var/ipfire/statusmail/keys --batch --yes --delete-keys $OLDKEY +fi; diff --git a/src/statusmail/statusmail.sh b/src/statusmail/statusmail.sh new file mode 100755 index 000000000..a996026f1 --- /dev/null +++ b/src/statusmail/statusmail.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +sudo -u nobody /usr/local/bin/statusmail.pl diff --git a/src/statusmail/stylesheet.css b/src/statusmail/stylesheet.css new file mode 100755 index 000000000..3ee93b5cc --- /dev/null +++ b/src/statusmail/stylesheet.css @@ -0,0 +1,30 @@ +h1 { color: #fff; font-size: 2.5em; font-weight: bold; padding-top: 0.2em; padding-left: 0.5em; } +h2, h3, h4, h5, h6 { font-size: 20px; font-weight: normal; letter-spacing: -1px; text-align: left; } +h3 { font-size: 18px; color: #000000; } +h4 { font-size: 16px; color: #505050; } +table { border-spacing: 0; border: 1px solid lightgrey; width: 90%; margin: auto; } +img {width: 90% } +th {color: #000000; border-top: 1px solid lightgrey; border-bottom: 1px solid lightgrey; background: #cccccc; padding-left: 0.5em; padding-right: 0.5em; text-align: center; } +tr:nth-child(even) { background-color: #D6D6D6; } +tr:nth-child(odd) { background-color: #F0F0F0;} +td { padding-left: 0.5em; padding-right: 0.5em; } +div. +div.head { height: 70px; margin: 0 auto; } +.error { background-color: #993333; color: white; font-weight: bold; text-align: center; } +.ok { background-color: #339933; color: white; font-weight: bold; text-align: center; } +body { + /* SVG as background image (IE9/Chrome/Safari/Opera) */ + background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIHZpZXdCb3g9IjAgMCAxIDEiIHByZXNlcnZlQXNwZWN0UmF0aW89Im5vbmUiPgo8bGluZWFyR3JhZGllbnQgaWQ9Imc2ODQiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIiB4MT0iMTAwJSIgeTE9IjEwMCUiIHgyPSIxMDAlIiB5Mj0iMCUiPgo8c3RvcCBzdG9wLWNvbG9yPSIjMDAwMDAwIiBvZmZzZXQ9IjAiLz48c3RvcCBzdG9wLWNvbG9yPSIjODgwNDAwIiBvZmZzZXQ9IjU3JSIvPgo8L2xpbmVhckdyYWRpZW50Pgo8cmVjdCB4PSIwIiB5PSIwIiB3aWR0aD0iMSIgaGVpZ2h0PSIxIiBmaWxsPSJ1cmwoI2c2ODQpIiAvPgo8L3N2Zz4=); + + background-image: linear-gradient( bottom, #000000 0%, #880400 57% ); + + background-attachment: fixed; + + font-size: 9pt; + font-family: "DejaVu Sans", Helvetica, sans-serif; +} +div.bigbox { margin: 0 auto; margin-top: 0.5em; padding: 1.5em 2em 0 2em; background: white 0px 0px repeat-x; border: 1px solid black; + border-radius: 3px 3px 3px 3px; -webkit-border-radius: 3px 3px 3px 3px; } +div.section { border: 1px solid silver; padding: 1em 2em 1em 2em; margin-bottom: 1em; clear: both; } +div.subsection { margin-left: 2%; clear: both; } +div.item { margin-left: 2%; clear: both; } diff --git a/src/statusmail/test_plugin.pl b/src/statusmail/test_plugin.pl new file mode 100755 index 000000000..247f61605 --- /dev/null +++ b/src/statusmail/test_plugin.pl @@ -0,0 +1,541 @@ +#!/usr/bin/perl + +############################################################################ +# # +# This 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 3 of the License, or # +# (at your option) any later version. # +# # +# This 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 IPFire; if not, write to the Free Software # +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # +# # +# Copyright (C) 2018 - 2019 The IPFire Team # +# # +############################################################################ + +use strict; +use warnings; + +use lib "/usr/lib/statusmail"; + +use Time::Local; + +require "/var/ipfire/general-functions.pl"; +require "${General::swroot}/lang.pl"; + +# Variables + +my $testdir = '/var/ipfire/statusmail/test'; +my $stylesheet = '/usr/lib/statusmail/stylesheet.css'; +my %items; +our $plugin; + +my $start_time = 0; +my @start_time = (); +my $end_time = 0; +my @end_time = (); +my $weeks_covered = 0; + +# Function prototypes + +sub add_mail_item( @ ); +sub get_period_start(); +sub get_period_end(); +sub get_weeks_covered(); +sub cache( $;$ ); + +sub choices( $@ ); +sub integer( $$$ ); +sub yesno( $ ); +sub get_period; + +# Main function + +unless (@ARGV) +{ + print "Usage: $0 path_to_plugin...\n"; + print "Tests statusmail plugins. Should be given the pathnames to one or more plugins\n"; + print "Asks for some general parameters and then for the optional parameters for the\n"; + print "plugins. Output is generated in a local file.\n"; + exit; +} + +foreach $plugin (@ARGV) +{ + if (-e $plugin) + { + require $plugin; + } + else + { + print "Can't find plugin $plugin\n"; + } +} + +if (not %items) +{ + print "No valid plugins found\n"; + exit; +} + +mkdir $testdir unless (-d $testdir); + +# Ask for message format + +my $format = choices( 'Message format', 'html', 'text' ); + +# Create message + +my $message = new TestStatusMail( format => $format, stylesheet => $stylesheet, subject => 'Test email' ); + +get_period ( $message ); + +$message->{'max_lines_per_item'} = integer( 'Maximum lines per item', 1, 1000 ); + +# Loop through the various items + +foreach my $section ( sort keys %items ) +{ + $message->add_section( $section ); + + foreach my $subsection ( sort keys %{ $items{$section} } ) + { + $message->add_subsection( $subsection ); + + foreach my $item ( sort keys %{ $items{$section}{$subsection} } ) + { + next unless ($items{$section}{$subsection}{$item}{'format'} eq 'both' or + $items{$section}{$subsection}{$item}{'format'} eq $format); + + if (yesno( "Add item $section : $subsection : $item ? " )) + { + $message->add_title( $item ); + + my $function = $items{$section}{$subsection}{$item}{'function'}; + + if (exists $items{$section}{$subsection}{$item}{'option'}) + { + if ($items{$section}{$subsection}{$item}{'option'}{'type'} eq 'select') + { + my $option = choices( $items{$section}{$subsection}{$item}{'option'}{'name'}, + @{$items{$section}{$subsection}{$item}{'option'}{'values'} } ); + + &$function( $message, $option ); + } + else + { + my $value = integer( $items{$section}{$subsection}{$item}{'option'}{'name'}, + $items{$section}{$subsection}{$item}{'option'}{'min'}, + $items{$section}{$subsection}{$item}{'option'}{'max'} ); + + &$function( $message, $value ); + } + } + else + { + &$function( $message ); + } + } + } + } +} + +$message->print( $testdir ); + +exit; + + +#------------------------------------------------------------------------------ +# sub choices( text, options ) +# +# Asks the user for an option from the provided list. +# +# Parameters: +# text the question to ask the user +# options list of options +# +# Returns: +# the selected option +#------------------------------------------------------------------------------ + +sub choices( $@ ) +{ + my ($text, @options) = @_; + + my $selection = ''; + my %options; + my @display; + + foreach my $option (@options) + { + my ($name, $value) = split /:/, $option; + + $value ||= $name; + + $options{$name} = $value; + push @display, $name; + } + + while (not $selection) + { + print "Select $text from the following options: " . join( ', ', @display ) . ": "; + + my $line = <STDIN>; + + chomp $line; + + ($selection) = grep /^$line/i, @display; + } + + return $options{$selection}; +} + + +#------------------------------------------------------------------------------ +# sub yesno( text ) +# +# Asks the user for a yes or no option. +# +# Parameters: +# text the question to ask the user +# +# Returns: +# true for yes, false for no +#------------------------------------------------------------------------------ + +sub yesno( $) +{ + my ($text) = @_; + + my $selection = ''; + + while (not $selection) + { + print "$text"; + + my $line = <STDIN>; + + chomp $line; + + ($selection) = grep /$line/i, ( 'yes', 'no' ); + } + + return $selection eq 'yes'; +} + + +#------------------------------------------------------------------------------ +# sub integer( text, min, max ) +# +# Asks the user for an integer within the specified limits. +# +# Parameters: +# text the question to ask the user +# min minimum value of input +# max maximum value of input +# +# Returns: +# the selected value +#------------------------------------------------------------------------------ + +sub integer( $$$ ) +{ + my ($text, $min, $max) = @_; + + my $value; + + while (not defined $value) + { + print "Select $text ($min..$max):"; + + my $line = <STDIN>; + + chomp $line; + + next if ($line =~ m/\D+/); + next unless ($line =~ m/\d/); + next if ($line < $min); + next if ($line > $max); + + $value = $line; + } + + return $value; +} + + +#------------------------------------------------------------------------------ +# sub add_mail_item( params ) +# +# Adds a possible status item to the section and subsection specified. +# +# Parameters: +# params hash containing details of the item to be added: +# section name of the section containing this item +# subsection name of the subsection containing this item +# item name of the item +# function function called to add item to message +# format available formats for the item 'html', 'text' or 'both' +# option hash specifying option parameter (optional) +# +# option can specify either a selection or an integer. For a selection it +# contains: +# type must be 'option' +# values array of strings representing the possible options +# +# For an integer option contains: +# type must be 'integer' +# min minimum valid value of parameter +# max maximum valid value of parameter +#------------------------------------------------------------------------------ + +sub add_mail_item( @ ) +{ + my %params = @_; + + if (not exists $params{'section'}) + { + print "Plugin $plugin has no section specified\n"; + return; + } + + if (not exists $params{'subsection'}) + { + print "Plugin $plugin has no subsection specified\n"; + return; + } + + if (not exists $params{'item'}) + { + print "Plugin $plugin has no item specified\n"; + return; + } + + if (not exists $params{'function'}) + { + print "Plugin $plugin has no function specified\n"; + return; + } + + if ($params{'option'}) + { + unless (ref $params{'option'} eq 'HASH') + { + print "Plugin $plugin option incorrectly specified - should be hash\n"; + } + + unless ($params{'option'}{'type'}) + { + print "Plugin $plugin has no option type specified\n"; + return; + } + + unless ($params{'option'}{'name'}) + { + print "Plugin $plugin has no option name specified\n"; + return; + } + + if ($params{'option'}{'type'} eq 'select') + { + unless (ref $params{'option'}{'values'} eq 'ARRAY' and @{ $params{'option'}{'values'} } > 1) + { + print "Plugin $plugin select option values incorrectly specified\n"; + return; + } + } + elsif ($params{'option'}{'type'} eq 'integer') + { + unless (exists $params{'option'}{'min'} and exists $params{'option'}{'max'} and $params{'option'}{'min'} < $params{'option'}{'max'}) + { + print "Plugin $plugin integer option limits not correctly specified\n"; + print "No minimum value specified\n" unless (exists $params{'option'}{'min'}); + print "No maximum value specified\n" unless (exists $params{'option'}{'max'}); + print "Maximum not greater than minimum\n" unless (exists $params{'option'}{'min'} and + exists $params{'option'}{'min'} and + $params{'option'}{'min'} < $params{'option'}{'max'}); + } + } + else + { + print "Plugin $plugin has invalid option $params{'option'}{'type'}\n"; + return; + } + } + + if ($params{'format'} and $params{'format'} ne 'html' and $params{'format'} ne 'text' and $params{'format'} ne 'both') + { + print "Plugin $plugin has invalid format\n"; + } + + $params{'format'} = 'both' unless (exists $params{'format'}); + + $items{$params{'section'}}{$params{'subsection'}}{$params{'item'}} = { 'function' => $params{'function'}, + 'format' => $params{'format'} }; + + if ($params{'option'}) + { + if ($params{'option'}{'type'} eq 'select') + { + $items{$params{'section'}}{$params{'subsection'}}{$params{'item'}}{'option'}{'type'} = $params{'option'}{'type'}; + $items{$params{'section'}}{$params{'subsection'}}{$params{'item'}}{'option'}{'values'} = $params{'option'}{'values'}; + $items{$params{'section'}}{$params{'subsection'}}{$params{'item'}}{'option'}{'name'} = $params{'option'}{'name'}; + } + elsif ($params{'option'}{'type'} eq 'integer') + { + $items{$params{'section'}}{$params{'subsection'}}{$params{'item'}}{'option'}{'type'} = $params{'option'}{'type'}; + $items{$params{'section'}}{$params{'subsection'}}{$params{'item'}}{'option'}{'min'} = $params{'option'}{'min'}; + $items{$params{'section'}}{$params{'subsection'}}{$params{'item'}}{'option'}{'max'} = $params{'option'}{'max'}; + $items{$params{'section'}}{$params{'subsection'}}{$params{'item'}}{'option'}{'name'} = $params{'option'}{'name'}; + } + } +} + + +#------------------------------------------------------------------------------ +# sub get_period +# +# Gets the period covered by a report +#------------------------------------------------------------------------------ + +sub get_period +{ + my $self = shift; + + my @monthnames = ('Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec' ); + + my $unit = choices( 'Period covered by report', 'hours', 'days', 'weeks', 'months' ); + my $value = integer( "$unit covered by report", 1, 365 ); + + $self->calculate_period( $value, $unit ); +} + +#------------------------------------------------------------------------------ +#------------------------------------------------------------------------------ +# Package TestStatusMail +# +# This package is used to override some of the functionality of the StatusMail +# and EncryptedMail packages. +#------------------------------------------------------------------------------ +#------------------------------------------------------------------------------ + + +package TestStatusMail; + +use base qw/StatusMail/; + +#------------------------------------------------------------------------------ +# sub print( directory ) +# +# Prints the plugin(s) output to the specified directory. +#------------------------------------------------------------------------------ + +sub print( $$ ) +{ + my $self = shift; + my $dir = shift; + my $file = "$dir/test.txt"; + + if ($self->{'empty'}) + { + print "No output produced\n"; + return; + } + + if ($self->{format} eq 'html') + { + $self->{message} .= "</div>\n" if ($self->{in_item}); + $self->{message} .= "</div>\n" if ($self->{in_subsection}); + $self->{message} .= "</div>\n" if ($self->{in_section}); + + $self->{message} .= "</div>\n</body>\n</html>\n"; + $file = "$dir/test.html"; + } + + open OUT, '>', $file or die "Can't open test output file $file: $!"; + + print OUT $self->{message}; + + close OUT; + + print "Output is in $file\n"; +} + + +#------------------------------------------------------------------------------ +# sub add_image( params ) +# +# Outputs an image as a file. +#------------------------------------------------------------------------------ + +sub add_image +{ + my ($self, %params) = @_; + + if ($self->{section}) + { + $self->{message} .= $self->{section}; + $self->{section} = ''; + $self->{in_section} = 1; + $self->{in_subsection} = 0; + $self->{in_item} = 0; + } + + if ($self->{subsection}) + { + $self->{message} .= $self->{subsection}; + $self->{subsection} = ''; + $self->{in_subsection} = 1; + $self->{in_item} = 0; + } + + if ($self->{item}) + { + $self->{message} .= $self->{item}; + $self->{item} = ''; + $self->{in_item} = 1; + } + + $self->{'image_file'}++; + + my $image_name = $self->{'image_file'}; + + $image_name .= '.jpg' if ($params{'type'} eq 'image/jpeg'); + $image_name .= '.gif' if ($params{'type'} eq 'image/gif'); + $image_name .= '.png' if ($params{'type'} eq 'image/png'); + + open OUT, '>', "test/$image_name" or die "Can't open image file $image_name: $!"; + binmode( OUT ); + + if (exists $params{fh}) + { + my $buffer; + binmode $params{fh}; + + while (read $params{fh}, $buffer, 1024) + { + print OUT $buffer; + } + } + elsif (exists $params{data}) + { + print OUT $params{data}; + } + + close OUT; + + $self->{message} .= "<img src='$image_name'"; + $self->{message} .= " alt='$params{alt}'" if (exists $params{alt}); + $self->{message} .= ">\n"; + + $self->{empty} = 0; +} + +1;