Skip to content

Graph: Improve Event TimeZone Handling #469

@tmcqueen-materials

Description

@tmcqueen-materials

Thank you for the great tool, and for working proactively to add Microsoft Graph (#404) support prior to EWS discontinuation.

In testing out the CalDav support (at 16b84b5), I observe that the user timezone (as set in mailbox settings on the server, or manually with davmail.timezoneId=XXX) does not necessarily match the timezone of the datetimes returned by Graph for event objects. For example, the user timezone might be Eastern Standard Time but all datetimes returned by graph have timezone of UTC.

This causes two problems.

Problem 1: Events end up at incorrect times because the timezone is changed to the user timezone value, but the time itself is left in UTC. What happens is convertDateTimeTimeZoneToVProperty takes the Graph datetime and converts it to ICS formatting style, and (correctly!) keeps the Graph-provided timezone as TZID. But then fixVCalendar blindly replaces the TZID with the user timezone, without doing the time conversion. Simply changing the VCalendar timezone to be that of the start date, instead of the user timezone is not an appropriate fix to the problem because of problem 2...

Problem 2: Even when using the Graph-reported timezone for DSTART, events with recurrences that cross daylight savings time transitions may end up with incorrect times on one side or the other. It appears to be the case that Microsoft Graph (at least on my tenant) always returns event times in UTC. The start times are correct (i.e. the specified UTC time is in fact the start time of the event on that date), but for recurring events, using the timezone UTC in the ICS file then fails to capture the shift in time across the daylight savings time transition.

For example, a recurring event created in Microsoft O365 online to occur at 2:30 PM every Thursday starting on August 1, 2025 in America/New_York will, through Microsoft online, correctly appear at 2:30 PM (in user timezone) in August and in December. Microsoft Graph returns the start time of this event series in UTC, but when the ICS file is then returned with the use of UTC, the fact that daylight savings time should be applied is lost, and so the ICS file implies a 2:30 PM start in August but a 1:30 PM start in December.

I am not entirely sure what the correct solution is here, but what is working for me is a two-fold change:

  1. Modify convertDateTimeTimeZoneToVProperty to convert from the Graph-provided timezone to the user timezone (whether that is the timezone from the server or set manually in davmail). Viz:
    private VProperty convertDateTimeTimeZoneToVproperty(String vPropertyName, JSONObject jsonDateTimeTimeZone) throws DavMailException {
        if (jsonDateTimeTimeZone != null) {
            String timeZone = jsonDateTimeTimeZone.optString("timeZone");
            String dateTime = convertDateFromExchange(jsonDateTimeTimeZone.optString("dateTime"));
            SimpleDateFormat parser = new SimpleDateFormat("yyyyMMdd'T'HHmmss");
            SimpleDateFormat formatter = new SimpleDateFormat("yyyyMMdd'T'HHmmss");
            parser.setTimeZone(TimeZone.getTimeZone(convertTimezoneFromExchange(timeZone)));
            formatter.setTimeZone(TimeZone.getTimeZone(convertTimezoneFromExchange(getVTimezone().getPropertyValue("TZID"))));
            try {
                dateTime = formatter.format(parser.parse(Character.isDigit(dateTime.charAt(dateTime.length()-1)) ? (dateTime) : (dateTime.substring(0, dateTime.length()-1))));
                timeZone = getVTimezone().getPropertyValue("TZID");
            } catch (ParseException e) {
                LOGGER.warn("Unable to convert to user timezone: " + dateTime + ", " + timeZone + ", " + getVTimezone().getPropertyValue("TZID"));
            }
            VProperty vProperty = new VProperty(vPropertyName, dateTime);
            vProperty.addParam("TZID", timeZone);
            return vProperty;
        }
        return new VProperty(vPropertyName, null);
    }
  1. Modify fixVCalendar to not blindly clobber TZID, but instead do the Exchange->Universal name conversion individually each time. This is probably just defense in depth given the first change, but I see no harm to correctly doing this, always, as the ICS specification allows different VTIMEZONE and TZIDs within a single ICS file.

Let me know if more information is needed to track this down. It is quite possible there are similar issues in the inverse (upload) direction, and/or for tasks/todo, but I have not checked.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions