GLPI Calendar Sync
Publishes GLPI schedules to external calendar applications (Outlook, Exchange, Google Calendar, Apple Calendar, Thunderbird) via iCalendar feeds and invites.
Work planned in GLPI often needs to be visible in external calendars. Without syncing, scheduled maintenance windows or assigned tasks are easy to overlook when planning other commitments.
Two delivery methods are provided:
| Subscription feed | Email invitation | |
|---|---|---|
| How | one secret URL per technician, webcal:// or https:// |
text/calendar; method=REQUEST in the message |
| Clients | Outlook “Subscribe from web”, Google “From URL”, Apple, Thunderbird | Outlook, Gmail, Apple Mail — with Accept and Decline |
| Good for | a whole schedule, always current | “you’ve been assigned this window”, the moment it happens |
| Weakness | the client refreshes when it wants — Outlook can lag hours | one event per message; needs UID/SEQUENCE discipline |
Not the Microsoft Graph or Google Calendar APIs: those need an application registration and admin consent per tenant, a stored refresh token per user, and between them they still only cover two providers. iCalendar covers everything.
Why not just use what GLPI has?
Section titled “Why not just use what GLPI has?”GLPI ships an ICS export at front/planning.php?genical=1 and a CalDAV server.
The CalDAV server does not answer this problem at all: Exchange and Gmail are not CalDAV clients.
The ICS export is real, and thin:
| Core export | This plugin | |
|---|---|---|
| Event identity | Ticket#12 — not unique between instances, so two GLPIs overwrite each other in one calendar |
planning-tickettask-12@your.host, stable across polls |
DTSTAMP |
absent | the row’s own modification time, so an unchanged feed is byte-identical |
| Calendar name | none — the subscription is listed under its URL, which is a secret | X-WR-CALNAME and NAME |
| Refresh hint | none, so some clients default to daily | REFRESH-INTERVAL and X-PUBLISHED-TTL |
| Reminders | none | opt-in VALARM per subscription |
| Free/busy | everything marks you busy | informational events are TRANSPARENT |
| Delivery | Content-Disposition: attachment |
inline, with ETag/Last-Modified and 304s |
| Horizon | fixed ±12 months | configurable, snapped to midnight so the ETag holds |
| Change windows | not present — a change has no plan dates in core, so it is not a planning type | implementation windows, releases and freezes |
And the part that is not cosmetic:
Core’s feed authenticates with the user’s
personal_token— the same credential that unlocks the REST API. Pasting that URL into Google Calendar hands a third party API access to the instance, and revoking it breaks every other integration sharing the token.
Feeds minted here use their own token, which grants one calendar and nothing else and is revoked on its own.
What a feed can carry
Section titled “What a feed can carry”Chosen per feed, by the person the feed belongs to, from what the administrator permits:
- My planning — ticket, problem and change tasks, reservations, project
tasks, reminders, external events. Sourced through core’s own
populatePlanning(), so the visibility rules are core’s rather than a second implementation of them. - Change implementation windows — with risk band and collision count in the description. (needs glpi-change)
- Releases (needs glpi-change)
- Change freeze periods — all-day and transparent, so a fortnight-long blackout does not mark every technician unbookable. (needs glpi-change)
- On-call shifts — projected from the rota’s rotation. (needs glpi-signal)
glpi-change and glpi-signal are optional. Every source guards on
class_exists(), so this plugin installs and serves feeds on a stock GLPI.
Invitations
Section titled “Invitations”Off on installation. A feed is inert until somebody creates one, so activating this plugin mails nobody; invitations are the opposite, and are an explicit choice rather than a surprise.
Sent when a change acquires or moves an implementation window (to everyone assigned, not to requesters or observers — Accept/Decline implies a say in whether it happens), and when a planned task is assigned to a technician.
The hard part is not sending, it is not sending. item_update fires on every
save, and a change is saved many times a day for reasons that have nothing to do
with its schedule. Every path is gated on the event actually differing from what
was last sent, recorded per event key. Mailing an invitation on every save
trains people to ignore them within a week.
Delivery is direct through GLPI’s configured SMTP transport rather than the
notification queue — the queue models a message as subject, text, HTML and
Document attachments, and there is no hook anywhere in
NotificationEventMailing::send() to add a MIME part. The structure is the one
Exchange and Google themselves send: multipart/mixed wrapping a
multipart/alternative of plain text, HTML and the calendar, plus an .ics
attachment for clients that only look for a file.
The core URL
Section titled “The core URL”With intercept_core_feed on (the default), this plugin answers
front/planning.php?genical=1 too, so subscriptions already pasted into
people’s clients get the better document without anyone editing a URL.
Authorisation is core’s, copied unchanged — the same people can read the same
plannings.
It upgrades the document. It cannot upgrade the credential: that URL still
carries a personal_token. Both the settings page and each user’s own tab say
so and point at the replacement.
Managing a feed
Section titled “Managing a feed”Preferences → Calendar feeds. Feeds are personal: no screen anywhere shows somebody else’s URL. An administrator can see that a feed exists and revoke it — that is incident response — but never read it.
Revoking purges the row rather than flagging it, because the point of the button is that the URL sitting in Google’s servers stops working.
docker compose -p glpi exec glpi sh -c 'cd /var/www/glpi/plugins/glpicalsync && tests/run.sh'61 pure assertions over the document format, the invitation rules and token
parsing. --with-db additionally exercises a real subscription against the live
endpoint and cleans up after itself.