Hello @chetan1,
Hello,
The values are not being “shifted” by Aspose.Email, they are being interpreted. The MAPI
property that actually carries the appointment time (PidLidAppointmentStartWhole) is defined
as UTC, and Outlook always renders it in the local time zone of the machine that opens the
item.
So everything depends on what Aspose.Email assumes about the DateTime you assign:
MapiCalendar.StartDate/EndDate - always taken as UTC, DateTimeKind is ignored
Appointment.StartDate/EndDate - taken as a value in the local zone of the machine, unless Kind == Utc (then taken as UTC), or unless SetTimeZone(...) was called (then taken as a value in that zone)
That is why you see exactly +5:30: your IST clock value ends up stored as if it were UTC (this happens with MapiCalendar, with a Kind == Utc value, or when the process runs on a server whose local zone is UTC), and Outlook on an IST client adds the offset back.
The fix: tell Aspose.Email which time zone values belong to, or you hand over real UTC values.
Option 1 (recommended) — keep your IST values, declare the zone
var appointment = new Appointment(location, summary, description, start, end, organizer, attendees);
appointment.StartDate = start; // 2026-09-10 14:00, IST wall-clock, Kind = Unspecified
appointment.EndDate = end; // 2026-09-10 15:30
appointment.SetTimeZone("India Standard Time"); // <-- this is the missing piece
// equivalent to:
// appointment.StartTimeZone = "India Standard Time";
// appointment.EndTimeZone = "India Standard Time";
MapiMessage msg = appointment.ToMapiMessage();
msg.Save("appointment.msg");
SetTimeZone does two things: it makes Aspose.Email convert 14:00 IST -> 08:30 UTC when
writing PidLidAppointmentStartWhole, and it writes the time-zone definition properties
(PidLidAppointmentTimeZoneDefinitionStartDisplay / ...EndDisplay,
PidLidTimeZoneDescription), so Outlook also shows the correct zone in the appointment form and
expands recurrences with the correct DST rules. For ICS output the same call produces
DTSTART;TZID=India Standard Time:20260910T140000.
Important: SetTimeZone does not re-interpret a value whose Kind is already Utc — pass
DateTimeKind.Unspecified (or Local) wall-clock values here.
Option 2 — convert to UTC yourself
TimeZoneInfo ist = TimeZoneInfo.FindSystemTimeZoneById("India Standard Time"); // "Asia/Kolkata" on Linux
appointment.StartDate = TimeZoneInfo.ConvertTimeToUtc(start, ist); // Kind = Utc
appointment.EndDate = TimeZoneInfo.ConvertTimeToUtc(end, ist);
appointment.SetTimeZone(ist.Id); // optional, but keeps the zone visible in Outlook
A DateTime with Kind == Utc is stored verbatim, so this is stable regardless of the server’s
own time zone (which is the usual reason the same code behaves differently on a dev machine and
on an Azure/Linux host running UTC).
If you build the item as MapiCalendar
MapiCalendar takes the values as UTC unconditionally — StartDateTimeZone only tags the
zone, it does not convert. So the conversion is mandatory there:
var cal = new MapiCalendar(location, summary, description,
TimeZoneInfo.ConvertTimeToUtc(start, ist),
TimeZoneInfo.ConvertTimeToUtc(end, ist))
{
StartDateTimeZone = new MapiCalendarTimeZone(ist),
EndDateTimeZone = new MapiCalendarTimeZone(ist)
};
cal.Save("appointment.msg", AppointmentSaveFormat.Msg);
Thank you.