Importing iCal Into Org-mode

Date: 03 June 2014

Updated: 03 February 2016

I’ve been using emacs and org-mode for some time to manage my tasks. Org-mode has a great feature which shows and agenda view which includes upcoming scheduled items and deadlines. One of the things that was missing was the ability to view my calendar (which is in Google Calendar) in the agenda.

There are a couple of ways of dealing the syncing the calendar data. One of the ways I tried was org-caldav. It kind of worked. Sort of. It did import the caledar but it failed spectaculary with repeating tasks set in Google by me or others. Since most of the things on my calendar are repeating events, this was a problem.

Alright, so org-caldav didn’t work for me. I could have looked for something else that did two-way sync but, in the end, it wasn’t that important to me. So, I worked up a way to do pull the iCal feed from Google and convert it into an org-mode file.

The first is a pretty simple script that pulls down the iCal files and pumps it through the translation script. I run this from cron every ten minutes.

my $debug = 0;

my $wget = "/usr/bin/wget"; my $ical2org = "$ENV{HOME}/bin/ical2org.pl";

my $base_dir = "$ENV{HOME}/.calendars"; my $org_dir = "$ENV{HOME}/org/calendars";

my %calendars = ( 'home' => 'http://www.google.com/calendar/ical/…', 'work' => 'https://www.google.com/calendar/ical/…', );

chdir $base_dir; # acad and ooo don't work my @cals = qw(home work);

foreach my $cal (@cals) { next unless $calendars{$cal};

<span style="color:#66d9ef">my</span> $cmd <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;$wget -q -O $cal.ics.new $calendars{$cal} &amp;&amp; mv $cal.ics.new $cal.ics&#34;</span>;
<span style="color:#66d9ef">print</span> STDERR <span style="color:#e6db74">&#34;$cmd\n&#34;</span> <span style="color:#66d9ef">if</span> $debug;
system $cmd;

<span style="color:#66d9ef">next</span> <span style="color:#66d9ef">unless</span> <span style="color:#f92672">-</span>r <span style="color:#e6db74">&#34;$cal.ics&#34;</span>;

$cmd <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;$ical2org -c $cal &lt; $base_dir/$cal.ics &gt; $org_dir/$cal.org.new&#34;</span>;
<span style="color:#66d9ef">print</span> STDERR <span style="color:#e6db74">&#34;$cmd\n&#34;</span> <span style="color:#66d9ef">if</span> $debug;
system $cmd;

<span style="color:#66d9ef">if</span> ( <span style="color:#f92672">-</span>s <span style="color:#e6db74">&#34;$org_dir/$cal.org.new&#34;</span> ) {
$cmd <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;cp $org_dir/$cal.org.new $org_dir/$cal.org&#34;</span>;
<span style="color:#66d9ef">print</span> STDERR <span style="color:#e6db74">&#34;$cmd\n&#34;</span> <span style="color:#66d9ef">if</span> $debug;
system $cmd;
}

}

The fun part is in ical2org.pl.

use Data::ICal; use Data::Dumper; use DateTime::Format::ICal;

use Getopt::Long; my $category = 'ical';

# Only sync events newer than this many weeks in the past. # Set to 0 to sync all past events. my $syncweeksback = 2;

my $debug = 0;

GetOptions( 'category|c=s' => </span>$category, 'debug|d!' => </span>$debug, );

#warn "Debug: $debug\n";

my $cal = Data::ICal->new(data => join '', <STDIN>);

#print Dumper $cal; my %gprops = %{ $cal->properties };

print "#+TITLE: ical entries\n"; print "#+AUTHOR: ".$gprops{'x-wr-calname'}[0]->decoded_value."\n"; print "#+EMAIL: \n"; print "#+DESCRIPTION: Converted using ical2org.pl\n"; print "#+CATEGORY: $category\n"; print "#+STARTUP: overview\n"; print "\n";

print "* COMMENT original iCal properties\n"; #print Dumper %gprops; print "Timezone: ", $gprops{'x-wr-timezone'}[0]->value, "\n";

foreach my $prop (values %gprops) { foreach my $p (@{ $prop }) { print $p->key, ':', $p->value, "\n"; } }

foreach my $entry (@{ $cal->entries }) { next if not $entry->isa('Data::ICal::Entry::Event'); #print 'Entry: ', Dumper $entry;

<span style="color:#66d9ef">my</span> %props <span style="color:#f92672">=</span> %{ $entry<span style="color:#f92672">-&gt;</span>properties };

<span style="color:#75715e"># skip entries with no start or end time</span>
<span style="color:#66d9ef">next</span> <span style="color:#66d9ef">if</span> (<span style="color:#f92672">not</span> $props{dtstart}[<span style="color:#ae81ff">0</span>] <span style="color:#f92672">or</span> <span style="color:#f92672">not</span> $props{dtend}[<span style="color:#ae81ff">0</span>]);

<span style="color:#66d9ef">my</span> $dtstart <span style="color:#f92672">=</span> DateTime::Format::ICal<span style="color:#f92672">-&gt;</span>parse_datetime($props{dtstart}[<span style="color:#ae81ff">0</span>]<span style="color:#f92672">-&gt;</span>value);
<span style="color:#66d9ef">my</span> $dtend   <span style="color:#f92672">=</span> DateTime::Format::ICal<span style="color:#f92672">-&gt;</span>parse_datetime($props{dtend}[<span style="color:#ae81ff">0</span>]<span style="color:#f92672">-&gt;</span>value);
<span style="color:#75715e"># Perhaps only sync some weeks back</span>
warn <span style="color:#e6db74">&#34;Old event: &#34;</span>, DateTime<span style="color:#f92672">-&gt;</span>compare($dtend, DateTime<span style="color:#f92672">-&gt;</span>now<span style="color:#f92672">-&gt;</span>subtract(weeks <span style="color:#f92672">=&gt;</span> $syncweeksback)), <span style="color:#e6db74">&#34; $dtend &#34;</span>, DateTime<span style="color:#f92672">-&gt;</span>now<span style="color:#f92672">-&gt;</span>subtract(weeks <span style="color:#f92672">=&gt;</span> $syncweeksback)<span style="color:#f92672">.</span> <span style="color:#e6db74">&#34;\n&#34;</span> <span style="color:#66d9ef">if</span> $debug;
<span style="color:#66d9ef">if</span> ($syncweeksback <span style="color:#f92672">!=</span> <span style="color:#ae81ff">0</span>
     <span style="color:#f92672">and</span> DateTime<span style="color:#f92672">-&gt;</span>compare($dtend, DateTime<span style="color:#f92672">-&gt;</span>now<span style="color:#f92672">-&gt;</span>subtract(weeks <span style="color:#f92672">=&gt;</span> $syncweeksback)) <span style="color:#f92672">==</span> <span style="color:#f92672">-</span><span style="color:#ae81ff">1</span>
     <span style="color:#f92672">and</span> <span style="color:#f92672">!</span>defined $props{rrule}
) {
warn <span style="color:#e6db74">&#34;... skipping\n&#34;</span> <span style="color:#66d9ef">if</span> $debug;
<span style="color:#66d9ef">next</span>;
}

<span style="color:#66d9ef">my</span> $duration <span style="color:#f92672">=</span> $dtend<span style="color:#f92672">-&gt;</span>subtract_datetime($dtstart);

<span style="color:#66d9ef">if</span> (defined $props{rrule}) {
<span style="color:#75715e">#print &#34;  REPEATABLE\n&#34;;</span>
<span style="color:#75715e"># Bad: There may be multiple rrules but I&#39;m ignoring them</span>
<span style="color:#66d9ef">my</span> $set <span style="color:#f92672">=</span> DateTime::Format::ICal<span style="color:#f92672">-&gt;</span>parse_recurrence(
    recurrence <span style="color:#f92672">=&gt;</span> $props{rrule}[<span style="color:#ae81ff">0</span>]<span style="color:#f92672">-&gt;</span>value,
    dtstart    <span style="color:#f92672">=&gt;</span> $dtstart,
    dtend      <span style="color:#f92672">=&gt;</span> DateTime<span style="color:#f92672">-&gt;</span>now<span style="color:#f92672">-&gt;</span>add(weeks <span style="color:#f92672">=&gt;</span> <span style="color:#ae81ff">1</span>),
);

<span style="color:#66d9ef">my</span> $itr <span style="color:#f92672">=</span> $set<span style="color:#f92672">-&gt;</span>iterator;
<span style="color:#66d9ef">while</span> (<span style="color:#66d9ef">my</span> $dt <span style="color:#f92672">=</span> $itr<span style="color:#f92672">-&gt;</span><span style="color:#66d9ef">next</span>) {
    $dt<span style="color:#f92672">-&gt;</span>set_time_zone(
	$props{dtstart}[<span style="color:#ae81ff">0</span>]<span style="color:#f92672">-&gt;</span>parameters<span style="color:#f92672">-&gt;</span>{<span style="color:#e6db74">&#39;TZID&#39;</span>} <span style="color:#f92672">||</span>
	$gprops{<span style="color:#e6db74">&#39;x-wr-timezone&#39;</span>}[<span style="color:#ae81ff">0</span>]<span style="color:#f92672">-&gt;</span>value
	);

    <span style="color:#66d9ef">my</span> $end <span style="color:#f92672">=</span> $dt<span style="color:#f92672">-&gt;</span>clone<span style="color:#f92672">-&gt;</span>add_duration($duration);
    <span style="color:#66d9ef">next</span> <span style="color:#66d9ef">if</span> ( $end <span style="color:#f92672">&lt;</span> DateTime<span style="color:#f92672">-&gt;</span>now<span style="color:#f92672">-&gt;</span>subtract(weeks <span style="color:#f92672">=&gt;</span> $syncweeksback) );
    
    <span style="color:#66d9ef">print</span> <span style="color:#e6db74">&#34;* &#34;</span><span style="color:#f92672">.</span>$props{summary}[<span style="color:#ae81ff">0</span>]<span style="color:#f92672">-&gt;</span>decoded_value<span style="color:#f92672">.</span><span style="color:#e6db74">&#34;\n&#34;</span>;
    <span style="color:#66d9ef">print</span> <span style="color:#e6db74">&#39;  &#39;</span>, org_date_range($dt, $end), <span style="color:#e6db74">&#34;\n&#34;</span>;
    <span style="color:#75715e">#print $dt, &#34;\n&#34;;</span>
    <span style="color:#66d9ef">print</span>  <span style="color:#e6db74">&#34;  :PROPERTIES:\n&#34;</span>;
    printf <span style="color:#e6db74">&#34;  :ID: %s\n&#34;</span>, $props{uid}[<span style="color:#ae81ff">0</span>]<span style="color:#f92672">-&gt;</span>value;

    <span style="color:#66d9ef">if</span> (defined $props{location}) {
	printf <span style="color:#e6db74">&#34;  :LOCATION: %s\n&#34;</span>, $props{location}[<span style="color:#ae81ff">0</span>]<span style="color:#f92672">-&gt;</span>value;
    }

    <span style="color:#66d9ef">if</span> (defined $props{status}) {
	printf <span style="color:#e6db74">&#34;  :STATUS: %s\n&#34;</span>, $props{status}[<span style="color:#ae81ff">0</span>]<span style="color:#f92672">-&gt;</span>value;
    }

    <span style="color:#66d9ef">print</span> <span style="color:#e6db74">&#34;  :END:\n&#34;</span>;

    <span style="color:#66d9ef">if</span> ($props{description}) {
	<span style="color:#66d9ef">print</span> <span style="color:#e6db74">&#34;\n&#34;</span>, $props{description}[<span style="color:#ae81ff">0</span>]<span style="color:#f92672">-&gt;</span>decoded_value, <span style="color:#e6db74">&#34;\n&#34;</span>;
    }
}
}
<span style="color:#66d9ef">else</span> {

<span style="color:#66d9ef">print</span> <span style="color:#e6db74">&#34;* &#34;</span><span style="color:#f92672">.</span>$props{summary}[<span style="color:#ae81ff">0</span>]<span style="color:#f92672">-&gt;</span>decoded_value<span style="color:#f92672">.</span><span style="color:#e6db74">&#34;\n&#34;</span>;

<span style="color:#66d9ef">my</span> $tz <span style="color:#f92672">=</span> $gprops{<span style="color:#e6db74">&#39;x-wr-timezone&#39;</span>}[<span style="color:#ae81ff">0</span>]<span style="color:#f92672">-&gt;</span>value;
$dtstart<span style="color:#f92672">-&gt;</span>set_time_zone($props{dtstart}[<span style="color:#ae81ff">0</span>]<span style="color:#f92672">-&gt;</span>parameters<span style="color:#f92672">-&gt;</span>{<span style="color:#e6db74">&#39;TZID&#39;</span>} <span style="color:#f92672">||</span> $tz);
$dtend<span style="color:#f92672">-&gt;</span>set_time_zone($props{dtend}[<span style="color:#ae81ff">0</span>]<span style="color:#f92672">-&gt;</span>parameters<span style="color:#f92672">-&gt;</span>{<span style="color:#e6db74">&#39;TZID&#39;</span>} <span style="color:#f92672">||</span> $tz);

<span style="color:#66d9ef">print</span> <span style="color:#e6db74">&#39;  &#39;</span>, org_date_range($dtstart, $dtend), <span style="color:#e6db74">&#34;\n&#34;</span>;

<span style="color:#66d9ef">print</span>  <span style="color:#e6db74">&#34;  :PROPERTIES:\n&#34;</span>;
printf <span style="color:#e6db74">&#34;  :ID: %s\n&#34;</span>, $props{uid}[<span style="color:#ae81ff">0</span>]<span style="color:#f92672">-&gt;</span>value;

<span style="color:#66d9ef">if</span> (defined $props{location}) {
    printf <span style="color:#e6db74">&#34;  :LOCATION: %s\n&#34;</span>, $props{location}[<span style="color:#ae81ff">0</span>]<span style="color:#f92672">-&gt;</span>value;
}

<span style="color:#66d9ef">if</span> (defined $props{status}) {
    printf <span style="color:#e6db74">&#34;  :STATUS: %s\n&#34;</span>, $props{status}[<span style="color:#ae81ff">0</span>]<span style="color:#f92672">-&gt;</span>value;
}

<span style="color:#66d9ef">print</span> <span style="color:#e6db74">&#34;  :END:\n&#34;</span>;

<span style="color:#66d9ef">if</span> ($props{description}) {
    <span style="color:#66d9ef">print</span> <span style="color:#e6db74">&#34;\n&#34;</span>, $props{description}[<span style="color:#ae81ff">0</span>]<span style="color:#f92672">-&gt;</span>decoded_value, <span style="color:#e6db74">&#34;\n&#34;</span>;
}

}

# print Dumper %props; }

sub org_date_range { my $start = shift; my $end = shift;

<span style="color:#66d9ef">my</span> $str <span style="color:#f92672">=</span> sprintf(<span style="color:#e6db74">&#39;&lt;%04d-%02d-%02d %s %02d:%02d&gt;&#39;</span>,
   $start<span style="color:#f92672">-&gt;</span>year,
   $start<span style="color:#f92672">-&gt;</span>month,
   $start<span style="color:#f92672">-&gt;</span>day,
   $start<span style="color:#f92672">-&gt;</span>day_abbr,
   $start<span style="color:#f92672">-&gt;</span>hour,
   $start<span style="color:#f92672">-&gt;</span>minute
   );
$str <span style="color:#f92672">.=</span> <span style="color:#e6db74">&#39;--&#39;</span>;
$str <span style="color:#f92672">.=</span> sprintf(<span style="color:#e6db74">&#39;&lt;%04d-%02d-%02d %s %02d:%02d&gt;&#39;</span>,
   $end<span style="color:#f92672">-&gt;</span>year,
   $end<span style="color:#f92672">-&gt;</span>month,
   $end<span style="color:#f92672">-&gt;</span>day,
   $end<span style="color:#f92672">-&gt;</span>day_abbr,
   $end<span style="color:#f92672">-&gt;</span>hour,
   $end<span style="color:#f92672">-&gt;</span>minute
   );

<span style="color:#66d9ef">return</span> $str;

}

I let Data::ICal parse the feed and DataTime::Format::ICal do the heavy lifting of parsing the date and time information from each entry. (Have I mentioned how cool the CPAN is?)

Most of the code is just reformating the iCal entry into org-mode syntax so that emacs can pull it into the agenda.

There’s one bit of magic I’m not showing here. In my .emacs config, I have this little gem.

(add-hook 'org-mode-hook 'auto-revert-mode)

That tells emacs to automatically revert (reload) any org-mode file that changes on disk while the buffer is open. Since I drop the converted files in a directory scanned by org-mode, emacs opens each converted calendar file in a buffer when the agenda view is first run. When the files are updated by the scripts above, emacs sees the changes and reverts the buffers. Anytime I regenerate the agenda view, emacs uses the updated buffers and the view is up-to-date.

Once again, this is a one way sync. I can’t edit the generated org-mode files and see the changes reflected in the Google calendars. If I want to make changes to my calendar, I have to do it through Google’s web interface. This actually works out for the best because Google provides all of the scheduling hooks to make sure others who I’ve invited to meetings can attend. I can’t get that, easily, in emacs.

So, there you go. A relatively pain free way to pull any iCal calendar into emacs.

Update 2016-01-21: I’ve incorporated a suggestion from Anders Johansson that prevents ical2org.pl from syncing old events. I set the default to two weeks in the past. To get the old behavior set $syncweeksback to 0.

Update 2016-02-03: Thanks to gr4nchio for a fix for recurring events.