Google Calendar Synchronization
Table of Contents
Overview
To get a real synchronization between org-mode and Google Calendar you need to sync two ways. We cover one way of handling the synchronization in the next two sections and also mention some other ways of synchronization at the end.
From Google Calendar into org using .ics files
Google Calendar offers access to each calendar via a hidden/secret url. That is, a url that only you know about and is very hard to guess for other people. You can use this to download your calendar in an iCalendar (.ics) format, which we then can rewrite into something usable for org-mode. For this conversion there luckily already exists a script written by Eric S. Fraga1. This is the latest version:
#!/usr/bin/awk -f # awk script for converting an iCal formatted file to a sequence of org-mode headings. # this may not work in general but seems to work for day and timed events from Google's # calendar, which is really all I need right now... # # usage: # awk -f THISFILE < icalinputfile.ics > orgmodeentries.org # # Note: change org meta information generated below for author and # email entries! # # Caveats: # # - date entries with no time specified are assumed to be local time zone; # same remark for date entries that do have a time but do not end with Z # e.g.: 20130101T123456 is local and will be kept as 2013-01-01 12:34 # where 20130223T123422Z is UTC and will be corrected appropriately # # - UTC times are changed into local times, using the time zone of the # computer that runs the script; it would be very hard in an awk script # to respect the time zone of a file belonging to another time zone: # the offsets will be different as well as the switchover time(s); # (consider a remote shell to a computer with the file's time zone) # # - the UTC conversion entirely relies on the built-in strftime method; # the author is not responsible for any erroneous conversions nor the # consequence of such conversions # # Eric S Fraga # 20100629 - initial version # 20100708 - added end times to timed events # - adjust times according to time zone information # - fixed incorrect transfer for entries with ":" embedded within the text # - added support for multi-line summary entries (which become headlines) # 20100709 - incorporated time zone identification # - fixed processing of continuation lines as Google seems to # have changed, in the last day, the number of spaces at # the start of the line for each continuation... # - remove backslashes used to protect commas in iCal text entries # no further revision log after this as the file was moved into a git # repository... # # Updated by: Guido Van Hoecke <guivhoATgmailDOTcom> # Last change: 2013.05.17 19:34:27 #---------------------------------------------------------------------------------- BEGIN { ### config section # maximum age in days for entries to be output: set this to -1 to # get all entries or to N>0 to only get enties that start or end # less than N days ago max_age = -1; max_age = 7; # set to 1 or 0 to yes or not output a header block with TITLE, # AUTHOR, EMAIL etc... header = 1; # set to 1 or 0 to yes or not output the original ical preamble as # comment preamble = 1; # set to 1 to output time and summary as one line starting with # the time (value 1) or to 0 to output the summary as first line # and the date and time info as a second line condense = 0; # set to 1 or 0 to yes or not output the original ical entry as a # comment (mostly useful for debugging purposes) original = 1; # google truncates long subjects with ... which is misleading in # an org file: it gives the unfortunate impression that an # expanded entry is still collapsed; value 1 will trim those # ... and value 0 doesn't touch them trimdots = 0; trimdots = 1; # change this to your name author = "Eric S Fraga" # and to your email address emailaddress = "e.fraga@ucl.ac.uk" ### end config section # use a colon to separate the type of data line from the actual contents FS = ":"; # we only need to preserve the original entry lines if either the # preamble or original options are true preserve = preamble || original date = ""; entry = "" first = 1; # true until an event has been found headline = "" icalentry = "" # the full entry for inspection id = "" indescription = 0; lasttimestamp = -1; if (header) { print "#+TITLE: Main Google calendar entries" print "#+AUTHOR: ", author print "#+EMAIL: ", emailaddress print "#+DESCRIPTION: converted using the ical2org awk script" print "#+CATEGORY: google" print "#+STARTUP: hidestars" print "#+STARTUP: overview" print "" } } # continuation lines (at least from Google) start with a space # if the continuation is after a description or a summary, append the entry # to the respective variable /^[ ]/ { if (indescription) { entry = entry gensub("\r", "", "g", gensub("^[ ]", "", "", $0)); } else if (insummary) { summary = summary gensub("\r", "", "g", gensub("^[ ]", "", "", $0)) } if (preserve) icalentry = icalentry "\n" $0 } /^BEGIN:VEVENT/ { # start of an event. if (first) { # if this is the first event, output the preamble from the iCal file if(preamble) { print "* COMMENT original iCal preamble" print gensub("\r", "", "g", icalentry) } if (preserve) icalentry = "" } first = false; } # any line that starts at the left with a non-space character is a new data field /^[A-Z]/ { # we ignore DTSTAMP lines as they change every time you download # the iCal format file which leads to a change in the converted # org file as I output the original input. This change, which is # really content free, makes a revision control system update the # repository and confuses. if (preserve) if (! index("DTSTAMP", $1)) icalentry = icalentry "\n" $0 # this line terminates the collection of description and summary entries indescription = 0; insummary = 0; } # this type of entry represents a day entry, not timed, with date stamp YYYYMMDD /^DTSTART;VALUE=DATE/ { date = datestring($2); } /^DTEND;VALUE=DATE/ { time2 = datestring($2, 1); if ( ! issameday ) # && substr(date,1,10) != substr(time2,1,10)) date = date ">--<" time2; } # this represents a timed entry with date and time stamp YYYYMMDDTHHMMSS # we ignore the seconds /^DTSTART[:;][^V]/ { date = datetimestring($2); # print date; } # and the same for the end date; /^DTEND[:;][^V]/ { time2 = datetimestring($2); if (substr(date,1,10) == substr(time2,1,10)) # timespan within same date, use one date with a time range date = date "-" substr(time2, length(time2)-4) else # timespan extends over at least two dates date = date ">--<" time2; } # The description will the contents of the entry in org-mode. # this line may be continued. /^DESCRIPTION/ { $1 = ""; entry = entry gensub("\r", "", "g", $0); indescription = 1; } # the summary will be the org heading /^SUMMARY/ { $1 = ""; summary = gensub("\r", "", "g", $0); # trim trailing dots if requested by config option if(trimdots && summary ~ /\.\.\.$/) sub(/\.\.\.$/, "", summary) insummary = 1; } # the unique ID will be stored as a property of the entry /^UID/ { id = gensub("\r", "", "g", $2); } /^LOCATION/ { location = gensub("\r", "", "g", $2); } /^STATUS/ { status = gensub("\r", "", "g", $2); } # when we reach the end of the event line, we output everything we # have collected so far, creating a top level org headline with the # date/time stamp, unique ID property and the contents, if any /^END:VEVENT/ { #output event if(max_age<0 || ( lasttimestamp>0 && systime()<lasttimestamp+max_age*24*60*60 ) ) { # translate \n sequences to actual newlines and unprotect commas (,) if (condense) print "* <" date "> " gensub("^[ ]+", "", "", gensub("\\\\,", ",", "g", gensub("\\\\n", " ", "g", summary))) else print "* " gensub("^[ ]+", "", "", gensub("\\\\,", ",", "g", gensub("\\\\n", " ", "g", summary))) "\n<" date ">" print ":PROPERTIES:" print ":ID: " id if(length(location)) print ":LOCATION: " location if(length(status)) print ":STATUS: " status print ":END:" print "" # translate \n sequences to actual newlines and unprotect commas (,) if(length(entry)>1) print gensub("^[ ]+", "", "", gensub("\\\\,", ",", "g", gensub("\\\\n", "\n", "g", entry))); # output original entry if requested by 'original' config option if (original) print "** COMMENT original iCal entry\n", gensub("\r", "", "g", icalentry) } summary = "" date = "" location = "" status = "" entry = "" icalentry = "" indescription = 0 insummary = 0 lasttimestamp = -1 } # funtion to convert an iCal time string 'yyyymmddThhmmss[Z]' into a # date time string as used by org, preferably including the short day # of week: 'yyyy-mm-dd day hh:mm' or 'yyyy-mm-dd hh:mm' if we cannot # define the day of the week function datetimestring(input) { # print "________" # print "input : " input # convert the iCal Date+Time entry to a format that mktime can understand spec = gensub("([0-9][0-9][0-9][0-9])([0-9][0-9])([0-9][0-9])T([0-9][0-9])([0-9][0-9])([0-9][0-9]).*[\r]*", "\\1 \\2 \\3 \\4 \\5 \\6", "g", input); # print "spec :" spec stamp = mktime(spec); lasttimestamp = stamp; if (stamp <= 0) { # this is a date before the start of the epoch, so we cannot # use strftime and will deliver a 'yyyy-mm-dd hh:mm' string # without day of week; this assumes local time, and does not # attempt UTC offset correction spec = gensub("([0-9][0-9][0-9][0-9])([0-9][0-9])([0-9][0-9])T([0-9][0-9])([0-9][0-9])([0-9][0-9]).*[\r]*", "\\1-\\2-\\3 \\4:\\5", "g", input); # print "==> spec:" spec; return spec; } if (input ~ /[0-9]{8}T[0-9]{6}Z/ ) { # this is an utc time; # we need to correct the timestamp by the utc offset for this time offset = strftime("%z", stamp) pm = substr(offset,1,1) 1 # define multiplier +1 or -1 hh = substr(offset,2,2) * 3600 * pm mm = substr(offset,4,2) * 60 * pm # adjust the timestamp stamp = stamp + hh + mm } return strftime("%Y-%m-%d %a %H:%M", stamp); } # function to convert an iCal date into an org date; # the optional parameter indicates whether this is an end date; # for single or multiple whole day events, the end date given by # iCal is the date of the first day after the event; # if the optional 'isenddate' parameter is non zero, this function # tries to reduce the given date by one day function datestring(input, isenddate) { #convert the iCal string to a an mktime input string spec = gensub("([0-9][0-9][0-9][0-9])([0-9][0-9])([0-9][0-9]).*[\r]*", "\\1 \\2 \\3 00 00 00", "g", input); # compute the nr of seconds after or before the epoch # dates before the epoch will have a negative timestamp # days after the epoch will have a positive timestamp stamp = mktime(spec); if (isenddate) { # subtract 1 day from the timestamp # note that this also works for dates before the epoch stamp = stamp - 86400; # register whether the end date is same as the start date issameday = lasttimestamp == stamp } else { # save timestamp of start date to allow for check of end date lasttimestamp = stamp } if (stamp < 0) { # this date is before the epoch; # the returned datestring will not have the short day of week string # as strftime does not handle negative times; # we have to construct the datestring directly from the input if (isenddate) { # we really should return the date before the input date, but strftime # does not work with negative timestamp values; so we can not use it # to obtain the string representation of the carrected timestamp; # we have to return the date specified in the iCal input and we # add time 00:00 to clarify this return spec = gensub("([0-9][0-9][0-9][0-9])([0-9][0-9])([0-9][0-9]).*[\r]*", "\\1-\\2-\\3 00:00", "g", input); } else { # just generate the desired representation of the input date, without time; return gensub("([0-9][0-9][0-9][0-9])([0-9][0-9])([0-9][0-9]).*[\r]*", "\\1-\\2-\\3", "g", input); } } # return the date and day of week return strftime("%Y-%m-%d %a", stamp); } # Local Variables: # time-stamp-line-limit: 1000 # time-stamp-format: "%04y.%02m.%02d %02H:%02M:%02S" # time-stamp-active: t # time-stamp-start: "Last change:[ \t]+" # time-stamp-end: "$" # End:
With this you can test your Google Calendar to org-mode synchronization by following these steps:
- Download the above script and save it as 'ical2org'. Make sure that the script is in your PATH and don't forget to set the executable flag (chmod u+x ical2org). You can also customize the script a bit by changing the variables in the config section of the script.
- Find your private URL for your calendar
- Log into Google Calendar
- Goto Settings
- Click on the calendar you want to export to org-mode
- At the bottom of the page find the 'private address' section and your ical link Use the 'reset private urls' if you need to, that is if you don't see a unique url.
- Download the url This can be done for example using 'wget <url>'
- Transform into org-file Use the downloaded script via 'ical2org < icsfile > orgfile'. Where icsfile is the path to the file you downloaded from Google and orgfile is the org-mode file you want to create.
- Add the orgfile to your agenda and test
If this all works you can automate the task via cron. Create a script such as the following that will automatically download and convert your calendar. Make sure that this file is only readable by you (chmod 700 <file>), since it will contain the url to your Google calendar.
#!/bin/bash # customize these WGET=<path to wget> ICS2ORG=<path to ical2org> ICSFILE=<path for icsfile> ORGFILE=<path to orgfile> URL=<url to your private Google calendar> # no customization needed below $WGET -O $ICSFILE $URL $ICS2ORG < $ICSFILE > $ORGFILE
automate this via cron by adding something like the following to your crontab:
5,20,35,50 * * * * <path to above script> &> /dev/null #sync my org files
This will sync every 15 minutes starting at 5 minutes past the hour.
From org to Google Calendar
There are at least two possible paths to get the information into Google:
- export from org mode to .ics; upload .ics to a public web server giving it a hidden/secret name; tell Google to import this .ics file
- use googlecl to import event when you create them into Google calendar (update entries won't be reflected in Google). See 2
The first one has the disadvantage that the item won't show up in your "main" calendar and therefore you can't easily share them with others. Nevertheless, this route is relatively easy and therefore we'll discuss it below. The second option (as described in the link) should work well, if you don't need to change things.
Also keep in mind that your mileage will vary, since everything described on this page works for some people, but perhaps not for you… if this is the case, feel free to ask on the org-email list and perhaps we can add missing features.
Back to the topic. To implement 1., we need org to export an .ics file, which can be achieved using the function: org-export-icalendar-combine-agenda-files. This will export all entries in you agenda. If you only want to export certain ones, you can set up a filter. For this we will define a filter function and then tell Emacs to use this filter. The filter shown here, will exclude items with a category "google" (for example from the ical2org script) and "private" and also only export entries that have a date and a time range set (that is, a start and a end time stamp). You can modify the function though to do anything you want!
;;; define categories that should be excluded (setq org-export-exclude-category (list "google" "private")) ;;; define filter. The filter is called on each entry in the agenda. ;;; It defines a regexp to search for two timestamps, gets the start ;;; and end point of the entry and does a regexp search. It also ;;; checks if the category of the entry is in an exclude list and ;;; returns either t or nil to skip or include the entry. (defun org-mycal-export-limit () "Limit the export to items that have a date, time and a range. Also exclude certain categories." (setq org-tst-regexp "<\\([0-9]\\{4\\}-[0-9]\\{2\\}-[0-9]\\{2\\} ... [0-9]\\{2\\}:[0-9]\\{2\\}[^\r\n>]*?\ \)>") (setq org-tstr-regexp (concat org-tst-regexp "--?-?" org-tst-regexp)) (save-excursion ; get categories (setq mycategory (org-get-category)) ; get start and end of tree (org-back-to-heading t) (setq mystart (point)) (org-end-of-subtree) (setq myend (point)) (goto-char mystart) ; search for timerange (setq myresult (re-search-forward org-tstr-regexp myend t)) ; search for categories to exclude (setq mycatp (member mycategory org-export-exclude-category)) ; return t if ok, nil when not ok (if (and myresult (not mycatp)) t nil))) ;;; activate filter and call export function (defun org-mycal-export () (let ((org-icalendar-verify-function 'org-mycal-export-limit)) (org-export-icalendar-combine-agenda-files)))
To use these function you can include the above code in your .emacs file and then in case you run Emacs server call
emacsclient -e "(save-excursion (org-mycal-export))"
in your shell to generate the .ics file.
If you want to export also TODO items that have a SCHEDULED timestamp, you can set
(setq org-icalendar-use-scheduled '(todo-start event-if-todo))
in your .emacs.
Another Emacs variable that you might want to look into is: org-icalendar-honor-noexport-tag.
You can now automate this via a cron job to generate updated .ics files.
The next step is to give the file a cryptic name (so that other people have a hard time accessing your file, also make sure that your web server doesn't show an index for your directory, etc.) and copy it to a public accessible web server. Then log into your Google calendar and in the left column under "Other calendars" use "Add"->"Add by url" to point Google at your freshly generated .ics file and you should be all set up. Once you done this Google will every now and then (every few hours?) look for a newer version of your .ics file and include this in your calendar.
Other methods of syncing
org-caldav
David Engster writes:
I have written a package 'org-caldav' which can sync items to a remote calendar server using the CalDAV protocol. The main purpose of this package is to make better use of Org in combination with Android-based mobile devices (yes, there is mobileOrg, but I have several problems with it; that's a topic for another day, though).
I think org-caldav is now "good enough" to allow some testing by adventurous people. I put the code up on github here