Skip to content

Instantly share code, notes, and snippets.

@mhl
Last active August 2, 2023 08:31
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mhl/4f65d8d502b2044710a62377e03c5773 to your computer and use it in GitHub Desktop.
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
#!/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 <mark-birthdays@longair.net>"
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