Ruby: Send Email Reminders from iCal Calendar

This is going to be long, so hunker down for a good read.

Problem: Our Cub Scout den leader sends email reminders to all the parent about upcoming events. I decided to setup a gCal (google calendar) will all the events so that other parents could just check the calendar as they wish, or subscribe to the ical file to integrate with their own calendar. But several of the parents don’t have calendars (shame, shame) and everyone still liked the idea of receiving email reminders. So I decided to write a ruby script to email reminders automatically two days before the event.

Solution: Ruby is a great language to handle this. Especially since there are gems to handle many of the functions we need. Some of the gems we will be using are as follows:

  • open-uri: to get the ical file via http
  • net/smtp: to send the email, you could use ActionMailer as well
  • erb: email body is in a template.html.erb file for easy editing, erb processes the file
  • yaml: a list of sent notifications are logged in a yaml file
  • icalendar: the superb ical manipulation gem from Jeff Rose

All you need to do is setup a cron job to run this script. I chose to run the script at 8pm every evening.

I’m going to break this script apart by methods for easier commenting. You can download the complete script file and templates to follow along. In the single script file is logging and some formatting methods that don’t really need explanation here.

The script all assembled with some nice logging: notification_script.rb
A sample email template file: mail_template.html.erb
A sample recipient groupt list: group_list.txt

Setup and Requires

First, you are going to need the icalendar gem (documentation), which you can install with:

[sourcecode language=’ruby’]
sudo gem install icalendar
[/sourcecode]

For the beginning of our script, we’ll need a few other libraries to be required, and set a constant or two.
[sourcecode language=’ruby’]
require ‘rubygems’
require ‘open-uri’
require ‘icalendar’
require ‘date’
require ‘net/smtp’
require ‘erb’
require ‘yaml’

ICAL_URL = ‘http://www.google.com/calendar/ical…path_to_your_ical_file’
FROM_EMAIL = ‘[email protected]
MAIL_TEMPLATE_FILE = ‘mail_template.html.erb’
SENT_NOTIFICATIONS_FILE = ‘sent_notifications.yml’
GROUP_LIST_FILE = ‘group_list.txt’
DAYS_BEFORE_TO_SEND = 2
[/sourcecode]

All of these requires should already be installed in your standard ruby library. Following the requires are some constants you need to edit. The ICAL_URL constant is the url that you can look up from google calendars by going to your Calendar Settings, and selecting Calendar Details. You should see the ICAL (green) button that when you click, it shows you the url for the ical file of your calendar. Copy and paste this url into the ICAL_URL constant. You will also want to change the FROM_EMAIL to your email address, or the one you want notifications to be from.

The other constants are the names of the files that should be in the same directory as the script. You can rename them or change the paths as necessary.

The Main Event

I like to wrap my main events in a method called ‘init()’ and place it near the top of the script file so that it is quick and easy to find. You don’t have to do this, but supporting methods will have to come before your code.

[sourcecode language=’ruby’]
def init
begin
myFile = open(ICAL_URL).read

cals = Icalendar.parse(myFile)
cal = cals.first

cal.events.each{ |e|
time_diff = e.dtstart – DateTime.now
if time_diff < (DAYS_BEFORE_TO_SEND * 24 * 60 * 60) &amp;&amp; time_diff > 0

if notified?(e.uid)
#log ‘ notification previousy sent.’

else
send_to = group_list
msg = ERB.new(File.read( MAIL_TEMPLATE_FILE )).result( binding )

send_email(‘Den 4 Notifications’, ‘[email protected]’, send_to, e.summary, msg)

add_notification_to_list(e.uid)
end
end
}

rescue
#log the error
end
end
[/sourcecode]

Pretty easy, huh? Let start breaking this down in greater detail:

Step 1: Read the iCal File

Read the contents of the ical file at the ICAL_URL location. Using the open-uri library, the content of the ical file is read into a string object.

Step 2: Parse the iCal File with Icalendar

The Icalendar gem first requires that we parse the ical file, so that it can create the representative ‘calendar’ and ‘event’ objects for reading. There is the potential of more than one calendar in the ics file. But in our case, we know there is only one, so we need to get that first ‘calendar’ object.

Step 3: Loop Through the Calendar Events

We need to look at every calendar ‘event’ object in the calendar so that we can look at the start date (e.dtstart) so see if it’s within the DAYS_BEFORE_TO_SEND.

Step 4: Previous Notification Sent? Ready Email Template

The ‘notified?’ method will tell us if this event has received previous notification. The e.uid attribute is a unique identifier associated with every event (an event id, if you will). We check that e.uid against a list of saved uid’s.

Then we can read the MAIL_TEMPLATE_FILE and use erb to process the file, substituting the various object attributes (variables). This makes it easy to change the email template without editing our script file.

Step 5: Send the Email Message

A Couple of things happen here that we’ll discuss in the ‘send_email’ method, but suffice it to say that this sends the email to all the addresses in the GROUP_LIST_FILE.

Step 6: Add Notification to List

We save a list of the event uids (e.uid) that have been notified so that we don’t send a notification again.

Pretty spiffy, huh?

Now lets show some of the supporting methods and discuss.

Method: notified?

This method reads the SENT_NOTIFICATIONS_FILE yaml file to see if the passed ‘uid’ exists in the list of events with previous notification. The loop looks at each item in the ‘y_content’ array to see if the ‘uid’ is a match. If there is a match, return true (I like the explicitness of using return here). You can see how the yaml file is structured in the ‘add_notification_to_list’ method.
[sourcecode language=’ruby’]
def notified?(uid)
if File.exist?( SENT_NOTIFICATIONS_FILE )
File.open( SENT_NOTIFICATIONS_FILE ) { |yf|
y_content = YAML.load( yf )

if y_content.is_a? Array
y_content.each{ |e| return true if e[:uid] == uid }
end
}
end

return false #not found
end
[/sourcecode]

Method: group_list

The GROUP_LIST_FILE is a text file with the address of whom we want to send the notification emails to, separated by a linefeed. You can change this to suit your needs, like comma or pipe separation. It returns the list of email addresses in an array.

If the GROUP_LIST_FILE does not exist or there is an error reading the file, a default email address is returned so that at least someone gets notified. I would normally not do it his way and let the rescue raise an error and halt the script.
[sourcecode language=’ruby’]
def group_list
begin
IO.read( GROUP_LIST_FILE ).split(/\n/)
rescue
return Array[‘[email protected]’]
end
end
[/sourcecode]

Method: send_email

Here we get to use the net/smtp gem to send the email. I chose this because it has fairly low overhead and it actually pretty easy to use. I’m not sure you can use it to send attachments, but in our case, we are just sending HTML emails.

A heredoc is used to create the string for the message header. I’m not such a fan of heredoc, so this is my one and only time I’ll use it.

Notice that the ‘Content-Type: text/html’ is set so that the receive recognizes this as an HTML email and not plain text. Use ‘Content-Type: text/plain’ if you’re sending a text only email.

You will have to set your SMTP server setting were you see ‘mail.my_server.com’. For more information on this, see the NET::SMTP documentation. Because there are many configurations for this, I didn’t place constants at the beginning of the script. The NET::SMTP docs pretty much cover it.

I would have liked to send all the messages using BCC, but alas I could find no documentation in NET::SMTP supporting BCC. In my case, everyone know everyone else, so it’s no big whoop.
[sourcecode language=’ruby’]
def send_email(from_alias, from, to_array, subject, message)
msg = <
To: #{to_array.collect{ |item| ‘<' + item + '>‘ }.join(‘,’)}
Subject: [CUB SCOUTS]#{subject}
Content-Type: text/html

#{message}
END_OF_MESSAGE

Net::SMTP.start( ‘mail.my_server.com’, 25 ) do |smtp|
smtp.send_message msg, from, to_array
end
end
[/sourcecode]

Method: add_notification_to_list

Here we add an event uid to a yaml file and timestamp it. I’m sure there are a million ways to do this, but I just wanted to play around reading/writing yaml files.

There is one thing I don’t like about this though… the entire yaml file is read in, the new event is added to the end, and the resulting yaml replaces the entire existing file. I could not find a reliable way to append a yaml object to an existing yaml file. Well, sort of, but it creates an new array object hash for each append, instead of having all events in one array of hashes. Yeah, yeah, I’m being picky.
[sourcecode language=’ruby’]
def add_notification_to_list(uid)
if File.exists?( SENT_NOTIFICATIONS_FILE )
y_content = YAML.load( File.open( SENT_NOTIFICATIONS_FILE ) )
end

y_content = [] unless y_content.is_a?( Array )

File.open( SENT_NOTIFICATIONS_FILE, ‘w+’ ) { |yf|
y_content.concat [ :date => Time.now, :uid => “#{uid}” ]
YAML.dump( y_content, yf )
}
end
[/sourcecode]

Put it All Together…

The script all assembled with some nice logging: notification_script.rb
A sample email template file: mail_template.html.erb
A sample recipient groupt list: group_list.txt

All that’s left is to put all the files in a directory that has read/write access (for the log file and notification list file) and setup a cron job. Now you have automated notifications from your calendar.

Posted in Ruby. Tagged with , , , , .

One Response