[04/12] statusmail: Supporting files

Message ID 20190405172940.13168-5-ipfr@tfitzgeorge.me.uk
State Dropped
Headers
Series statusmail: Status and Log Summary Emails |

Commit Message

Tim FitzGeorge April 6, 2019, 4:29 a.m. UTC
  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
  

Patch

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;