forked from HPR/hpr-tools
		
	
		
			
				
	
	
		
			1579 lines
		
	
	
		
			44 KiB
		
	
	
	
		
			Perl
		
	
	
		
			Executable File
		
	
	
	
	
			
		
		
	
	
			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
 |