iCal4j Connector - Microsoft Graph
The Microsoft Graph connector exposes a Microsoft 365 user's Outlook calendars through the standard
ObjectStore and CalendarCollection interfaces. Each Outlook calendar becomes a CalendarCollection,
calendar groups are surfaced as workspaces, and each Graph event is converted to and from an iCal4j
Calendar containing a single VEVENT.
Setup
Add the module together with an Azure credential library. The module depends transitively on the
Microsoft Graph SDK, but the credential classes used to sign in live in azure-identity, which you must
add yourself.
dependencies {
implementation 'org.ical4j:ical4j-connector-msgraph:2.0.0-beta2'
implementation 'com.azure:azure-identity:1.18.6'
}
<dependency>
<groupId>org.ical4j</groupId>
<artifactId>ical4j-connector-msgraph</artifactId>
<version>2.0.0-beta2</version>
</dependency>
<dependency>
<groupId>com.azure</groupId>
<artifactId>azure-identity</artifactId>
<version>1.18.6</version>
</dependency>
Microsoft Entra application
The connector needs an application registration that is allowed to use the Graph calendar API:
- Register an application in the Microsoft Entra admin center and note its Application (client) ID and Directory (tenant) ID.
- Under Authentication, enable Allow public client flows if you intend to use the device code or interactive browser flows shown below.
- Under API permissions, add the Microsoft Graph delegated permission
Calendars.ReadWrite(orCalendars.Readfor read-only access).
The store operates on the signed-in user's mailbox via the /me endpoint, so a delegated (user) sign-in
is required. Application-only credentials such as a client secret without a user context will not work.
Authentication
The connector does not perform authentication itself. You construct an authenticated
GraphServiceClient and pass it to the store. The connect() and disconnect() methods on the store
are no-ops that exist only to satisfy the ObjectStore interface.
The example below uses the device code flow, which prints a URL and a code for the user to enter in any browser. It suits command-line tools and headless environments.
import com.azure.identity.DeviceCodeCredential;
import com.azure.identity.DeviceCodeCredentialBuilder;
import com.microsoft.graph.serviceclient.GraphServiceClient;
DeviceCodeCredential credential = new DeviceCodeCredentialBuilder()
.clientId("<application-client-id>")
.tenantId("<directory-tenant-id>")
.challengeConsumer(challenge -> System.out.println(challenge.getMessage()))
.build();
GraphServiceClient client = new GraphServiceClient(credential, "Calendars.ReadWrite");
For a desktop application, InteractiveBrowserCredentialBuilder opens the system browser instead. Any
TokenCredential from azure-identity that yields a delegated user token can be used in the same way.
Creating the store
import org.ical4j.connector.msgraph.MSGraphCalendarStore;
MSGraphCalendarStore store = new MSGraphCalendarStore(client);
Working with collections
Outlook organises calendars into calendar groups. The connector exposes those groups as workspaces, so
listWorkspaceIds() returns the user's calendar group ids and the workspace-parameterised methods
operate inside the named group. Methods without a workspace argument operate on the user's default
calendar list. Listing follows Graph's @odata.nextLink paging, so every group and calendar is returned.
// calendar group ids
List<String> groups = store.listWorkspaceIds();
// calendars in the user's default list
List<CalendarCollection> collections = store.getCollections();
for (CalendarCollection collection : collections) {
System.out.println(collection.getDisplayName());
}
// calendars within a specific calendar group
List<CalendarCollection> groupCalendars = store.getCollections(groups.get(0));
// wrap a known calendar id (no request is made until the collection is used)
CalendarCollection calendar = store.getCollection("<calendar-id>");
CalendarCollection grouped = store.getCollection("<calendar-id>", groups.get(0));
// create a calendar in the default list, or inside a calendar group
CalendarCollection team = store.addCollection("Team Events");
CalendarCollection projects = store.addCollection("Projects", groups.get(0));
// delete a calendar, either by id or through the collection
store.removeCollection("<calendar-id>");
team.delete();
Collection ids are Graph calendar ids. A Graph calendar carries only a name, so the five-argument form
of addCollection() ignores the id, description, supported components and time zone arguments and
creates the calendar by name alone. Likewise getDescription() returns an empty string and
getTimeZone() returns null.
Working with events
Each event is represented as an iCal4j Calendar containing a single VEVENT. Events are identified by
Graph's iCalUId, which is assigned by the server on creation. The UID you submit is not preserved,
so always use the identifier returned by add() for later lookups.
import net.fortuna.ical4j.model.Calendar;
import net.fortuna.ical4j.model.component.VEvent;
CalendarCollection collection = store.getCollection("<calendar-id>");
// add an event; the returned string is the iCalUId assigned by Graph
VEvent event = new VEvent(LocalDate.of(2026, 6, 1), "Team offsite");
String uid = collection.add(new Calendar().withComponent(event).getFluentTarget());
// list the UIDs of every event in the calendar
List<String> uids = collection.listObjectUIDs();
// fetch a single event by UID
Optional<Calendar> found = collection.get(uid);
// merge a multi-event calendar; one VEVENT is added per UID and the assigned ids are returned
Uid[] assigned = collection.merge(importedCalendar);
// export the whole calendar as a single iCalendar object
Calendar everything = collection.export();
// delete events by UID; the removed events are returned, unknown UIDs are skipped
List<Calendar> removed = collection.removeAll(uid);
Lookups filter on iCalUId server-side where the service allows it and fall back to a paged scan if
Graph rejects the filter. Only series master events are listed, so occurrences of a recurring event
appear once, as the series.
Field mapping
The following properties are mapped between a VEVENT and a Graph Event. Any other property is dropped
without error when writing to Graph.
iCal4j VEVENT property |
Microsoft Graph Event field |
|---|---|
UID |
iCalUId (read only; assigned by Graph on write) |
SUMMARY |
subject |
DESCRIPTION |
body.content with contentType = text |
LOCATION |
location.displayName |
DTSTART / DTEND (date) |
start / end at midnight with isAllDay = true |
DTSTART / DTEND (date-time) |
start.dateTime + start.timeZone (and end) |
ORGANIZER |
organizer.emailAddress.address / .name |
ATTENDEE |
attendees[].emailAddress / status.response |
RRULE |
recurrence (PatternedRecurrence) |
CREATED |
createdDateTime (read only) |
LAST-MODIFIED |
lastModifiedDateTime (read only) |
Attendee participation status maps as follows.
PARTSTAT |
Graph status.response |
|---|---|
NEEDS-ACTION |
none / notResponded |
ACCEPTED |
accepted |
DECLINED |
declined |
TENTATIVE |
tentativelyAccepted |
Time zones
Graph identifies time zones by name and by default returns Windows zone names such as
AUS Eastern Standard Time. The connector ships a Windows-to-IANA mapping so events are read back in
their original zone, giving a faithful round trip for values such as
DTSTART;TZID=Australia/Melbourne:20260601T093000.
- A
TZIDnaming a known IANA or Windows zone is written with its local time in that zone. - UTC values, fixed-offset values and unrecognised
TZIDvalues are converted to the same instant in UTC. - Floating values (no
TZIDand noZ) keep their wall-clock time in the builder's floating time zone, which defaults to the JVM's system zone. - On read, a Graph zone name the connector does not recognise is emitted as a floating value rather than being mislabelled as UTC.
Recurrence
RRULE values are converted to Graph's structured recurrence pattern. Daily, weekly, monthly and yearly
rules with INTERVAL, COUNT, UNTIL, a single BYDAY week offset or BYSETPOS, a single
BYMONTHDAY and BYMONTH are supported. A weekly rule without BYDAY repeats on the DTSTART weekday,
as Graph requires. Rules Graph cannot express, such as FREQ=HOURLY, several BYMONTHDAY values or
BYDAY=-2FR, cause the event to be created without recurrence and a warning to be logged.
Body text
Descriptions are written as plain text. When Graph returns an HTML body, the markup is stripped and the
text is placed in DESCRIPTION.
The two builder classes can also be used directly when you want to convert events without going through a collection. The event builder additionally lets you choose the zone used for floating times.
import org.ical4j.connector.msgraph.MSGraphEventBuilder;
import org.ical4j.connector.msgraph.ICalCalendarBuilder;
com.microsoft.graph.models.Event graphEvent = new MSGraphEventBuilder()
.vevent(vevent)
.floatingTimeZone(ZoneId.of("Australia/Melbourne"))
.build();
Calendar calendar = new ICalCalendarBuilder().build(graphEvent);
Limitations
- Only the series master
VEVENTin the calendar passed toadd()is stored. Recurrence overrides (instances with aRECURRENCE-ID) are ignored with a warning, andRDATEandEXDATEare dropped. merge()skips objects that contain noVEVENT, such asVTODOorVJOURNAL, with a warning.- The submitted
UIDis not preserved. Track events by the identifier returned fromadd()ormerge(). - Collection metadata such as
getSupportedComponentTypes()and the size limits return stub values rather than data from Graph. - Store and collection listener lists are not supported and return
null. - The
servicepackage contains placeholders for To Do, Planner and OneNote integration that are not yet implemented.