Org sync with Google Calendar. (active maintained project as of 2019-11-06)
Emacs Lisp
494
414 commits
updated Sep 16, 2026
* Org-Gcal [[https://melpa.org/#/org-gcal][file:https://melpa.org/packages/org-gcal-badge.svg]] [[https://github.com/kidd/org-gcal.el/actions/workflows/main.yml][file:https://github.com/kidd/org-gcal.el/actions/workflows/main.yml/badge.svg]]
=org-gcal= offers
- Fetch google calendar event
- Post/edit org element
- Sync between Org and Gcal
Data will be UTF-8 encoded for sync.
* Getting Started
Follow the instructions at [[#Installation][Installation]], and then run ~org-gcal-fetch~ to
download Google Calendar events into your Org-mode files using the Google
Calendar API. Run ~org-gcal-fetch~ to retrieve new events and update events
already retrieved, and run ~org-gcal-post-at-point~ on an Org-mode headline
corresponding to a downloaded event to push updates to the events on the Google
Calendar API server.
*First time setup:* on the initial run, in order to obtain the initial OAuth
authorization, ~org-gcal~ will open a link in the browser to obtain authorization
for the OAuth credentials generated in [[#Installation][Installation]]. You will probably see a
screen that says "This app isn't verified". You will need to click on the
"Advanced" link to authorize the application:
[[file:https://user-images.githubusercontent.com/44981227/71685532-d892ce00-2d98-11ea-8981-1adce23e8678.png]]
* Requirements
- Emacs 26+
- [[https://orgmode.org/][Org mode]] 9.3+
- [[https://github.com/tkf/emacs-request][tkf/emacs-request]]
- [[https://github.com/jwiegley/alert][jwiegley/alert]]
- [[https://elpa.gnu.org/packages/persist.html][~persist~]]
- [[https://github.com/skeeto/emacs-aio][skeeto/emacs-aio]]
- [[https://github.com/rhaps0dy/emacs-oauth2-auto][rhaps0dy/emacs-oauth2-auto]] (actually using vendored fork
[[https://github.com/telotortium/emacs-oauth2-auto/tree/main][telotortium/emacs-oauth2-auto]])
=org-gcal= is now available in the famous emacs package repo [[http://melpa.milkbox.net/][MELPA]], so the
recommended way is to install it through Emacs package management system.
NOTE: ~persist~ lives in GNU ELPA. If you're running Emacs 26.x with x < 3,
you may need to add ~(setq gnutls-algorithm-priority "NORMAL:-VERS-TLS1.3")~
to your ~init.el~ to work around Emacs bug https://debbugs.gnu.org/34341
([[https://www.reddit.com/r/emacs/comments/cdei4p/failed_to_download_gnu_archive_bad_request/][more info]]).
* Installation
:PROPERTIES:
:ID: f5de2e1e-80a1-4ee3-8eeb-fd4db0794448
:CUSTOM_ID: Installation
:END:
The method below will allow you to use oauth2-auto to perform authentication
of the Google Calendar API. This new method is required due to a February 2022
change on Google's end that removed Out-of-Band redirect urls.
Please note that users still the old OOB method of authentication will be required to
use the below method before Feburary 2023, [[https://developers.google.com/identity/protocols/oauth2/resources/oob-migration][as Google will be blocking all existing
OOB clients on January 31, 2023.]]
1. Go to [[https://console.developers.google.com/project][Google Developers Console]]
2. Create a project (with any name)
3. Click on the project
4. Click on the hamburger menu at the upper-left, then *APIs & Services*, then
*Credentials*.
5. Set up a consent screen. Give the application a name and set it up as type
"external application", and accept the defaults for everything else. You
should not need to verify the application, because only your user will be
using it.
6. Once you've set up a consent screen (this is required), click on *Create
Credentials* and *Oauth client ID* with Application type /Desktop/ (or /Other/, if /Desktop/ is not present).
7. Click on *Create Client ID*
8. Record the Client ID and Client secret for setup.
9. Under the same *APIs & Services* menu section, select *Library*
10. Scroll down to *Calendar API*. Click the *Enable* button to enable calendar
API access to the app you created in steps 5 & 6.
Go to [[https://www.google.com/calendar/render][Google setting page]] to
check the calendar ID.
11. Go to [[https://www.google.com/calendar/render][Google setting page]] and
click the gear-shaped settings icon in the upper right, then select
"Settings" from the drop down list.
12. Select the "Setting for my Calendars" tab on the left, which will
display a list of your calendars.
13. Select the calendar you would like to synchronize with. This will
take you to the "Calendar Settings" page for that calendar. Near
the end is a section titled "Integrate Calendar". Following the XML,
ICAL, and HTML tags, you will see your Calendar ID.
14. Copy the Calendar ID for use in the settings below, where you will
use it as the first element in the org-gcal-fetch-file-alist for
associating calendars with specific org files. You can associate
different calendars with different org files, so repeat this for
each calendar you want to use.
** Setting example
#+begin_src elisp
(setq org-gcal-client-id "your-id-foo.apps.googleusercontent.com"
org-gcal-client-secret "your-secret"
org-gcal-fetch-file-alist '(("your-mail@gmail.com" . "~/schedule.org")
("another-mail@gmail.com" . "~/task.org")))
(require 'org-gcal)
#+end_src
*** Note
This package uses ~plstore~ as a dependency for storing OAuth tokens. In order
to avoid getting prompted all the time for the password to your plstore, it is
recommended that you put the following in your init.el:
#+begin_src elisp
(setq plstore-cache-passphrase-for-symmetric-encryption t)
#+end_src
You may run into an issue where emacs asks for your PLSTORE password, then asks
whether you want to kill a buffer for `oauth2-auto.plist`without actually creating
such a file or before returning dispatching `(epg-error "Encrypt failed" exit)`.
If this occurs, try the following:
1. If a `oauth2-auto.plist` file exists already in your `USER-EMACS-DIRECTORY`,
make sure you use the correct password.
2. If this file does not exist already, create an empty file titled
`oauth2-auto.plist` inside your `USER-EMACS-DIRECTORY` and run `org-gcal-fetch`.
Alternatively, you may want to use an *asymmetric* GPG key instead. The main
advantage of this is that the key can be retrieved from ~gpg-agent~ instead.
In particular, on many systems you can configure ~gpg-agent~ to read the key
from the system keychain, which means that you should only need to enter the
keychain password, instead of having to memorize and enter a separate
password for ~plstore~. As a bonus, you'll be prompted fewer times to enter
a password, and it's easier to automatically run ~org-gcal~ unattended.
To do this, follow these steps:
1. Generate a new GPG key following [these
instructions](https://docs.github.com/en/authentication/managing-commit-signature-verification/generating-a-new-gpg-key).
You could reuse an existing one also, but it's probably best to separate
it from any other keys whose public keys you publish (Git signing,
GPG-encrypted email, etc.).
2. Once you have a key, copy the long form of the GPG key ID you'd like to
use (see the Github instructions above, or run
~gpg --list-secret-keys --keyid-format=long~).
3. Add the key ID to the Emacs variable ~plstore-encrypt-to~:
#+begin_src elisp
(require 'plstore)
(add-to-list 'plstore-encrypt-to '("GPG-key-id"))
#+end_src
4. Set up ~pinentry~ for ~gpg-agent~, so that the password to decrypt the GPG
key is stored in the system keychain. For example, on macOS you can follow
[these
instructions](https://gist.github.com/koshatul/2427643668d4e89c0086f297f9ed2130).
Set `epg-pinentry-mode` to `'loopback` to avoid hanging errors during sync (#267).
#+begin_src elisp
(setq epg-pinentry-mode 'loopback)
#+end_src
** Multiple accounts
There's no support for multiple accounts. If you want to use
org-gcal with calendars from different accounts, you can give
permissions to the account you configured via the calendar's
settings interface.
To get more detailed information you can [[https://digibites.zendesk.com/hc/en-us/articles/200299863-How-do-I-share-my-calendar-with-someone-else-Google-Calendar-or-Outlook-com-][check this link]].
** Different timezone
If you local timezone is different from the Calendar. You can use
your local timezone to fetch events. Event will be fetched using
timezone defined in =org-gcal-local-timezone=. Timezone string can
be found from:
https://en.wikipedia.org/wiki/List_of_tz_database_time_zones.
* Testing
To execute compile and regression tests, run ~make~.
This will use your existing Emacs installation to generate a value of
~load-path~ that allows ~org-gcal~ to find all its dependencies, and save it to
~.load-path.el~ in this directory. To delete this and other temporary files,
run ~make clean~.
* Usage
** Getting started
Once you've set up the basic settings (see [[Setting example]]), you can run
=org-gcal-fetch= to fetch events into the files configured in
=org-gcal-fetch-file-alist=. After the initial fetch, running =org-gcal-fetch=
will retrieve newly-created events and update already-fetched events.
To create a Google Calendar event from an Org-mode event, it's enough to run the
=org-gcal-post-at-point= command on a simple headline:
#+BEGIN_SRC org
,* Event title
#+END_SRC
This will prompt you for the calendar ID, start time, and end time of the event.
Therefore, if you'd like to create an event without user interaction (from a
template, for example), you should set these fields before running
=org-gcal-post-at-point=:
#+BEGIN_SRC org
,* Event title
:PROPERTIES:
:calendar-id: jjjjjjjjjjjfuuuk842fdok134@group.calendar.google.com
:END:
:org-gcal:
<2020-07-15 wed 09:15-09:30>
Event details
(can be multiple paragraphs).
:END:
#+END_SRC
After the event has been created, some Google Calendar API-specific fields will
be set for future updates to the event:
#+BEGIN_SRC org
,* Event title
:PROPERTIES:
:calendar-id: jjjjjjjjjjjfuuuk842fdok134@group.calendar.google.com
:ETag: "7777777777980000"
:ID: xxxxxxxxxxa4jlcj0v998f4u18/jjjjjjjjjjjfuuuk842fdok134@group.calendar.google.com
:END:
:org-gcal:
<2020-07-15 wed 09:15-09:30>
Event details
(can be multiple paragraphs).
:END:
#+END_SRC
If you want to schedule the event in your Org Agenda, you can use the SCHEDULED
property (set by =org-schedule=) instead of storing the date in the =:org-gcal:=
drawer. The drawer will still be present to contain event details:
#+BEGIN_SRC org
,* Event title
SCHEDULED: <2020-07-15 wed 09:15-09:30>
:PROPERTIES:
:calendar-id: jjjjjjjjjjjfuuuk842fdok134@group.calendar.google.com
:ETag: "7777777777980000"
:ID: xxxxxxxxxxa4jlcj0v998f4u18/jjjjjjjjjjjfuuuk842fdok134@group.calendar.google.com
:END:
:org-gcal:
Event details
(can be multiple paragraphs).
:END:
#+END_SRC
After editing an event in Org mode, you can also run =org-gcal-post-at-point= to
update the event on Google Calendar. The command =org-gcal-sync= does what
=org-gcal-fetch= does, but also runs =org-gcal-post-at-point= on all events that
you've edited in Org mode to update the corresponding events in Google Calendar.
** Event structure
=org-gcal= modifies the following Org-mode properties and drawers when updating
an event from the Google Calendar API:
- Title :: contains the event summary (minus any TODO keywords or tags).
- Timestamps:
- =SCHEDULED= :: if the =SCHEDULED= attribute of a headline is present,
=org-gcal= will maintain the start and end times of an event there rather
than in a timestamp in the =org-gcal= drawer (see below).
- Properties:
- =calendar-id= (can be modified using the [[help:org-gcal-calendar-id-property][=org-gcal-calendar-id-property=]]
variable) :: contains the calendar ID of the calendar on which the event is
maintained.
- =ETag= (can be modified using the [[help:org-gcal-etag-property][=org-gcal-etag-property=]]
variable) :: contains the most recent ETag retrieved from the Google Calendar
API for the event (see [[https://developers.google.com/calendar/v3/version-resources#conditional_modification][the Google Calendar API documentation]]). Used to
support automatically updating the headline using the most recent event data
from the API if it has changed on the server since it was last retrieved.
**NOTE**: If the =ETag= property is present on a headline (even if empty (=""=)) and not equal to the value
stored by the Calendar API, your event will be **overwritten** with the data from the server. In this case,
you'll see a notification for a HTTP 512. This is intended behavior. If your event is being overwritten when
you don't want it, remove the =ETag= property from your headline.
- =ID= :: contains =<event_id>/<calendar_id>= of the event, as provided by the
Google Calendar API. Don't change the ID manually, or else the event won't be
able to retrieved or updated from the headline.
- Drawers:
- =org-gcal= (can be modified using the [[help:org-gcal-drawer-name][=org-gcal-drawer-name=]] variable) ::
contains the event description. Unless the timestamp is maintained using
=SCHEDULED=, the initial line of this drawer contains the event start and
end time, with the event description starting in the next paragraph.
Apart from these, all other attributes are preserved when an event is updated
in any way.
** Commands
*** =org-gcal-fetch=
Fetch Google calendar events for all calendar IDs in =org-gcal-fetch-file-alist=
occurring between =org-gcal-up-days= before today and =org-gcal-down-days=
after today. If the events have already been retrieved and can be located
using their Org-mode headline IDs, update the event in place. Otherwise,
insert it at the end of the file corresponding to the event's calendar ID in
=org-gcal-fetch-file-alist=. Does not update events on the server.
*** =org-gcal-sync=
Like =org-gcal-fetch=, but also update events on the server if they have
changed locally.
Note: This command does not post /newly created events/ onto the server, which
is done with =org-gcal-post-at-point=. =org-gcal-sync= updates events /after/
they are posted to the server.
*** =org-gcal-fetch-buffer=
Fetch changes to Google calendar events to update entries in the current
buffer, but don't update events on server.
*** =org-gcal-sync-buffer=
Sync entries in the current buffer with Google Calendar.
*** =org-gcal-post-at-point=
Update the event represented by the Org-mode headline at POINT on the server
using the Google Calendar API.
If the event has changed on the server since it was last retrieved (detected
using the =ETag= property), automatically update the headline using the
event data from the server instead of updating the event on the server.
*** =org-gcal-delete-at-point=
Delete the event represented by the Org-mode headline at POINT on the server
using the Google Calendar API. This will not delete the Org-mode headline.
If the event has changed on the server since it was last retrieved (detected
using the =ETag= property), automatically update the headline using the
event data from the server instead of updating the event on the server.
** Deleting events
If an event is deleted on the server, then updating an event (via
=org-gcal-post-at-point=, =org-gcal-sync=, etc.) will optionally cancel and
delete the corresponding Org mode headlines:
- =org-gcal-update-cancelled-events-with-todo=: if set (the default), mark all
cancelled events with the =org-gcal-cancelled-todo-keyword= (default:
=CANCELLED=). This keyword must be one defined in =org-todo-keywords= or it
will be ignored.
- =org-gcal-remove-api-cancelled-events=: if set, delete Org headlines of
cancelled events. The default value is ='ask=, which means to prompt for
deletion.
- =org-gcal-remove-events-with-cancelled-todo=: by default, avoid deleting Org
headlines of events that are marked with =org-gcal-cancelled-todo-keyword=
beforehand, to allow preserving a headline for a cancelled event from being
deleted. Setting this to =t= always deletes headlines, even already cancelled
ones.
* Other features
** Minimize alerts
Modify =org-gcal-notify-p= from =t= to =nil=
** Strip HTML from event descriptions
Google Calendar returns event descriptions as HTML. By default, org-gcal
preserves that HTML so event descriptions can be edited locally without losing
formatting.
If you prefer plain text descriptions in Org, set
=org-gcal-strip-html-descriptions= to =t=:
#+BEGIN_SRC emacs-lisp
(setq org-gcal-strip-html-descriptions t)
#+END_SRC
You can override this global default for individual calendars with
=org-gcal-strip-html-descriptions-overrides=. This is useful when you want to
strip HTML for most calendars but preserve it for shared calendars:
#+BEGIN_SRC emacs-lisp
(setq org-gcal-strip-html-descriptions t
org-gcal-strip-html-descriptions-overrides
'(("shared-calendar@group.calendar.google.com" . nil)))
#+END_SRC
** Collect instances of recurring events under parent event
By default, =org-gcal-recurring-events-mode= is set to ='top-level=, which means
that new fetched events that are instances of recurring events will be inserted
at the top level of the file for the calendar ID configured in
=org-gcal-fetch-file-alist=:
#+BEGIN_SRC org
,* Meeting
SCHEDULED: <2020-08-07 Fri 11:00>
,* Meeting
SCHEDULED: <2020-08-14 Fri 11:00>
,* Meeting
SCHEDULED: <2020-08-21 Fri 11:00>
,* Meeting
SCHEDULED: <2020-08-28 Fri 11:00>
#+END_SRC
If =org-gcal-recurring-events-mode= is instead set to ='nested=, such events
will be inserted as Org-mode child headlines under the headline for the parent
event:
#+BEGIN_SRC org
,* Meeting
SCHEDULED: <2017-02-17 Fri 11:00>
,** Meeting
SCHEDULED: <2020-08-07 Fri 11:00>
,** Meeting
SCHEDULED: <2020-08-14 Fri 11:00>
,** Meeting
SCHEDULED: <2020-08-21 Fri 11:00>
,** Meeting
SCHEDULED: <2020-08-28 Fri 11:00>
#+END_SRC
Here the parent meeting has been running for several years, but only the
instances of the meeting in the range given by =org-gcal-down-days= and
=org-gcal-up-days= are fetched.
** Customizing the contents of event entries
If you would like to customize the contents of event entries (for example, to add a property from the Google Calendar API that's not automatically written to the Org-mode entry), you can add a function to the list =org-gcal-after-update-entry-functions=. For example, here is some code to add the =Effort= property to an entry based on the duration of the event (note that the current point is placed at the beginning of the entry when the function is called):
#+BEGIN_SRC emacs-lisp
(defun my-org-gcal-set-effort (_calendar-id event _update-mode)
"Set Effort property based on EVENT if not already set."
(when-let* ((stime (plist-get (plist-get event :start)
:dateTime))
(etime (plist-get (plist-get event :end)
:dateTime))
(diff (float-time
(time-subtract (org-gcal--parse-calendar-time-string etime)
(org-gcal--parse-calendar-time-string stime))))
(minutes (floor (/ diff 60))))
(let ((effort (org-entry-get (point) org-effort-property)))
(unless effort
(message "need to set effort - minutes %S" minutes)
(org-entry-put (point)
org-effort-property
(apply #'format "%d:%02d" (cl-floor minutes 60)))))))
(add-hook 'org-gcal-after-update-entry-functions #'my-org-gcal-set-effort)
#+END_SRC
** org-capture template
Here is a recommended snippet to use as a =org-capture-template= for making Google Calendar appointments:
#+begin_src elisp
(setq org-capture-templates
`(("a" "Appointment" entry (file ,(concat org-directory "/gcal.org"))
"* %?\n:PROPERTIES:\n:calendar-id:\tFirst.Last@gmail.com\n:END:\n:org-gcal:\n%^T--%^T\n:END:\n\n" :jump-to-captured t)))
#+end_src
Change the following:
- `/gcal.org` should be whichever org file you plan to capture appointments to. If this file is not in your generic =org-directory=, then adjust the `(concat org-directory "/gcal.org")` as needed.
- `First.Last@gmail.com` should be the calendar id you want to capture to. If this is your Gmail account's calendar, then just change `First.Last` to be your email handle.
** Sync automatically at regular times
This snippet uses `run-at-time` to perform a `org-gcal-sync` at specific times.
#+begin_src elisp
;; Run ‘org-gcal-sync’ regularly not at startup, but at 8 AM every day,
;; starting the next time 8 AM arrives.
(run-at-time
(let* ((now-decoded (decode-time))
(today-8am-decoded
(append '(0 0 8) (nthcdr 3 now-decoded)))
(now (encode-time now-decoded))
(today-8am (encode-time today-8am-decoded)))
(if (time-less-p now today-8am)
today-8am
(time-add today-8am (* 24 60 60))))
(* 24 60 60)
(defun my-org-gcal-sync-clear-token ()
"Sync calendar, clearing tokens first."
(interactive)
(require 'org-gcal)
(when org-gcal--sync-lock
(warn "%s" "‘my-org-gcal-sync-clear-token’: ‘org-gcal--sync-lock’ not nil - calling ‘org-gcal--sync-unlock’.")
(org-gcal--sync-unlock))
(org-gcal-sync-tokens-clear)
(org-gcal-sync)
nil))
#+end_src
* Troubleshooting
** Debugging
Because we used the [[https://github.com/kiwanami/emacs-deferred][deferred.el]] to perform asynchronous operations like calling
~request~ (via [[https://github.com/tkf/emacs-request/blob/master/request-deferred.el][~request-deferred~]]), normal Emacs debugging and stack traces tend
not to be as useful as usual. The best way to debug is to run ~M-x
org-gcal-toggle-debug~, which sets a variety of debugging variables to ease
debugging. The old values of the variables are saved so they can be restored by
another call to ~M-x org-gcal-toggle-debug~.
One of the most useful things this enables is logging of HTTP requests. Open the
~*request-log*~ buffer to see all requests issued by this library.
** Errors
*** Duplicate ID
You get an error like this:
#+BEGIN_EXAMPLE
Duplicate ID "FOO", also in file BAR
#+END_EXAMPLE
Most likely, this means some calendar events were mistakenly retrieved twice
(for example, if you ran =org-gcal-fetch= on different computers). Search your
Org-mode files for the duplicate ID "FOO" and delete one of the headlines with
duplicate IDs (or just change the =ID= property on one of the events to
something else).
* Similar applications
[[https://github.com/dengste/org-caldav][dengste/org-caldav]]
Not written in Markdown, so it's shown here as plain text — view it formatted on GitHub.
(top 30 of 33)
Emacs Lisp
99.5%
Org sync with Google Calendar. (active maintained project as of 2019-11-06)
Emacs Lisp
494
414 commits
updated Sep 16, 2026
* Org-Gcal [[https://melpa.org/#/org-gcal][file:https://melpa.org/packages/org-gcal-badge.svg]] [[https://github.com/kidd/org-gcal.el/actions/workflows/main.yml][file:https://github.com/kidd/org-gcal.el/actions/workflows/main.yml/badge.svg]]
=org-gcal= offers
- Fetch google calendar event
- Post/edit org element
- Sync between Org and Gcal
Data will be UTF-8 encoded for sync.
* Getting Started
Follow the instructions at [[#Installation][Installation]], and then run ~org-gcal-fetch~ to
download Google Calendar events into your Org-mode files using the Google
Calendar API. Run ~org-gcal-fetch~ to retrieve new events and update events
already retrieved, and run ~org-gcal-post-at-point~ on an Org-mode headline
corresponding to a downloaded event to push updates to the events on the Google
Calendar API server.
*First time setup:* on the initial run, in order to obtain the initial OAuth
authorization, ~org-gcal~ will open a link in the browser to obtain authorization
for the OAuth credentials generated in [[#Installation][Installation]]. You will probably see a
screen that says "This app isn't verified". You will need to click on the
"Advanced" link to authorize the application:
[[file:https://user-images.githubusercontent.com/44981227/71685532-d892ce00-2d98-11ea-8981-1adce23e8678.png]]
* Requirements
- Emacs 26+
- [[https://orgmode.org/][Org mode]] 9.3+
- [[https://github.com/tkf/emacs-request][tkf/emacs-request]]
- [[https://github.com/jwiegley/alert][jwiegley/alert]]
- [[https://elpa.gnu.org/packages/persist.html][~persist~]]
- [[https://github.com/skeeto/emacs-aio][skeeto/emacs-aio]]
- [[https://github.com/rhaps0dy/emacs-oauth2-auto][rhaps0dy/emacs-oauth2-auto]] (actually using vendored fork
[[https://github.com/telotortium/emacs-oauth2-auto/tree/main][telotortium/emacs-oauth2-auto]])
=org-gcal= is now available in the famous emacs package repo [[http://melpa.milkbox.net/][MELPA]], so the
recommended way is to install it through Emacs package management system.
NOTE: ~persist~ lives in GNU ELPA. If you're running Emacs 26.x with x < 3,
you may need to add ~(setq gnutls-algorithm-priority "NORMAL:-VERS-TLS1.3")~
to your ~init.el~ to work around Emacs bug https://debbugs.gnu.org/34341
([[https://www.reddit.com/r/emacs/comments/cdei4p/failed_to_download_gnu_archive_bad_request/][more info]]).
* Installation
:PROPERTIES:
:ID: f5de2e1e-80a1-4ee3-8eeb-fd4db0794448
:CUSTOM_ID: Installation
:END:
The method below will allow you to use oauth2-auto to perform authentication
of the Google Calendar API. This new method is required due to a February 2022
change on Google's end that removed Out-of-Band redirect urls.
Please note that users still the old OOB method of authentication will be required to
use the below method before Feburary 2023, [[https://developers.google.com/identity/protocols/oauth2/resources/oob-migration][as Google will be blocking all existing
OOB clients on January 31, 2023.]]
1. Go to [[https://console.developers.google.com/project][Google Developers Console]]
2. Create a project (with any name)
3. Click on the project
4. Click on the hamburger menu at the upper-left, then *APIs & Services*, then
*Credentials*.
5. Set up a consent screen. Give the application a name and set it up as type
"external application", and accept the defaults for everything else. You
should not need to verify the application, because only your user will be
using it.
6. Once you've set up a consent screen (this is required), click on *Create
Credentials* and *Oauth client ID* with Application type /Desktop/ (or /Other/, if /Desktop/ is not present).
7. Click on *Create Client ID*
8. Record the Client ID and Client secret for setup.
9. Under the same *APIs & Services* menu section, select *Library*
10. Scroll down to *Calendar API*. Click the *Enable* button to enable calendar
API access to the app you created in steps 5 & 6.
Go to [[https://www.google.com/calendar/render][Google setting page]] to
check the calendar ID.
11. Go to [[https://www.google.com/calendar/render][Google setting page]] and
click the gear-shaped settings icon in the upper right, then select
"Settings" from the drop down list.
12. Select the "Setting for my Calendars" tab on the left, which will
display a list of your calendars.
13. Select the calendar you would like to synchronize with. This will
take you to the "Calendar Settings" page for that calendar. Near
the end is a section titled "Integrate Calendar". Following the XML,
ICAL, and HTML tags, you will see your Calendar ID.
14. Copy the Calendar ID for use in the settings below, where you will
use it as the first element in the org-gcal-fetch-file-alist for
associating calendars with specific org files. You can associate
different calendars with different org files, so repeat this for
each calendar you want to use.
** Setting example
#+begin_src elisp
(setq org-gcal-client-id "your-id-foo.apps.googleusercontent.com"
org-gcal-client-secret "your-secret"
org-gcal-fetch-file-alist '(("your-mail@gmail.com" . "~/schedule.org")
("another-mail@gmail.com" . "~/task.org")))
(require 'org-gcal)
#+end_src
*** Note
This package uses ~plstore~ as a dependency for storing OAuth tokens. In order
to avoid getting prompted all the time for the password to your plstore, it is
recommended that you put the following in your init.el:
#+begin_src elisp
(setq plstore-cache-passphrase-for-symmetric-encryption t)
#+end_src
You may run into an issue where emacs asks for your PLSTORE password, then asks
whether you want to kill a buffer for `oauth2-auto.plist`without actually creating
such a file or before returning dispatching `(epg-error "Encrypt failed" exit)`.
If this occurs, try the following:
1. If a `oauth2-auto.plist` file exists already in your `USER-EMACS-DIRECTORY`,
make sure you use the correct password.
2. If this file does not exist already, create an empty file titled
`oauth2-auto.plist` inside your `USER-EMACS-DIRECTORY` and run `org-gcal-fetch`.
Alternatively, you may want to use an *asymmetric* GPG key instead. The main
advantage of this is that the key can be retrieved from ~gpg-agent~ instead.
In particular, on many systems you can configure ~gpg-agent~ to read the key
from the system keychain, which means that you should only need to enter the
keychain password, instead of having to memorize and enter a separate
password for ~plstore~. As a bonus, you'll be prompted fewer times to enter
a password, and it's easier to automatically run ~org-gcal~ unattended.
To do this, follow these steps:
1. Generate a new GPG key following [these
instructions](https://docs.github.com/en/authentication/managing-commit-signature-verification/generating-a-new-gpg-key).
You could reuse an existing one also, but it's probably best to separate
it from any other keys whose public keys you publish (Git signing,
GPG-encrypted email, etc.).
2. Once you have a key, copy the long form of the GPG key ID you'd like to
use (see the Github instructions above, or run
~gpg --list-secret-keys --keyid-format=long~).
3. Add the key ID to the Emacs variable ~plstore-encrypt-to~:
#+begin_src elisp
(require 'plstore)
(add-to-list 'plstore-encrypt-to '("GPG-key-id"))
#+end_src
4. Set up ~pinentry~ for ~gpg-agent~, so that the password to decrypt the GPG
key is stored in the system keychain. For example, on macOS you can follow
[these
instructions](https://gist.github.com/koshatul/2427643668d4e89c0086f297f9ed2130).
Set `epg-pinentry-mode` to `'loopback` to avoid hanging errors during sync (#267).
#+begin_src elisp
(setq epg-pinentry-mode 'loopback)
#+end_src
** Multiple accounts
There's no support for multiple accounts. If you want to use
org-gcal with calendars from different accounts, you can give
permissions to the account you configured via the calendar's
settings interface.
To get more detailed information you can [[https://digibites.zendesk.com/hc/en-us/articles/200299863-How-do-I-share-my-calendar-with-someone-else-Google-Calendar-or-Outlook-com-][check this link]].
** Different timezone
If you local timezone is different from the Calendar. You can use
your local timezone to fetch events. Event will be fetched using
timezone defined in =org-gcal-local-timezone=. Timezone string can
be found from:
https://en.wikipedia.org/wiki/List_of_tz_database_time_zones.
* Testing
To execute compile and regression tests, run ~make~.
This will use your existing Emacs installation to generate a value of
~load-path~ that allows ~org-gcal~ to find all its dependencies, and save it to
~.load-path.el~ in this directory. To delete this and other temporary files,
run ~make clean~.
* Usage
** Getting started
Once you've set up the basic settings (see [[Setting example]]), you can run
=org-gcal-fetch= to fetch events into the files configured in
=org-gcal-fetch-file-alist=. After the initial fetch, running =org-gcal-fetch=
will retrieve newly-created events and update already-fetched events.
To create a Google Calendar event from an Org-mode event, it's enough to run the
=org-gcal-post-at-point= command on a simple headline:
#+BEGIN_SRC org
,* Event title
#+END_SRC
This will prompt you for the calendar ID, start time, and end time of the event.
Therefore, if you'd like to create an event without user interaction (from a
template, for example), you should set these fields before running
=org-gcal-post-at-point=:
#+BEGIN_SRC org
,* Event title
:PROPERTIES:
:calendar-id: jjjjjjjjjjjfuuuk842fdok134@group.calendar.google.com
:END:
:org-gcal:
<2020-07-15 wed 09:15-09:30>
Event details
(can be multiple paragraphs).
:END:
#+END_SRC
After the event has been created, some Google Calendar API-specific fields will
be set for future updates to the event:
#+BEGIN_SRC org
,* Event title
:PROPERTIES:
:calendar-id: jjjjjjjjjjjfuuuk842fdok134@group.calendar.google.com
:ETag: "7777777777980000"
:ID: xxxxxxxxxxa4jlcj0v998f4u18/jjjjjjjjjjjfuuuk842fdok134@group.calendar.google.com
:END:
:org-gcal:
<2020-07-15 wed 09:15-09:30>
Event details
(can be multiple paragraphs).
:END:
#+END_SRC
If you want to schedule the event in your Org Agenda, you can use the SCHEDULED
property (set by =org-schedule=) instead of storing the date in the =:org-gcal:=
drawer. The drawer will still be present to contain event details:
#+BEGIN_SRC org
,* Event title
SCHEDULED: <2020-07-15 wed 09:15-09:30>
:PROPERTIES:
:calendar-id: jjjjjjjjjjjfuuuk842fdok134@group.calendar.google.com
:ETag: "7777777777980000"
:ID: xxxxxxxxxxa4jlcj0v998f4u18/jjjjjjjjjjjfuuuk842fdok134@group.calendar.google.com
:END:
:org-gcal:
Event details
(can be multiple paragraphs).
:END:
#+END_SRC
After editing an event in Org mode, you can also run =org-gcal-post-at-point= to
update the event on Google Calendar. The command =org-gcal-sync= does what
=org-gcal-fetch= does, but also runs =org-gcal-post-at-point= on all events that
you've edited in Org mode to update the corresponding events in Google Calendar.
** Event structure
=org-gcal= modifies the following Org-mode properties and drawers when updating
an event from the Google Calendar API:
- Title :: contains the event summary (minus any TODO keywords or tags).
- Timestamps:
- =SCHEDULED= :: if the =SCHEDULED= attribute of a headline is present,
=org-gcal= will maintain the start and end times of an event there rather
than in a timestamp in the =org-gcal= drawer (see below).
- Properties:
- =calendar-id= (can be modified using the [[help:org-gcal-calendar-id-property][=org-gcal-calendar-id-property=]]
variable) :: contains the calendar ID of the calendar on which the event is
maintained.
- =ETag= (can be modified using the [[help:org-gcal-etag-property][=org-gcal-etag-property=]]
variable) :: contains the most recent ETag retrieved from the Google Calendar
API for the event (see [[https://developers.google.com/calendar/v3/version-resources#conditional_modification][the Google Calendar API documentation]]). Used to
support automatically updating the headline using the most recent event data
from the API if it has changed on the server since it was last retrieved.
**NOTE**: If the =ETag= property is present on a headline (even if empty (=""=)) and not equal to the value
stored by the Calendar API, your event will be **overwritten** with the data from the server. In this case,
you'll see a notification for a HTTP 512. This is intended behavior. If your event is being overwritten when
you don't want it, remove the =ETag= property from your headline.
- =ID= :: contains =<event_id>/<calendar_id>= of the event, as provided by the
Google Calendar API. Don't change the ID manually, or else the event won't be
able to retrieved or updated from the headline.
- Drawers:
- =org-gcal= (can be modified using the [[help:org-gcal-drawer-name][=org-gcal-drawer-name=]] variable) ::
contains the event description. Unless the timestamp is maintained using
=SCHEDULED=, the initial line of this drawer contains the event start and
end time, with the event description starting in the next paragraph.
Apart from these, all other attributes are preserved when an event is updated
in any way.
** Commands
*** =org-gcal-fetch=
Fetch Google calendar events for all calendar IDs in =org-gcal-fetch-file-alist=
occurring between =org-gcal-up-days= before today and =org-gcal-down-days=
after today. If the events have already been retrieved and can be located
using their Org-mode headline IDs, update the event in place. Otherwise,
insert it at the end of the file corresponding to the event's calendar ID in
=org-gcal-fetch-file-alist=. Does not update events on the server.
*** =org-gcal-sync=
Like =org-gcal-fetch=, but also update events on the server if they have
changed locally.
Note: This command does not post /newly created events/ onto the server, which
is done with =org-gcal-post-at-point=. =org-gcal-sync= updates events /after/
they are posted to the server.
*** =org-gcal-fetch-buffer=
Fetch changes to Google calendar events to update entries in the current
buffer, but don't update events on server.
*** =org-gcal-sync-buffer=
Sync entries in the current buffer with Google Calendar.
*** =org-gcal-post-at-point=
Update the event represented by the Org-mode headline at POINT on the server
using the Google Calendar API.
If the event has changed on the server since it was last retrieved (detected
using the =ETag= property), automatically update the headline using the
event data from the server instead of updating the event on the server.
*** =org-gcal-delete-at-point=
Delete the event represented by the Org-mode headline at POINT on the server
using the Google Calendar API. This will not delete the Org-mode headline.
If the event has changed on the server since it was last retrieved (detected
using the =ETag= property), automatically update the headline using the
event data from the server instead of updating the event on the server.
** Deleting events
If an event is deleted on the server, then updating an event (via
=org-gcal-post-at-point=, =org-gcal-sync=, etc.) will optionally cancel and
delete the corresponding Org mode headlines:
- =org-gcal-update-cancelled-events-with-todo=: if set (the default), mark all
cancelled events with the =org-gcal-cancelled-todo-keyword= (default:
=CANCELLED=). This keyword must be one defined in =org-todo-keywords= or it
will be ignored.
- =org-gcal-remove-api-cancelled-events=: if set, delete Org headlines of
cancelled events. The default value is ='ask=, which means to prompt for
deletion.
- =org-gcal-remove-events-with-cancelled-todo=: by default, avoid deleting Org
headlines of events that are marked with =org-gcal-cancelled-todo-keyword=
beforehand, to allow preserving a headline for a cancelled event from being
deleted. Setting this to =t= always deletes headlines, even already cancelled
ones.
* Other features
** Minimize alerts
Modify =org-gcal-notify-p= from =t= to =nil=
** Strip HTML from event descriptions
Google Calendar returns event descriptions as HTML. By default, org-gcal
preserves that HTML so event descriptions can be edited locally without losing
formatting.
If you prefer plain text descriptions in Org, set
=org-gcal-strip-html-descriptions= to =t=:
#+BEGIN_SRC emacs-lisp
(setq org-gcal-strip-html-descriptions t)
#+END_SRC
You can override this global default for individual calendars with
=org-gcal-strip-html-descriptions-overrides=. This is useful when you want to
strip HTML for most calendars but preserve it for shared calendars:
#+BEGIN_SRC emacs-lisp
(setq org-gcal-strip-html-descriptions t
org-gcal-strip-html-descriptions-overrides
'(("shared-calendar@group.calendar.google.com" . nil)))
#+END_SRC
** Collect instances of recurring events under parent event
By default, =org-gcal-recurring-events-mode= is set to ='top-level=, which means
that new fetched events that are instances of recurring events will be inserted
at the top level of the file for the calendar ID configured in
=org-gcal-fetch-file-alist=:
#+BEGIN_SRC org
,* Meeting
SCHEDULED: <2020-08-07 Fri 11:00>
,* Meeting
SCHEDULED: <2020-08-14 Fri 11:00>
,* Meeting
SCHEDULED: <2020-08-21 Fri 11:00>
,* Meeting
SCHEDULED: <2020-08-28 Fri 11:00>
#+END_SRC
If =org-gcal-recurring-events-mode= is instead set to ='nested=, such events
will be inserted as Org-mode child headlines under the headline for the parent
event:
#+BEGIN_SRC org
,* Meeting
SCHEDULED: <2017-02-17 Fri 11:00>
,** Meeting
SCHEDULED: <2020-08-07 Fri 11:00>
,** Meeting
SCHEDULED: <2020-08-14 Fri 11:00>
,** Meeting
SCHEDULED: <2020-08-21 Fri 11:00>
,** Meeting
SCHEDULED: <2020-08-28 Fri 11:00>
#+END_SRC
Here the parent meeting has been running for several years, but only the
instances of the meeting in the range given by =org-gcal-down-days= and
=org-gcal-up-days= are fetched.
** Customizing the contents of event entries
If you would like to customize the contents of event entries (for example, to add a property from the Google Calendar API that's not automatically written to the Org-mode entry), you can add a function to the list =org-gcal-after-update-entry-functions=. For example, here is some code to add the =Effort= property to an entry based on the duration of the event (note that the current point is placed at the beginning of the entry when the function is called):
#+BEGIN_SRC emacs-lisp
(defun my-org-gcal-set-effort (_calendar-id event _update-mode)
"Set Effort property based on EVENT if not already set."
(when-let* ((stime (plist-get (plist-get event :start)
:dateTime))
(etime (plist-get (plist-get event :end)
:dateTime))
(diff (float-time
(time-subtract (org-gcal--parse-calendar-time-string etime)
(org-gcal--parse-calendar-time-string stime))))
(minutes (floor (/ diff 60))))
(let ((effort (org-entry-get (point) org-effort-property)))
(unless effort
(message "need to set effort - minutes %S" minutes)
(org-entry-put (point)
org-effort-property
(apply #'format "%d:%02d" (cl-floor minutes 60)))))))
(add-hook 'org-gcal-after-update-entry-functions #'my-org-gcal-set-effort)
#+END_SRC
** org-capture template
Here is a recommended snippet to use as a =org-capture-template= for making Google Calendar appointments:
#+begin_src elisp
(setq org-capture-templates
`(("a" "Appointment" entry (file ,(concat org-directory "/gcal.org"))
"* %?\n:PROPERTIES:\n:calendar-id:\tFirst.Last@gmail.com\n:END:\n:org-gcal:\n%^T--%^T\n:END:\n\n" :jump-to-captured t)))
#+end_src
Change the following:
- `/gcal.org` should be whichever org file you plan to capture appointments to. If this file is not in your generic =org-directory=, then adjust the `(concat org-directory "/gcal.org")` as needed.
- `First.Last@gmail.com` should be the calendar id you want to capture to. If this is your Gmail account's calendar, then just change `First.Last` to be your email handle.
** Sync automatically at regular times
This snippet uses `run-at-time` to perform a `org-gcal-sync` at specific times.
#+begin_src elisp
;; Run ‘org-gcal-sync’ regularly not at startup, but at 8 AM every day,
;; starting the next time 8 AM arrives.
(run-at-time
(let* ((now-decoded (decode-time))
(today-8am-decoded
(append '(0 0 8) (nthcdr 3 now-decoded)))
(now (encode-time now-decoded))
(today-8am (encode-time today-8am-decoded)))
(if (time-less-p now today-8am)
today-8am
(time-add today-8am (* 24 60 60))))
(* 24 60 60)
(defun my-org-gcal-sync-clear-token ()
"Sync calendar, clearing tokens first."
(interactive)
(require 'org-gcal)
(when org-gcal--sync-lock
(warn "%s" "‘my-org-gcal-sync-clear-token’: ‘org-gcal--sync-lock’ not nil - calling ‘org-gcal--sync-unlock’.")
(org-gcal--sync-unlock))
(org-gcal-sync-tokens-clear)
(org-gcal-sync)
nil))
#+end_src
* Troubleshooting
** Debugging
Because we used the [[https://github.com/kiwanami/emacs-deferred][deferred.el]] to perform asynchronous operations like calling
~request~ (via [[https://github.com/tkf/emacs-request/blob/master/request-deferred.el][~request-deferred~]]), normal Emacs debugging and stack traces tend
not to be as useful as usual. The best way to debug is to run ~M-x
org-gcal-toggle-debug~, which sets a variety of debugging variables to ease
debugging. The old values of the variables are saved so they can be restored by
another call to ~M-x org-gcal-toggle-debug~.
One of the most useful things this enables is logging of HTTP requests. Open the
~*request-log*~ buffer to see all requests issued by this library.
** Errors
*** Duplicate ID
You get an error like this:
#+BEGIN_EXAMPLE
Duplicate ID "FOO", also in file BAR
#+END_EXAMPLE
Most likely, this means some calendar events were mistakenly retrieved twice
(for example, if you ran =org-gcal-fetch= on different computers). Search your
Org-mode files for the duplicate ID "FOO" and delete one of the headlines with
duplicate IDs (or just change the =ID= property on one of the events to
something else).
* Similar applications
[[https://github.com/dengste/org-caldav][dengste/org-caldav]]
Not written in Markdown, so it's shown here as plain text — view it formatted on GitHub.
(top 30 of 33)
Emacs Lisp
99.5%