ExDAV synchronizes the calendar of a Microsoft Exchange account to a CalDAV server such as Nextcloud, SOGo or Mailcow. Currently, only one-way synchronization from Exchange to CalDAV is supported. The synchronization covers single events, recurring series, modified occurrences and deletions.
Exchange is accessed read-only; existing non-ExDAV events in the target calendar are left untouched. A dry-run mode is available to preview changes before they are written.
- Strictly one-way synchronization with read-only access to Exchange
- Creates, updates and removes CalDAV copies as Exchange events change
- Supports single events, recurring series, modified occurrences and deletions
- Works with Nextcloud, SOGo and Mailcow calendars
- Leaves existing non-ExDAV events in the target calendar untouched
- Offers a dry-run mode to preview changes before writing them
- Stores synchronization state and operational logs in a local SQLite database
| Exchange event attribute | CalDAV/iCalendar property | Behavior |
|---|---|---|
| Subject | SUMMARY |
Copied as the event title |
| Plain-text body | DESCRIPTION |
HTML-only bodies are not copied |
| Location | LOCATION |
Copied as plain text |
| Categories | CATEGORIES |
All categories are copied |
| Start | DTSTART |
Timed single events are written in UTC |
| End | DTEND |
Written as the event's end, not as a duration |
| All-day setting | DTSTART, DTEND |
Written as dates with an exclusive end date |
| Start and end time zones | DTSTART, DTEND, VTIMEZONE |
Recurring events retain their IANA time zone |
| Sensitivity | CLASS |
Mapped to public, private or confidential |
| Free/busy state | TRANSP |
Free events are transparent; others are busy |
| Tentative state | STATUS |
Written as tentative when Exchange reports it |
| Cancellation state | STATUS |
Cancelled events are marked as cancelled |
| Appointment sequence | SEQUENCE |
Copied when available |
| Creation time | CREATED |
Copied when available |
| Last modification time | LAST-MODIFIED, DTSTAMP |
Used as the event's latest timestamp |
| Exchange UID | X-EXDAV-SOURCE-UID |
Kept for reference; the CalDAV UID is generated by ExDAV |
| Recurrence rule | RRULE |
Supports daily, weekly, monthly and yearly series |
| Additional recurrence dates | RDATE |
Copied when supported by Exchange recurrence data |
| Deleted occurrences | EXDATE |
Removed dates are excluded from the series |
| Modified occurrences | RECURRENCE-ID |
Copies changed time, text, location, categories, privacy and status |
ExDAV does not copy organizers, attendees, reminders, attachments, HTML bodies or unknown Exchange-specific properties.
- Linux or macOS operating system.
- Conda or Miniconda package/environment management.
- Read access to a Microsoft Exchange mailbox through EWS.
- Read/write access to a Nextcloud, SOGo or Mailcow CalDAV calendar.
Place the ExDAV source files in ~/ExDAV. Then create a Python 3.12 conda environment and install its pinned dependencies:
cd ~/ExDAV
conda create --name exdav python=3.12 pip
conda activate exdav
python -m pip install -r ~/ExDAV/requirements.txtRename the example configuration and restrict it to the current user:
mv ~/ExDAV/config.example.toml ~/ExDAV/config.toml
chmod 600 ~/ExDAV/config.tomlEdit ~/ExDAV/config.toml and provide:
- The Exchange server hostname, mailbox, credentials and IANA time zone.
- The CalDAV server type, HTTPS URL, credentials and target calendar name.
- The database saving location and operation timeout. Use an absolute database path.
ExDAV reads config.toml from the current working directory. Run all ExDAV commands from ~/ExDAV.
The Exchange server must be a hostname, not a URL. Common CalDAV root URLs are:
| Server | Example URL |
|---|---|
| Nextcloud | https://cloud.example.com/remote.php/dav |
| SOGo | https://sogo.example.com/SOGo/dav |
| Mailcow | https://mail.example.com/SOGo/dav |
Initialize ExDAV once after the final Exchange and CalDAV settings are in place:
python -m exdav initThe state database links Exchange events to their CalDAV copies. Keep it in a durable location and back it up; deleting it can cause duplicate events.
Preview the next synchronization without changing CalDAV:
python -m exdav sync --dry-runTo run a synchronization:
python -m exdav syncIf the conda environment is not active yet, use:
cd ~/ExDAV
conda run --name exdav python -m exdav sync| Command | Purpose |
|---|---|
python -m exdav init |
Discover both calendars and initialize the synchronization state |
python -m exdav sync |
Run one synchronization from Exchange to CalDAV |
python -m exdav sync --dry-run |
Preview creates, updates and deletions without changing CalDAV |
python -m exdav retry-exhausted |
Reopen operations blocked after exhausting their bounded retry cycle |
python -m exdav status |
Show state, recent activity and connection health |
python -m exdav log |
Show stored operational logs without contacting either server |
ExDAV is built on three established Python libraries:
- exchangelib reads calendar events and changes through Exchange Web Services (EWS).
- icalendar converts and sanitizes events into standard iCalendar data.
- python-caldav discovers the target calendar and creates, updates or removes CalDAV resources.
ExDAV assigns its own stable UID to every copied event and marks it with X-EXDAV-MANAGED. This lets it manage only the resources it created while leaving other events in the CalDAV calendar alone.
ExDAV was tested using live Microsoft Exchange 2016, Nextloud 34, Mailcow 2026-03b and SoGO 5.12.8 servers.