---
title: "Convert an Outlook appointment (MSG) to ICS in C#"
url: https://recursiveai.org/onemail/cookbook/convert-msg-to-ics/
description: "Convert an Outlook calendar item (.msg appointment, meeting or task) to an .ics file in C#: start, end, time zone, location, attendees and recurrence, with a loss report. Runnable code with OneMail 0.3.0-preview.2."
version: "0.3.0-preview.2"
---

# Convert an Outlook appointment (MSG) to ICS in C#

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.

```shell
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`:

```csharp
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)}");
```

```text
Appointment: Quick brown fox
appointment.ics  942 bytes
  Dropped OrganizerWithoutSmtpAddress: 1
  Dropped RtfBodyNotCarried: 1
  Dropped NoIcalMapping: 49
  Internal: 33
```

The file it writes:

```text
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](https://recursiveai.org/onemail/docs/calendar-items/). Reading the `.ics` back: [Read an ICS calendar file](https://recursiveai.org/onemail/cookbook/read-ics-file/).
