Open the .msg with Mail.Open and call Mail.Convert(item, ConvertFormat.Ics). You get the iCalendar bytes, ready for any calendar app, and a report of every Outlook property that didn’t carry over. Meetings, tasks and recurring series convert the same way, following Microsoft’s published MS-OXCICAL rules. The program below converts the appointment test file of Apache POI.
dotnet new console -o MsgToIcs && cd MsgToIcs dotnet add package RecursiveAI.OneMail --version 0.3.0-preview.2 curl -LO https://raw.githubusercontent.com/apache/poi/REL_5_4_1/test-data/hsmf/msgClassAppointment.msg
Put this in Program.cs and run dotnet run:
using RecursiveAI.OneMail; using RecursiveAI.OneMail.Conversion; using RecursiveAI.OneMail.Conversion.Stores; using MailContainer file = Mail.Open("msgClassAppointment.msg"); IMailItem item = file.Enumerate().First(); Console.WriteLine($"{item.Kind}: {item.Subject}"); // Appointments, meetings and tasks become iCalendar; a plain email gives null ConversionResult<byte[]?> ics = Mail.Convert(item, ConvertFormat.Ics); if (ics.Value is null) { Console.WriteLine("Not a calendar item"); return; } File.WriteAllBytes("appointment.ics", ics.Value); Console.WriteLine($"appointment.ics {ics.Value.Length:N0} bytes"); // What didn't carry over: content first, store bookkeeping only counted foreach (var loss in ics.Report.Where(l => l.Kind != LossKind.Internal) .GroupBy(l => (l.Kind, l.Code))) Console.WriteLine($" {loss.Key.Kind} {loss.Key.Code}: {loss.Count()}"); Console.WriteLine($" Internal: {ics.Report.Count(l => l.Kind == LossKind.Internal)}");
Appointment: Quick brown fox appointment.ics 942 bytes Dropped OrganizerWithoutSmtpAddress: 1 Dropped RtfBodyNotCarried: 1 Dropped NoIcalMapping: 49 Internal: 33
The file it writes:
BEGIN:VCALENDAR VERSION:2.0 PRODID:-//Recursive AI//OneMail//EN METHOD:PUBLISH BEGIN:VTIMEZONE TZID:Eastern Standard Time BEGIN:STANDARD DTSTART:20071104T020000 TZOFFSETFROM:-0400 TZOFFSETTO:-0500 RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU END:STANDARD BEGIN:DAYLIGHT DTSTART:20070311T020000 TZOFFSETFROM:-0500 TZOFFSETTO:-0400 RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU END:DAYLIGHT END:VTIMEZONE BEGIN:VEVENT UID:040000008200E00074C5B7101A82E008000000003042F07DC891D201000000000000000 01000000091B31F0C0B120942B66C023A533DD16C DTSTAMP:20170228T184317Z DTSTART;TZID=Eastern Standard Time:20170228T133000 DTEND;TZID=Eastern Standard Time:20170228T140000 SUMMARY:Quick brown fox LOCATION:under lazy dog DESCRIPTION:This is a meeting to discuss jumping CLASS:PUBLIC PRIORITY:5 TRANSP:OPAQUE X-MICROSOFT-CDO-BUSYSTATUS:BUSY SEQUENCE:0 CREATED:20170228T184317Z LAST-MODIFIED:20170228T184317Z END:VEVENT END:VCALENDAR
What carries over
Python’s icalendar 7.3.0 reads the file back as 13:30 to 14:00 on February 28, 2017, at UTC−05:00, US Eastern time. Start and end keep the item’s own time zone, written as a VTIMEZONE. Subject, location, description, attendees with their roles and responses, the organizer, reminder, busy status, privacy and the recurrence pattern with its exceptions all map to their iCalendar properties. The UID comes from Outlook’s global object ID, so updates to the same meeting match up in other calendars.
The report names the rest. Here the organizer had no SMTP address in the file, so ORGANIZER is left out (OrganizerWithoutSmtpAddress), and the RTF body isn’t carried (its text is the DESCRIPTION). NoIcalMapping entries are Outlook properties iCalendar has no field for. Internal entries are store bookkeeping such as entry IDs.
A whole calendar
To convert every appointment of a PST, OST or OLM, pass the store to Mail.ConvertAsync(path, ConvertFormat.Ics, "calendar"): one .ics per item, in a folder tree like the store’s. From the command line: onemail convert --to ics archive.pst calendar. For the reverse direction, IcsToMapiConverter turns iCalendar into an Outlook item in memory.
Limits
- A preview. 0.3.0-preview.2: the API can change before 1.0.
- No Outlook-written meetings yet. The project checked this on the two appointments of the public test files and on generated items: “the corpus holds 2 distinct appointment and 2 task items and no meeting or recurring item: those are covered by generated items following the MS-OXOCAL examples”.
- No MSG out. iCalendar to Outlook works in memory only; OneMail can’t write MSG or PST files yet.
Every mapping and loss code: Outlook calendar and task items. Reading the .ics back: Read an ICS calendar file.