Last active
August 2, 2023 08:31
-
-
Save mhl/4f65d8d502b2044710a62377e03c5773 to your computer and use it in GitHub Desktop.
A very old Ruby script to email me reminders of when people's birthdays are coming up
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/ruby -w | |
# encoding: US-ASCII | |
require 'date' | |
def usage | |
STDERR.print <<EOUSAGE | |
It is intended that this script should be run from cron.daily, or | |
once every few days by some other means. | |
Usage: | |
birthdays [ -h | --help ] | |
Display this usage message and exit | |
[ -m <email-address> | --mail=<email-address> ] | |
Instead of reporting to standard output, | |
silently send a reminder by email. | |
-d <n> | --days=<n> | |
Report on any birthdays coming up in | |
the next <n> days. | |
EOUSAGE | |
end | |
require 'getoptlong' | |
options = GetoptLong.new( | |
[ "--help", "-h", GetoptLong::NO_ARGUMENT ], | |
[ "--sort", "-s", GetoptLong::NO_ARGUMENT ], | |
[ "--mail", "-m", GetoptLong::REQUIRED_ARGUMENT ], | |
[ "--days", "-d", GetoptLong::REQUIRED_ARGUMENT ] | |
) | |
mail_report_address = nil | |
days_in_advance = nil | |
birthday_line_re = Regexp.new( '^((XXXX|\d{4})-(\d\d)-(\d\d)) (.*)$' ) | |
filename = ENV['HOME'] + "/.birthdays" | |
begin | |
options.each do |opt, arg| | |
case opt | |
when "--help" | |
usage | |
exit | |
when "--mail" | |
mail_report_address = arg | |
unless mail_report_address =~ /^.+@[^\.].*$/ | |
raise "'#{mail_report_address}' doesn't look like a valid email address." | |
end | |
when "--sort" | |
lines = open( filename ) { |f| f.readlines }.find_all { |l| l =~ birthday_line_re } | |
lines.sort_by { |k| | |
k =~ birthday_line_re | |
$3 + "-" + $4 + "-" + $5 | |
}.each { |l| puts l } | |
exit | |
when "--days" | |
begin | |
days_in_advance = Integer(arg) | |
raise unless days_in_advance >= 0 | |
rescue | |
raise "The argument for -d or --days must be a non-negative integer." | |
end | |
end | |
end | |
rescue | |
STDERR.print $!.to_s + "\n\n" | |
usage | |
exit | |
end | |
unless ARGV.length == 0 | |
STDERR.puts "Extraneous command line arguments: #{ARGV.join(' ')}" | |
end | |
unless days_in_advance | |
days_in_advance = 7 | |
end | |
def fudge_feb_29(year, month, day) | |
# Just pretend that birthdays on the 29th of February in non-leap years | |
# are on the 28th, for the purposes of deciding whether they're in | |
# the next n days. | |
is_leap_year = Date.new(year, 1, 1).leap? | |
day = 28 if (day == 29) && (month == 2) && !is_leap_year | |
Date.new(year, month, day) | |
end | |
def within_next_days( day, month, days ) | |
day = Integer(day.gsub(/^0/,'')) | |
month = Integer(month.gsub(/^0/,'')) | |
today = Date.today | |
day_this_year = fudge_feb_29( today.year, month, day ) | |
day_next_year = fudge_feb_29( today.year + 1, month, day ) | |
for d in [ day_this_year, day_next_year ] | |
difference = d - today | |
if (difference >= 0) && (difference <= days) | |
return difference.round | |
end | |
end | |
return nil | |
end | |
class String | |
def to_qp | |
result = self.gsub( /=/m, '=3D' ) | |
result.gsub!( /([ \x09]+)\n/m ) { |m| | |
m.gsub( /[ \x09]/ ) { |n| | |
sprintf( '=%02X', n.unpack('C')[0] ) | |
} | |
} | |
result.gsub!( /[\x00-\x08\x09\x0b\x0d\x1f\x7f-\xff]/m ) { |m| | |
sprintf( '=%02X', m.unpack('C')[0] ) | |
} | |
result.gsub!( /(.?.?.?.{69}(=[^=]{2}|[^=]{3}))/ ) { |m| | |
m + "=\n" | |
} | |
result | |
end | |
end | |
just_people = Array.new | |
full_details = Array.new | |
open( filename ) do |f| | |
f.each do |line| | |
line.chomp! | |
line.gsub!( /#.*$/, '' ) | |
next if line =~ /^\s*$/ | |
if line =~ birthday_line_re | |
date = $1 | |
year = $2 | |
month = $3 | |
day = $4 | |
name = $5 | |
begin | |
within = within_next_days( day, month, days_in_advance ) | |
rescue | |
STDERR.puts "A date calculation failed; are you sure '#{date}'" | |
STDERR.puts "is a valid date?" | |
end | |
if within | |
just_people.push name | |
all_details = name + "'s birthday is on: #{date}\n" | |
if within == 0 | |
all_details += " (that's today)\n" | |
else | |
all_details += " (in #{within} day's time)\n" | |
end | |
if year != 'XXXX' | |
if (day != 29) && (month != 2) # Because then it's more complicated... | |
actual_birthday = (Date.today + within).year - Integer(year) | |
all_details += " (he/she will be #{(actual_birthday)})\n" | |
end | |
end | |
full_details.push all_details | |
end | |
else | |
STDERR.puts "This line is malformed: #{line}" | |
end | |
end | |
end | |
quoted_message = full_details.join("\n").to_qp | |
if mail_report_address && (full_details.length > 0) | |
subject_line = "birthdays for: " + just_people.join(', ') | |
command = [ '/sbin/exim', | |
'-bm', | |
'-i', | |
mail_report_address ] | |
Kernel.open( "|-", "w" ) do |f| | |
if f | |
f.puts "To: #{mail_report_address}" | |
f.puts "From: Birthday Reminder <[email protected]>" | |
f.puts "Subject: #{subject_line}" | |
f.puts "MIME-Version: 1.0" | |
f.puts 'Content-Type: text/plain; charset=UTF-8' | |
f.puts 'Content-Transfer-Encoding: quoted-printable' | |
f.puts | |
f.puts quoted_message | |
f.puts | |
else | |
begin | |
exec( *command ) | |
rescue | |
raise "Couldn't exec #{command.join(' ')}: #{$!}\n" | |
end | |
end | |
end | |
else | |
full_details.each do |d| | |
puts d | |
puts | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment