hpr-tools/Community_News/make_email

1579 lines
44 KiB
Perl
Executable File

#!/usr/bin/env perl
#===============================================================================
#
# FILE: make_email
#
# USAGE: ./make_email [-debug=N] [-month=DATE] [-from=ADDRESS]
# [-to=ADDRESS] [-[no]mail] [-date=DATE] [-start=START_TIME]
# [-end=END_TIME] [-config=FILE] [-dbconfig=FILE]
#
# DESCRIPTION: Make and send an invitation email for the next Community News
# with times per timezone.
#
# The configuration file (.make_email.cfg) defines the name of
# the email template and the defaults used when generating the
# message. The date of the recording is computed from the
# current month (Saturday before the first Monday of the month
# when the show will be posted). It can also be specified
# through the -date=DATE option.
#
# The month the email relates to can be changed through the
# -month=DATA option, though this is rarely used. Use a date of
# the format 'YYYY-MM-DD' here. The day is ignored but the year
# and month are used in the computation. The month specified
# must be in the future.
#
# The database configuration file defines the database to be
# used to compute the date and show number. Use .hpr_db.cfg for
# the local MariaDB copy (for testing) and .hpr_livedb.cfg for
# the live database (over the ssh tunnel, which must have been
# opened already).
#
# OPTIONS: ---
# REQUIREMENTS: ---
# BUGS: ---
#
# NOTES: Does not send the email at present. Needs work
# 2022-02-28: DBD::MariaDB has vanished, had to revert to MySQL
# again
#
# AUTHOR: Dave Morriss (djm), Dave.Morriss@gmail.com
# VERSION: 0.2.7
# CREATED: 2013-10-28 20:35:22
# REVISION: 2024-05-24 18:53:17
#
#===============================================================================
use 5.010;
use strict;
use warnings;
use Getopt::Long;
use Pod::Usage;
use Config::General;
use Date::Parse;
use DateTime;
use DateTime::TimeZone;
use DateTime::Format::Duration;
use Date::Calc qw{:all};
use Template;
use Mail::Mailer;
use DBI;
use Data::Dumper;
#
# Version number (manually incremented)
#
our $VERSION = '0.2.7';
#
# Script name
#
( my $PROG = $0 ) =~ s|.*/||mx;
#-------------------------------------------------------------------------------
# Declarations
#-------------------------------------------------------------------------------
#
# Constants and other declarations
#
my $basedir = "$ENV{HOME}/HPR/Community_News";
my $configfile1 = "$basedir/.${PROG}.cfg";
my $configfile2 = "$basedir/.hpr_db.cfg";
my ( $dbh, $sth1, $sth2, $sth3, $h1, $h2, $rv );
#
# The timezones we want to report. These were generated with
# DateTime::TimeZone->all_names(). Just uncomment the desired elements.
#
my @zones = (
#{{{
#'Africa/Abidjan',
#'Africa/Accra',
#'Africa/Addis_Ababa',
#'Africa/Algiers',
#'Africa/Asmara',
#'Africa/Bamako',
#'Africa/Bangui',
#'Africa/Banjul',
#'Africa/Bissau',
#'Africa/Blantyre',
#'Africa/Brazzaville',
#'Africa/Bujumbura',
#'Africa/Cairo',
#'Africa/Casablanca',
#'Africa/Ceuta',
#'Africa/Conakry',
#'Africa/Dakar',
#'Africa/Dar_es_Salaam',
#'Africa/Djibouti',
#'Africa/Douala',
#'Africa/El_Aaiun',
#'Africa/Freetown',
#'Africa/Gaborone',
#'Africa/Harare',
#'Africa/Johannesburg',
#'Africa/Kampala',
#'Africa/Khartoum',
#'Africa/Kigali',
#'Africa/Kinshasa',
#'Africa/Lagos',
#'Africa/Libreville',
#'Africa/Lome',
#'Africa/Luanda',
#'Africa/Lubumbashi',
#'Africa/Lusaka',
#'Africa/Malabo',
#'Africa/Maputo',
#'Africa/Maseru',
#'Africa/Mbabane',
#'Africa/Mogadishu',
#'Africa/Monrovia',
#'Africa/Nairobi',
#'Africa/Ndjamena',
#'Africa/Niamey',
#'Africa/Nouakchott',
#'Africa/Ouagadougou',
#'Africa/Porto-Novo',
#'Africa/Sao_Tome',
#'Africa/Tripoli',
#'Africa/Tunis',
#'Africa/Windhoek',
#'America/Adak',
#'America/Anchorage',
#'America/Antigua',
#'America/Araguaina',
#'America/Argentina/Buenos_Aires',
#'America/Argentina/Catamarca',
#'America/Argentina/Cordoba',
#'America/Argentina/Jujuy',
#'America/Argentina/La_Rioja',
#'America/Argentina/Mendoza',
#'America/Argentina/Rio_Gallegos',
#'America/Argentina/Salta',
#'America/Argentina/San_Juan',
#'America/Argentina/San_Luis',
#'America/Argentina/Tucuman',
#'America/Argentina/Ushuaia',
#'America/Asuncion',
#'America/Atikokan',
#'America/Bahia',
#'America/Bahia_Banderas',
#'America/Barbados',
#'America/Belem',
#'America/Belize',
#'America/Blanc-Sablon',
#'America/Boa_Vista',
#'America/Bogota',
#'America/Boise',
#'America/Cambridge_Bay',
#'America/Campo_Grande',
#'America/Cancun',
#'America/Caracas',
#'America/Cayenne',
#'America/Cayman',
'America/Chicago',
#'America/Chihuahua',
#'America/Costa_Rica',
#'America/Creston',
#'America/Cuiaba',
#'America/Curacao',
#'America/Danmarkshavn',
#'America/Dawson',
#'America/Dawson_Creek',
#'America/Denver',
#'America/Detroit',
#'America/Edmonton',
#'America/Eirunepe',
#'America/El_Salvador',
#'America/Fortaleza',
#'America/Glace_Bay',
#'America/Godthab',
#'America/Goose_Bay',
#'America/Grand_Turk',
#'America/Guatemala',
#'America/Guayaquil',
#'America/Guyana',
#'America/Halifax',
#'America/Havana',
#'America/Hermosillo',
#'America/Indiana/Indianapolis',
#'America/Indiana/Knox',
#'America/Indiana/Marengo',
#'America/Indiana/Petersburg',
#'America/Indiana/Tell_City',
#'America/Indiana/Vevay',
#'America/Indiana/Vincennes',
#'America/Indiana/Winamac',
#'America/Inuvik',
#'America/Iqaluit',
#'America/Jamaica',
#'America/Juneau',
#'America/Kentucky/Louisville',
#'America/Kentucky/Monticello',
#'America/La_Paz',
#'America/Lima',
'America/Los_Angeles',
#'America/Maceio',
#'America/Managua',
#'America/Manaus',
#'America/Martinique',
#'America/Matamoros',
#'America/Mazatlan',
#'America/Menominee',
#'America/Merida',
#'America/Metlakatla',
#'America/Mexico_City',
#'America/Miquelon',
#'America/Moncton',
#'America/Monterrey',
#'America/Montevideo',
#'America/Montreal',
#'America/Nassau',
'America/New_York',
#'America/Nipigon',
#'America/Nome',
#'America/Noronha',
#'America/North_Dakota/Beulah',
#'America/North_Dakota/Center',
#'America/North_Dakota/New_Salem',
#'America/Ojinaga',
#'America/Panama',
#'America/Pangnirtung',
#'America/Paramaribo',
#'America/Phoenix',
#'America/Port-au-Prince',
#'America/Port_of_Spain',
#'America/Porto_Velho',
#'America/Puerto_Rico',
#'America/Rainy_River',
#'America/Rankin_Inlet',
#'America/Recife',
#'America/Regina',
#'America/Resolute',
#'America/Rio_Branco',
#'America/Santa_Isabel',
#'America/Santarem',
#'America/Santiago',
#'America/Santo_Domingo',
#'America/Sao_Paulo',
#'America/Scoresbysund',
#'America/Sitka',
#'America/St_Johns',
#'America/Swift_Current',
#'America/Tegucigalpa',
#'America/Thule',
#'America/Thunder_Bay',
#'America/Tijuana',
#'America/Toronto',
#'America/Vancouver',
#'America/Whitehorse',
#'America/Winnipeg',
#'America/Yakutat',
#'America/Yellowknife',
#'Antarctica/Casey',
#'Antarctica/Davis',
#'Antarctica/DumontDUrville',
#'Antarctica/Macquarie',
#'Antarctica/Mawson',
#'Antarctica/Palmer',
#'Antarctica/Rothera',
#'Antarctica/Syowa',
#'Antarctica/Vostok',
#'Asia/Aden',
#'Asia/Almaty',
#'Asia/Amman',
#'Asia/Anadyr',
#'Asia/Aqtau',
#'Asia/Aqtobe',
#'Asia/Ashgabat',
#'Asia/Baghdad',
#'Asia/Bahrain',
#'Asia/Baku',
#'Asia/Bangkok',
#'Asia/Beirut',
#'Asia/Bishkek',
#'Asia/Brunei',
#'Asia/Choibalsan',
#'Asia/Chongqing',
#'Asia/Colombo',
#'Asia/Damascus',
#'Asia/Dhaka',
#'Asia/Dili',
#'Asia/Dubai',
#'Asia/Dushanbe',
#'Asia/Gaza',
#'Asia/Harbin',
#'Asia/Hebron',
#'Asia/Ho_Chi_Minh',
'Asia/Hong_Kong',
#'Asia/Hovd',
#'Asia/Irkutsk',
#'Asia/Jakarta',
#'Asia/Jayapura',
#'Asia/Jerusalem',
#'Asia/Kabul',
#'Asia/Kamchatka',
#'Asia/Karachi',
#'Asia/Kashgar',
#'Asia/Kathmandu',
#'Asia/Khandyga',
#'Asia/Kolkata',
#'Asia/Krasnoyarsk',
#'Asia/Kuala_Lumpur',
#'Asia/Kuching',
#'Asia/Kuwait',
#'Asia/Macau',
#'Asia/Magadan',
#'Asia/Makassar',
#'Asia/Manila',
#'Asia/Muscat',
#'Asia/Nicosia',
#'Asia/Novokuznetsk',
#'Asia/Novosibirsk',
#'Asia/Omsk',
#'Asia/Oral',
#'Asia/Phnom_Penh',
#'Asia/Pontianak',
#'Asia/Pyongyang',
#'Asia/Qatar',
#'Asia/Qyzylorda',
#'Asia/Rangoon',
#'Asia/Riyadh',
#'Asia/Sakhalin',
#'Asia/Samarkand',
#'Asia/Seoul',
#'Asia/Shanghai',
#'Asia/Singapore',
#'Asia/Taipei',
#'Asia/Tashkent',
#'Asia/Tbilisi',
#'Asia/Tehran',
#'Asia/Thimphu',
#'Asia/Tokyo',
#'Asia/Ulaanbaatar',
#'Asia/Urumqi',
#'Asia/Ust-Nera',
#'Asia/Vientiane',
#'Asia/Vladivostok',
#'Asia/Yakutsk',
#'Asia/Yekaterinburg',
#'Asia/Yerevan',
#'Atlantic/Azores',
#'Atlantic/Bermuda',
#'Atlantic/Canary',
#'Atlantic/Cape_Verde',
#'Atlantic/Faroe',
#'Atlantic/Madeira',
#'Atlantic/Reykjavik',
#'Atlantic/South_Georgia',
#'Atlantic/St_Helena',
#'Atlantic/Stanley',
#'Australia/Adelaide',
'Australia/Brisbane',
#'Australia/Broken_Hill',
#'Australia/Currie',
#'Australia/Darwin',
#'Australia/Eucla',
#'Australia/Hobart',
#'Australia/Lindeman',
#'Australia/Lord_Howe',
#'Australia/Melbourne',
'Australia/Perth',
'Australia/Sydney',
#'CET',
#'CST6CDT',
#'EET',
#'EST',
#'EST5EDT',
'Europe/Amsterdam',
#'Europe/Andorra',
#'Europe/Athens',
#'Europe/Belgrade',
#'Europe/Berlin',
#'Europe/Brussels',
#'Europe/Bucharest',
#'Europe/Budapest',
#'Europe/Chisinau',
#'Europe/Copenhagen',
#'Europe/Dublin',
#'Europe/Gibraltar',
#'Europe/Helsinki',
#'Europe/Istanbul',
#'Europe/Kaliningrad',
#'Europe/Kiev',
#'Europe/Lisbon',
'Europe/London',
#'Europe/Luxembourg',
#'Europe/Madrid',
#'Europe/Malta',
#'Europe/Minsk',
#'Europe/Monaco',
#'Europe/Moscow',
#'Europe/Oslo',
#'Europe/Paris',
#'Europe/Prague',
#'Europe/Riga',
#'Europe/Rome',
#'Europe/Samara',
#'Europe/Simferopol',
#'Europe/Sofia',
#'Europe/Stockholm',
#'Europe/Tallinn',
#'Europe/Tirane',
#'Europe/Uzhgorod',
#'Europe/Vienna',
#'Europe/Vilnius',
#'Europe/Volgograd',
#'Europe/Warsaw',
#'Europe/Zaporozhye',
'Europe/Zurich',
#'HST',
#'Indian/Antananarivo',
#'Indian/Chagos',
#'Indian/Christmas',
#'Indian/Cocos',
#'Indian/Comoro',
#'Indian/Kerguelen',
#'Indian/Mahe',
#'Indian/Maldives',
#'Indian/Mauritius',
#'Indian/Mayotte',
#'Indian/Reunion',
#'MET',
#'MST',
#'MST7MDT',
#'PST8PDT',
#'Pacific/Apia',
'Pacific/Auckland',
#'Pacific/Chatham',
#'Pacific/Chuuk',
#'Pacific/Easter',
#'Pacific/Efate',
#'Pacific/Enderbury',
#'Pacific/Fakaofo',
#'Pacific/Fiji',
#'Pacific/Funafuti',
#'Pacific/Galapagos',
#'Pacific/Gambier',
#'Pacific/Guadalcanal',
#'Pacific/Guam',
#'Pacific/Honolulu',
#'Pacific/Kiritimati',
#'Pacific/Kosrae',
#'Pacific/Kwajalein',
#'Pacific/Majuro',
#'Pacific/Marquesas',
#'Pacific/Midway',
#'Pacific/Nauru',
#'Pacific/Niue',
#'Pacific/Norfolk',
#'Pacific/Noumea',
#'Pacific/Pago_Pago',
#'Pacific/Palau',
#'Pacific/Pitcairn',
#'Pacific/Pohnpei',
#'Pacific/Port_Moresby',
#'Pacific/Rarotonga',
#'Pacific/Saipan',
#'Pacific/Tahiti',
#'Pacific/Tarawa',
#'Pacific/Tongatapu',
#'Pacific/Wake',
#'Pacific/Wallis',
'UTC',
#'WET',
#}}}
);
#
# Enable Unicode mode
#
binmode STDOUT, ":encoding(UTF-8)";
binmode STDERR, ":encoding(UTF-8)";
#
# Defaults for options
#
my $DEF_DEBUG = 0;
my $DEF_FROM = 'Dave.Morriss@gmail.com';
my $DEF_TO = 'perloid@autistici.org';
#
# Options and arguments
#
my %options;
Options( \%options );
#
# Default help
#
pod2usage( -msg => "$PROG version $VERSION\n", -verbose => 0, -exitval => 1 )
if ( $options{'help'} );
#
# Full documentation if requested with -doc
#
pod2usage(
-msg => "$PROG version $VERSION\n",
-verbose => 2,
-exitval => 1,
# -noperldoc => 0,
) if ( $options{'documentation'} );
#
# Collect options
#
my $DEBUG = ( defined( $options{debug} ) ? $options{debug} : $DEF_DEBUG );
my $month = $options{month};
my $mail = ( defined( $options{mail} ) ? $options{mail} : 0 );
my $from_address = (
defined( $options{fromaddress} ) ? $options{fromaddress} : $DEF_FROM );
my $to_address
= ( defined( $options{toaddress} ) ? $options{toaddress} : $DEF_TO );
my $date = $options{date};
my $start = $options{starttime};
my $end = $options{endtime};
# This value is in the configuration file and can't be overridden. The planned
# end time can be specified however.
#my $duration = $options{duration};
my $cfgfile
= ( defined( $options{config} ) ? $options{config} : $configfile1 );
my $dbcfgfile
= ( defined( $options{dbconfig} ) ? $options{dbconfig} : $configfile2 );
#
# Use the 'testfile' mailer if option -nomail was chosen. This writes the file
# 'mailer.testfile' and sends no message
#
my $mailertype = ( $mail ? 'sendmail' : 'testfile' );
#
# Sanity checking the options
#
die "Unable to find $cfgfile\n" unless ( -e $cfgfile );
die "Use only one of -month=MONTH or -date=DATE\n"
if (defined($month) && defined($date));
#die "Use only one of -endtime=TIME or -duration=HOURS\n"
# if (defined($end) && defined($duration));
#-------------------------------------------------------------------------------
# Load script and database configuration data
#-------------------------------------------------------------------------------
my $conf = new Config::General(
-ConfigFile => $cfgfile,
-InterPolateVars => 1,
-ExtendedAccess => 1
);
my %config = $conf->getall();
#print Dumper( \%config ), "\n";
#
# Load database configuration data
#
my $dbconf = new Config::General(
-ConfigFile => $dbcfgfile,
-InterPolateVars => 1,
-ExtendedAccess => 1
);
my %dbconfig = $dbconf->getall();
#print Dumper( \%dbconfig ), "\n";
#
# Configuration file values with defaults and/or checks
#
my $server = $config{email}->{server} // 'chatter.skyehaven.net';
my $port = $config{email}->{port} // 64738;
my $room = $config{email}->{room} // 'Hacker Public Radio';
my $duration = $config{email}->{duration} // 2;
my $dayname = $config{email}->{dayname} // 'Sunday';
#
# If we had a start time specified then check it and ensure the end time makes
# sense. Otherwise use the configuration file defaults, but don't check them.
#
if ($start) {
#
# Check start time and add a seconds field if needed
#
$start = validate_time($start);
#
# The end time usually needs to be 2 hours from the start if not
# specified. The actual duration is specified in the configuration file
# (with a default above).
#
unless ($end) {
my @end = split( ':', $start );
$end[0] += $duration;
$end = join(':',@end);
}
#
# Check and add a seconds field if needed
#
$end = validate_time($end);
}
else {
$start = $config{email}->{starttime};
$end = $config{email}->{endtime};
}
#
# Start and end times from options or the configuration file
#
my @starttime = split( ':', $start );
my @endtime = split( ':', $end );
die "Missing start/end time(s)\n" unless ( @starttime && @endtime );
my $template = $config{email}->{template};
die "Missing template file $template\n" unless (-e $template);
_debug($DEBUG >= 2,
'$start: ' . coalesce($start,''),
'$end: ' . coalesce($end,''),
'--'
);
if ($DEBUG >= 1) {
report_settings();
}
#-------------------------------------------------------------------------------
# Connect to the database
# 2021-12-24: moved to MariaDB
# 2022-02-28: the MariaDB driver has gone away apparently. Reverted to MySQL
# again
#-------------------------------------------------------------------------------
my $dbhost = $dbconfig{database}->{host} // '127.0.0.1';
my $dbport = $dbconfig{database}->{port} // 3306;
my $dbname = $dbconfig{database}->{name};
my $dbuser = $dbconfig{database}->{user};
my $dbpwd = $dbconfig{database}->{password};
#$dbh = DBI->connect( "DBI:MariaDB:database=$dbname;host=$dbhost;port=$dbport",
# $dbuser, $dbpwd, { AutoCommit => 1 } )
# or croak $DBI::errstr;
$dbh = DBI->connect( "dbi:mysql:host=$dbhost;port=$dbport;database=$dbname",
$dbuser, $dbpwd, { AutoCommit => 1 } )
or die $DBI::errstr;
#
# Enable client-side UTF8
#
$dbh->{mysql_enable_utf8} = 1;
#
# Date and time values using Date::Calc format
#
my @today = Today();
my @startdate;
my @startmonth;
my @reviewdate;
my $monday = 1; # Day of week number 1-7, Monday-Sunday
my $offset = day_offset($dayname)->{offset};
#-------------------------------------------------------------------------------
# Work out the start date from the -date=DATE option, the -month=DATE option
# or the current date.
#-------------------------------------------------------------------------------
#
# If there's an argument then it'll be an override for the start date
# otherwise we'll compute it.
#
if ( defined($date) ) {
#
# Parse and perform rudimentary validation on the argument
#
my @parsed = strptime($date);
die "Invalid -date=DATE option '$date'\n"
unless ( defined( $parsed[3] )
&& defined( $parsed[4] )
&& defined( $parsed[5] ) );
$parsed[5] += 1900;
$parsed[4] += 1;
@startdate = @parsed[ 5, 4, 3 ];
die "Date is in the past '$date'; aborting\n"
unless ( Date_to_Days(@startdate) > Date_to_Days(@today) );
}
elsif ( defined($month) ) {
#
# Parse the month out of the -month=DATE argument
#
my @parsed = strptime($month);
die "Invalid -month=DATE option '$month'\n"
unless ( defined( $parsed[3] )
&& defined( $parsed[4] )
&& defined( $parsed[5] ) );
$parsed[5] += 1900;
$parsed[4] += 1;
@startmonth = @parsed[ 5, 4, 3 ];
die "Date is in the past '$month'; aborting\n"
unless ( Date_to_Days(@startmonth) > Date_to_Days(@today) );
#
# Compute the next meeting date from now (by finding the next first Monday
# of the month then backing up two days to the Saturday).
#
@startdate = make_date( \@startmonth, $monday, 1, $offset );
}
else {
#
# Compute the next meeting date from now (by finding the next first Monday
# of the month then backing up a number of days to the required date).
#
@startdate = make_date( \@today, $monday, 1, $offset );
}
_debug($DEBUG >= 2, '@startdate: ' . join(',',@startdate));
#
# The month being reviewed is sometimes the same month and sometimes the month
# before.
#
if ( $startdate[1] eq $today[1] ) {
@reviewdate = @startdate;
}
else {
@reviewdate = Add_Delta_YM( @startdate, 0, -1 );
}
_debug($DEBUG >= 2, '@reviewdate: ' . join(',',@reviewdate));
#
# Transfer Date::Calc values into hashes for initialising DateTime objects so
# we can play time zone games
#
my ( %dtargs, $dtstart, $dtend );
@dtargs{ 'year', 'month', 'day', 'hour', 'minute', 'second', 'time_zone' }
= ( @startdate, @starttime, 'UTC' );
$dtstart = DateTime->new(%dtargs);
@dtargs{ 'hour', 'minute', 'second' } = (@endtime);
$dtend = DateTime->new(%dtargs);
#
# Compute the number of days until the recording
#
my $dtnow = DateTime->now( time_zone => 'UTC' );
my $dtoffset = $dtstart->delta_days($dtnow);
my $dtf = DateTime::Format::Duration->new( pattern => '%e' );
my $days = $dtf->format_duration($dtoffset);
#
# Formatted dates for the mail message body
#
my ( $year, $monthname, $nicedate, $starttime, $endtime ) = (
$dtstart->strftime("%Y"), Month_to_Text( $reviewdate[1] ),
$dtstart->strftime("%A, %B %d %Y"), $dtstart->strftime("%R (%Z)"),
$dtend->strftime("%R (%Z)"),
);
_debug($DEBUG >= 2,
"\$year: $year",
"\$monthname: $monthname",
"\$nicedate: $nicedate",
"\$starttime: $starttime",
"\$endtime: $endtime"
);
#
# Build the subject line
#
my $waittime = ( $days > 6 ? "in $days days" : "next %A" );
my $next = ( $days > 6 ? '' : 'next ' );
my $forspec = ( $days > 6 ? "for $monthname " : "" );
my $subject = $dtstart->strftime(
"HPR Community News ${forspec}- $waittime on %FT%TZ");
_debug( $DEBUG >= 2, "\$subject: $subject" );
#
# Prepare to send mail
#
my $mailer = Mail::Mailer->new($mailertype);
#
# Generate the headers we need
#
$mailer->open(
{ To => $to_address,
From => $from_address,
Subject => $subject,
}
);
#
# Build an array of timezone data for the template
#
my @timezones;
for my $tz (@zones) {
push( @timezones, storeTZ( $dtstart, $dtend, $tz ) );
}
#-------------------------------------------------------------------------------
# Find the number of the show with the notes. Take care because the recording
# date might not be on the weekend before the show is released.
#-------------------------------------------------------------------------------
my $isodate = $dtstart->ymd;
$sth1 = $dbh->prepare(q{
SELECT id FROM eps
WHERE date > ?
AND date_format(date,"%W") = 'Monday'
AND title LIKE 'HPR Community News%'
ORDER BY date
LIMIT 1
});
$sth1->execute($isodate);
if ( $dbh->err ) {
warn $dbh->errstr;
}
_debug( $DEBUG >= 2, "\$isodate: $isodate" );
unless ( $h1 = $sth1->fetchrow_hashref ) {
warn "Unable to find a reserved show on the specified date - cannot continue\n";
exit 1;
}
my $shownotes = $h1->{id};
_debug( $DEBUG >= 2, "\$shownotes (slot): $shownotes" );
$sth1->finish;
$dbh->disconnect;
#-------------------------------------------------------------------------------
# Fill the template
#-------------------------------------------------------------------------------
my $tt = Template->new(
{ ABSOLUTE => 1,
ENCODING => 'utf8',
}
);
my $vars = {
# subject => $subject,
# from => $from_address,
# to => $to_address,
server => $server,
port => $port,
room => $room,
timezones => \@timezones,
utc => {
days => $days,
month => $monthname,
year => $year,
date => $nicedate,
start => $starttime,
end => $endtime,
},
shownotes => $shownotes,
};
my $document;
$tt->process( $template,
$vars, \$document, { binmode => ':utf8' } )
|| die $tt->error(), "\n";
#
# Add the template-generated body to the mail message
#
print $mailer $document;
#
# Send the message
#
$mailer->close
or die "Couldn't send message: $!\n";
unless ($mail) {
print "Message was not sent since -nomail was selected (or defaulted).\n";
print "Look in 'mailer.testfile' for the output\n";
}
exit;
#=== FUNCTION ================================================================
# NAME: report_settings
# PURPOSE: Report settings from options (or defaults)
# PARAMETERS: None
# RETURNS: Nothing
# DESCRIPTION:
# THROWS: No exceptions
# COMMENTS: None
# SEE ALSO: N/A
#===============================================================================
sub report_settings {
my $fmt = "D> %-14s = %s\n";
print "D> Settings from options or default values:\n";
printf $fmt, "Month", coalesce($month,'undef');
printf $fmt, "Mail", coalesce($mail,'undef');
printf $fmt, "From", coalesce($from_address,'undef');
printf $fmt, "To", coalesce($to_address,'undef');
printf $fmt, "Meeting date", coalesce($date,'undef');
printf $fmt, "Start time", join(':',@starttime);
printf $fmt, "End time", join(':',@endtime);
printf $fmt, "Config file", coalesce($cfgfile,'undef');
printf $fmt, "DB config file", coalesce($dbcfgfile,'undef');
printf $fmt, "Server", coalesce($server,'undef');
printf $fmt, "Port", coalesce($port,'undef');
printf $fmt, "Room", coalesce($room,'undef');
printf $fmt, "Template", coalesce($template,'undef');
print "D> ----\n";
}
#=== FUNCTION ================================================================
# NAME: compute_endtime
# PURPOSE: Given a start time and a duration computes the end time
# PARAMETERS: $rdate arrayref for the date
# $rstime arrayref for the start time
# $rduration arrayref for the duration [HH,MM,SS]
# RETURNS: The end time as a string (HH:MM:SS)
# DESCRIPTION:
# THROWS: No exceptions
# COMMENTS: Decided not to implement this, may do so in future.
# SEE ALSO: N/A
#===============================================================================
#sub compute_endtime {
#}
#=== FUNCTION ================================================================
# NAME: validate_time
# PURPOSE: Validates a time in HH:MM:SS format
# PARAMETERS: $time Time string
# RETURNS: The input string with any missing fields added in
# DESCRIPTION: The input time needs to be in the format HH:MM[:SS] where
# a missing seconds value is replaced with '00'. A regex check
# on the format is performed, and if that passes a check is made
# on the time values (using 'check_time').
# THROWS: No exceptions
# COMMENTS: None
# SEE ALSO: N/A
#===============================================================================
sub validate_time {
my ($time) = (@_);
if ( defined($time) ) {
if ( ( my @fields )
= ( $time =~ /(\d{1,2}):(\d{1,2})(?::(\d{1,2}))?/ ) )
{
@fields = map { defined($_) ? sprintf('%02d',$_) : "00" } @fields;
$time = join( ':', @fields );
}
else {
die "Invalid time: $time\n";
}
unless ( check_time(split(':',$time)) ) {
die "Invalid time: $time\n";
}
}
return $time;
}
#=== FUNCTION ================================================================
# NAME: make_date
# PURPOSE: Make the event date for recurrence
# PARAMETERS: $refdate
# An arrayref to the reference date array (usually
# today's date)
# $dow Day of week for the event date (1-7, 1=Monday)
# $n The nth day of the week in the given month required
# for the event date
# $offset Number of days to offset the computed date
# RETURNS: The resulting date as a list for Date::Calc
# DESCRIPTION: We want to compute a simple date with an offset, such as
# "the Saturday before the first Monday of the month". We do
# this by computing a pre-offset date (first Monday of month)
# then apply the offset (Saturday before).
# THROWS: No exceptions
# COMMENTS: This function was originally written for my HPR episode on
# iCalendar.
# TODO Needs more testing to be considered truly universal
# SEE ALSO:
#===============================================================================
sub make_date {
my ( $refdate, $dow, $n, $offset ) = @_;
#
# Compute the required date: the "$n"th day of week "$dow" in the year and
# month in @$refdate. This could be a date in the past.
#
my @date = Nth_Weekday_of_Month_Year( @$refdate[ 0, 1 ], $dow, $n );
#
# If the computed date plus the offset is before the base date advance
# a month
#
if ( Day_of_Year(@date) + $offset < Day_of_Year(@$refdate) ) {
#
# Add a month and recompute
#
@date = Add_Delta_YM( @date, 0, 1 );
@date = Nth_Weekday_of_Month_Year( @date[ 0, 1 ], $dow, $n );
}
#
# Apply the day offset
#
@date = Add_Delta_Days( @date, $offset ) if $offset;
#
# Return a list
#
return (@date);
}
#=== FUNCTION ================================================================
# NAME: storeTZ
# PURPOSE: Store start/end times for a timezone
# PARAMETERS: $start DateTime object containing the starting
# datetime as UTC
# $end DateTime object containing the ending datetime
# as UTC
# $tz The textual time zone (need this to be valid)
# RETURNS: A hash containing the timezone name and start and end times
# DESCRIPTION: Relies on DateTime::TimeZone to do the work to turn a UTC time
# into a time in a different time zone. Uses the DateTime
# strftime method to format the dates and times for printing.
# THROWS: No exceptions
# COMMENTS: None
# SEE ALSO: N/A
#===============================================================================
sub storeTZ {
my ( $start, $end, $tz ) = @_;
my %result;
#
# Adjust time zone
#
$start->set_time_zone($tz);
$end->set_time_zone($tz);
#
# Print time zone and start/end times in that zone
#
$result{name} = "$tz";
$result{start} = $start->strftime("%H:%S %a, %b %d %Y");
$result{end} = $end->strftime("%H:%S %a, %b %d %Y");
return \%result;
}
#=== FUNCTION ================================================================
# NAME: printTZ
# PURPOSE: Print start/end times for a timezone
# PARAMETERS: $fh File handle for writing
# $start DateTime object containing the starting
# datetime as UTC
# $end DateTime object containing the ending datetime
# as UTC
# $tz The textual time zone (need this to be valid)
# RETURNS: Nothing
# DESCRIPTION: Relies on DateTime::TimeZone to do the work to turn a UTC time
# into a time in a different time zone. Uses the DateTime
# strftime method to format the dates and times for printing.
# THROWS: No exceptions
# COMMENTS: None
# SEE ALSO:
#===============================================================================
sub printTZ {
my ( $fh, $start, $end, $tz ) = @_;
#
# Adjust time zone
#
$start->set_time_zone($tz);
$end->set_time_zone($tz);
#
# Print time zone and start/end times in that zone
#
print $fh "$tz\n";
print $fh $start->strftime("Start: %H:%S %a, %b %d %Y\n");
print $fh $end->strftime("End: %H:%S %a, %b %d %Y\n\n");
}
#=== FUNCTION ================================================================
# NAME: day_offset
# PURPOSE: Given a day name computes day attributes including the
# (negative) offset in days from the target Monday to the
# recording date.
# PARAMETERS: $dayname Name of a day of the week
# RETURNS: Hashref containing the full day name, the weekday number and
# the integer offset from Monday to the recording day, or undef.
# DESCRIPTION: Uses the hash '%matches' keyed by regular expressions matching
# day names. The argument '$dayname' is matched against each
# regex in turn and if it matches the sub-hash is returned. This
# allows the caller to use 'day_offset($dayname)->{dayname}' to
# get the full name of the day if needed, or the offset as
# 'day_offset($dayname)->{offset}'. If there is no match
# 'undef' is returned.
# THROWS: No exceptions
# COMMENTS: None
# SEE ALSO: N/A
#===============================================================================
sub day_offset {
my ( $dayname ) = @_;
my %matches = (
qr{^(?i)Sun(day)?$} => {
dayname => 'Sunday',
wday => 7,
offset => -1,
},
qr{^(?i)Sat(urday)?$} => {
dayname => 'Saturday',
wday => 6,
offset => -2,
},
qr{^(?i)Fri(day)?$} => {
dayname => 'Friday',
wday => 5,
offset => -3,
},
qr{^(?i)Thu(rsday)?$} => {
dayname => 'Thursday',
wday => 4,
offset => -4,
},
qr{^(?i)Wed(nesday)?$} => {
dayname => 'Wednesday',
wday => 3,
offset => -5,
},
qr{^(?i)Tue(sday)?$} => {
dayname => 'Tuesday',
wday => 2,
offset => -6,
},
qr{^(?i)Mon(day)?$} => {
dayname => 'Monday',
wday => 1,
offset => -7,
},
);
my $match;
foreach my $re (keys(%matches)) {
if ($dayname =~ $re) {
$match = $matches{$re};
last;
}
}
return $match;
}
#=== FUNCTION ================================================================
# NAME: _debug
# PURPOSE: Prints debug reports
# PARAMETERS: $active Boolean: 1 for print, 0 for no print
# $messages... Arbitrary list of messages to print
# RETURNS: Nothing
# DESCRIPTION: Outputs messages if $active is true. It removes any trailing
# newline from each one and then adds one in the 'print' to the
# caller doesn't have to bother. Prepends each message with 'D>'
# to show it's a debug message.
# THROWS: No exceptions
# COMMENTS: Differs from other functions of the same name
# SEE ALSO: N/A
#===============================================================================
sub _debug {
my $active = shift;
my $message;
return unless $active;
while ($message = shift) {
chomp($message);
print STDERR "D> $message\n";
}
}
#=== FUNCTION ================================================================
# NAME: coalesce
# PURPOSE: To find the first defined argument and return it
# PARAMETERS: Arbitrary number of arguments
# RETURNS: The first defined argument or undef if there are none
# DESCRIPTION:
# THROWS: No exceptions
# COMMENTS: None
# SEE ALSO: N/A
#===============================================================================
sub coalesce {
foreach (@_) {
return $_ if defined($_);
}
return undef; ## no critic
}
#=== FUNCTION ================================================================
# NAME: Options
# PURPOSE: Processes command-line options
# PARAMETERS: $optref Hash reference to hold the options
# RETURNS: Undef
# DESCRIPTION:
# THROWS: no exceptions
# COMMENTS: none
# SEE ALSO: n/a
#===============================================================================
sub Options {
my ($optref) = @_;
my @options = (
"help", "documentation|man",
"debug=i", "mail!",
"fromaddress=s", "toaddress=s",
"date=s", "starttime=s",
"endtime=s", "month=s",
"config=s", "dbconfig=s",
);
# "duration=s",
if ( !GetOptions( $optref, @options ) ) {
pod2usage( -msg => "Version $VERSION\n", -verbose => 0, -exitval => 1 );
}
return;
}
__END__
#%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
# Application Documentation
#%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
#{{{
=head1 NAME
make_email - generates an HPR Community News recording invitation email
=head1 VERSION
This documentation refers to make_email version 0.2.7
=head1 USAGE
make_email [-help] [-documentation] [-debug=N] [-month=DATE] [-[no]mail]
[-from=FROM_ADDRESS] [-to=TO_ADDRESS] [-date=DATE] [-start=START_TIME]
[-end=END_TIME] [-config=FILE] [-dbconfig=FILE]
./make_email -dbconf=$HOME/HPR/.hpr_livedb.cfg -date=2022-12-27
=head1 OPTIONS
=over 8
=item B<-help>
Prints a brief help message describing the usage of the program, and then exits.
=item B<-documentation> B<-man>
Prints the entire embedded documentation for the program, then exits.
Another way to see the full documentation use:
B<perldoc ./make_email>
=item B<-debug=N>
Enables debugging mode when N > 0 (zero is the default, no debugging output).
The levels are:
Values are:
=over 4
=item 1
Reports all of the settings taken from the configuration file, the provided
command line options or their default values. The report is generated early on
in the processing of these values. Use B<-debug=2> for information about the
next stages.
=item 2
Reports the following (as well as the data for level 1):
=over 4
=item .
Details of the start date chosen
=item .
Details of the year, name of month, readable date, and recording start and end
times.
=item .
The subject line chosen for the email.
=item .
The date of the show being searched for in the database.
=item .
The number of the show found in the database.
=back
=back
=item B<-month=DATE>
Defines the month for which the email will be generated using a date in that
month. Normally (without this option) the current month is chosen and the date
of recording computed within it. The month specified here is provided as
a ISO8601 date such as 2014-03-08 (meaning March 2014) or 1-Jan-2017 (meaning
January 2017). Only the year and month parts are used but a valid day must be
present.
=item B<-[no]mail>
** NOTE ** The sending of mail does not work at present, and B<-nomail> should
always be used.
Causes mail to be sent (B<-mail>) or not sent (B<-nomail>). If the mail is
sent then it is sent via the local MTA (in the assumption that there is one).
If this option is omitted, the default is B<-nomail>, in which case the
message is appended to the file B<mailer.testfile> in the current directory.
=item B<-from=FROM_ADDRESS>
** NOTE ** The sending of mail does not work at present.
This option defines the address from which the message is to be sent. This
address is used in the message header; the message envelope will contain the
I<real> sender.
=item B<-to=TO_ADDRESS>
** NOTE ** The sending of mail does not work at present.
This option defines the address to which the message is to be sent.
=item B<-date=DATE>
This is an option provides a non-default date for the recording. Normally the
script computes the next scheduled date based on the algorithm "DAY_OF_WEEK
before the first Monday of the next month" (where DAY_OF_WEEK is the value
defined in the configuration file as B<dayname>) starting from the current
date or the start of the month given in the B<-month=DATE> option. If for any
reason a different date is required then this may be specified via this
option.
The recording date should be given as an ISO8601 date (such as 2014-03-08).
=item B<-start=START_TIME>
The default start time is defined in the configuration file, but if it is
necessary to change it, this option can be used to do it. The B<START_TIME>
value must be a valid B<HH:MM> time specification.
A change to the start time in the configuration file also implies that the end
time should change. If the B<-start=START_TIME> option is present but
B<-end=END_TIME> is not, then the end time is computed by adding a number of
hours to the start time this number is defined in the configuration file as
B<duration>.
=item B<-end=END_TIME>
The default end time is defined in the configuration file, but if it is
necessary to change it temporarily, this option can be used to do it. The
B<END_TIME> value must be a valid B<HH:MM> time specification.
=item B<-config=FILE>
This option defines a configuration file other than the default
B<.make_email.cfg>. The file must be formatted as described below in the
section I<CONFIGURATION AND ENVIRONMENT>.
=item B<-dbconfig=FILE>
This option defines a database configuration file other than the default
B<.hpr_db.cfg>. The file must be formatted as described below in the section
I<CONFIGURATION AND ENVIRONMENT>.
The default file is configured to open a local copy of the HPR database. An
alternative is B<.hpr_livedb.cfg> which assumes an SSH tunnel to the live
database and attempts to connect to it. Use the script I<open_tunnel> to open
the SSH tunnel.
=back
=head1 DESCRIPTION
Makes and sends(*) an invitation email for the next Community News with times per
timezone. The message is structured by a Template Toolkit template, so its
content can be adjusted without changing this script.
In normal operation the script computes the date of the next recording using
the algorithm "Saturday before the first Monday of the next month" starting
from the current date or the start of the month (and year) given in the
B<-month=DATE> option.
It uses the recording date (B<-date=DATE> option) to access the MySQL database
to find the date on which the show will be released. It does that so the notes
on that show can be viewed by the volunteers recording the show. These notes
are expanded to be usable during the recording, with comments relating to
earlier shows being displayed in full, and any comments missed in the last
recording highlighted. Comments made to shows during the past month can be
seen as the shows are visited and discussed.
The email generated by the script is sent to the HPR mailing list, usually on
the Monday prior to the weekend of the recording.
Notes:
* Mail sending does not work at present.
=head1 DIAGNOSTICS
=over 8
=item B<Unable to find ...>
The configuration file specified in B<-config=FILE> (or the default file)
could not be found.
=item B<Use only one of -month=MONTH or -date=DATE>
These options are mutually exclusive. See their specifications earlier in this
document.
=item B<Missing start/end time(s)>
One or both of the start and end times is missing, either from the configuration file or
from the command line options.
=item B<Missing template file ...>
The template file specified in the configuration file could not be found.
=item B<Various database messages>
The program can generate warning messages from the database.
=item B<Invalid -date=DATE option '...'>
An invalid date has been supplied via this option.
=item B<Date is in the past '...'>
The date specified in B<-date=DATE> is in the past.
=item B<Invalid -month=DATE option '...'>
An invalid date has been supplied via this option.
=item B<Date is in the past '...'>
The month specified in B<-month=DATE> is in the past.
=item B<Various Template Toolkit messages>
The program can generate warning messages from the Template.
=item B<Couldn't send message: ...>
The email mesage has been constructed but could not be sent. See the error
returned by the mail subsystem for more information.
=back
=head1 CONFIGURATION AND ENVIRONMENT
=head2 EMAIL CONFIGURATION
The program obtains the settings it requires for preparing the email from
a configuration file, which by default is called B<.make_email.cfg>. This file
needs to contain the following data:
<email>
server = MUMBLE_SERVER_NAME
port = MUMBLE_PORT
room = NAME_OF_ROOM
dayname = DAY_OF_WEEK_OF_RECORDING
starttime = 18:00:00
endtime = 20:00:00
duration = 02 # hours
template = NAME_OF_TEMPLATE
</email>
=head2 DATABASE CONFIGURATION
The program obtains the credentials it requires for connecting to the HPR
database by loading them from a configuration file. The default file is called
B<.hpr_db.cfg> and should contain the following data:
<database>
host = 127.0.0.1
port = PORT
name = DBNAME
user = USER
password = PASSWORD
</database>
The file B<.hpr_livedb.cfg> should be available to allow access to the
database over an SSH tunnel which has been previously opened.
=head1 DEPENDENCIES
DBI
Date::Calc
Date::Parse
DateTime
DateTime::Format::Duration
DateTime::TimeZone
Getopt::Long
Mail::Mailer
Pod::Usage
Template
=head1 BUGS AND LIMITATIONS
There are no known bugs in this script.
Please report problems to Dave Morriss (Dave.Morriss@gmail.com)
Patches are welcome.
=head1 AUTHOR
Dave Morriss (Dave.Morriss@gmail.com) 2013 - 2024
=head1 LICENCE AND COPYRIGHT
Copyright (c) Dave Morriss (Dave.Morriss@gmail.com). All rights reserved.
This program is free software. You can redistribute it and/or modify it under
the same terms as perl itself.
=cut
#}}}
# [zo to open fold, zc to close]
# vim: syntax=perl:ts=8:sw=4:et:ai:tw=78:fo=tcrqn21:fdm=marker