Skip to content

Instantly share code, notes, and snippets.

@MnkyArts
Created October 10, 2025 14:03
Show Gist options
  • Select an option

  • Save MnkyArts/e056759631e347565850a65a42d67761 to your computer and use it in GitHub Desktop.

Select an option

Save MnkyArts/e056759631e347565850a65a42d67761 to your computer and use it in GitHub Desktop.
Outlook Calendar Bidirectional Sync Script it syncs events between two calendars in Outlook
# Outlook Calendar Bidirectional Sync Script
# Syncs events between two calendars in Outlook
# ============================================
# CONFIGURATION - EDIT THESE VALUES
# ============================================
# Email addresses of your two accounts
$Email1 = "example@example.com"
$Email2 = "example2@example2.com"
# Sync window (how many days forward and backward to sync)
$DaysBack = 7
$DaysForward = 90
# Log file location
$LogFile = "$env:USERPROFILE\Documents\CalendarSync.log"
# ============================================
# SCRIPT START
# ============================================
function Write-Log {
param($Message)
$timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
"$timestamp - $Message" | Out-File -FilePath $LogFile -Append
Write-Host "$timestamp - $Message"
}
function Get-Calendar {
param($EmailAddress)
try {
$namespace = $outlook.GetNamespace("MAPI")
$recipient = $namespace.CreateRecipient($EmailAddress)
$recipient.Resolve() | Out-Null
if ($recipient.Resolved) {
$calendar = $namespace.GetSharedDefaultFolder($recipient, 9) # 9 = olFolderCalendar
return $calendar
} else {
Write-Log "ERROR: Could not resolve calendar for $EmailAddress"
return $null
}
} catch {
Write-Log "ERROR: Failed to get calendar for $EmailAddress - $_"
return $null
}
}
function Get-SyncIdentifier {
param($Item)
# Create unique identifier from subject, start time, and location
$subject = if ($Item.Subject) { $Item.Subject } else { "" }
$start = if ($Item.Start) { $Item.Start.ToString("yyyyMMddHHmm") } else { "" }
$location = if ($Item.Location) { $Item.Location } else { "" }
return "$subject|$start|$location"
}
function Find-MatchingEvent {
param(
$SourceItem,
$TargetCalendar
)
try {
$sourceSubject = if ($SourceItem.Subject) { $SourceItem.Subject.Trim() } else { "" }
$sourceLocation = if ($SourceItem.Location) { $SourceItem.Location.Trim() } else { "" }
# For recurring events, use pattern start date
if ($SourceItem.IsRecurring) {
try {
$recPattern = $SourceItem.GetRecurrencePattern()
$searchStart = $recPattern.PatternStartDate.Date
} catch {
$searchStart = $SourceItem.Start.Date
}
} else {
$searchStart = $SourceItem.Start
}
# Search for matching event in target calendar (wider time window)
$filterStart = $searchStart.AddDays(-2).ToString("g")
$filterEnd = $searchStart.AddDays(2).ToString("g")
$filter = "[Start] >= '$filterStart' AND [Start] <= '$filterEnd'"
$items = $TargetCalendar.Items.Restrict($filter)
foreach ($item in $items) {
$targetSubject = if ($item.Subject) { $item.Subject.Trim() } else { "" }
$targetLocation = if ($item.Location) { $item.Location.Trim() } else { "" }
# Check if subjects match (ignore case)
if ($targetSubject -eq $sourceSubject -or
$targetSubject.ToLower() -eq $sourceSubject.ToLower()) {
# For recurring events, compare recurrence patterns
if ($SourceItem.IsRecurring -and $item.IsRecurring) {
try {
$sourceRec = $SourceItem.GetRecurrencePattern()
$targetRec = $item.GetRecurrencePattern()
# Match if same recurrence type and similar start times
if ($sourceRec.RecurrenceType -eq $targetRec.RecurrenceType -and
$sourceRec.StartTime.ToString("HHmm") -eq $targetRec.StartTime.ToString("HHmm")) {
return $item
}
} catch {
# If can't compare recurrence, fall through to other checks
}
}
# For non-recurring or if recurrence check failed, check start time
$timeDiff = [Math]::Abs(($item.Start - $SourceItem.Start).TotalMinutes)
if ($timeDiff -le 5) { # Within 5 minutes
# If location also matches (or both empty), it's definitely the same event
if ($targetLocation -eq $sourceLocation) {
return $item
}
# Even without location match, if subject and time match closely, treat as duplicate
if ($timeDiff -eq 0) {
return $item
}
}
}
}
return $null
} catch {
Write-Log "WARNING: Error in Find-MatchingEvent for '$($SourceItem.Subject)' - $_"
return $null
}
}
function Copy-CalendarEvent {
param(
$SourceItem,
$TargetCalendar,
$Direction
)
try {
# Check if matching event already exists (BEFORE checking sync marker)
$existingEvent = Find-MatchingEvent -SourceItem $SourceItem -TargetCalendar $TargetCalendar
if ($existingEvent) {
# Mark both as synced if not already marked
$marked = $false
if ($SourceItem.Categories -notlike "*[SYNCED]*") {
if ($SourceItem.Categories) {
$SourceItem.Categories = "$($SourceItem.Categories),[SYNCED]"
} else {
$SourceItem.Categories = "[SYNCED]"
}
$SourceItem.Save()
$marked = $true
}
if ($existingEvent.Categories -notlike "*[SYNCED]*") {
if ($existingEvent.Categories) {
$existingEvent.Categories = "$($existingEvent.Categories),[SYNCED]"
} else {
$existingEvent.Categories = "[SYNCED]"
}
$existingEvent.Save()
$marked = $true
}
if ($marked) {
Write-Log "MARKED: Event '$($SourceItem.Subject)' already exists in both calendars - marked as synced"
}
return $false
}
# Check if already synced (only skip if marked AND no matching event found above)
if ($SourceItem.Categories -like "*[SYNCED]*") {
return $false
}
# Validate date/time before proceeding
if (-not $SourceItem.Start -or -not $SourceItem.End) {
Write-Log "SKIPPED: Event '$($SourceItem.Subject)' has invalid date/time"
return $false
}
# Create new appointment
$newItem = $TargetCalendar.Items.Add(1) # 1 = olAppointmentItem
# Copy basic properties
$newItem.Subject = $SourceItem.Subject
$newItem.Location = $SourceItem.Location
$newItem.Body = $SourceItem.Body
$newItem.BusyStatus = $SourceItem.BusyStatus
$newItem.AllDayEvent = $SourceItem.AllDayEvent
$newItem.Sensitivity = $SourceItem.Sensitivity
# Copy reminder settings safely
try {
$newItem.ReminderSet = $SourceItem.ReminderSet
if ($SourceItem.ReminderSet) {
$newItem.ReminderMinutesBeforeStart = $SourceItem.ReminderMinutesBeforeStart
}
} catch {
$newItem.ReminderSet = $false
}
# Handle recurring appointments with better error handling
if ($SourceItem.IsRecurring) {
try {
$recPattern = $SourceItem.GetRecurrencePattern()
# Set dates and times BEFORE setting recurrence pattern
$newItem.Start = $recPattern.PatternStartDate.Date.Add($recPattern.StartTime.TimeOfDay)
$newItem.End = $recPattern.PatternStartDate.Date.Add($recPattern.EndTime.TimeOfDay)
$newPattern = $newItem.GetRecurrencePattern()
# Copy recurrence settings
$newPattern.RecurrenceType = $recPattern.RecurrenceType
$newPattern.Interval = $recPattern.Interval
# Copy day of week for weekly patterns
if ($recPattern.RecurrenceType -eq 1) { # olRecursWeekly
$newPattern.DayOfWeekMask = $recPattern.DayOfWeekMask
}
# Copy day of month for monthly patterns
if ($recPattern.RecurrenceType -eq 2 -or $recPattern.RecurrenceType -eq 3) { # olRecursMonthly
try {
$newPattern.DayOfMonth = $recPattern.DayOfMonth
} catch {
# Some patterns don't use DayOfMonth
}
}
# Set pattern dates
$newPattern.PatternStartDate = $recPattern.PatternStartDate
$newPattern.StartTime = $recPattern.StartTime
$newPattern.EndTime = $recPattern.EndTime
# Set end date if pattern has one
if ($recPattern.NoEndDate -eq $false) {
try {
$newPattern.PatternEndDate = $recPattern.PatternEndDate
} catch {
# If setting end date fails, try with occurrences
if ($recPattern.Occurrences -gt 0) {
$newPattern.Occurrences = $recPattern.Occurrences
}
}
}
} catch {
Write-Log "WARNING: Could not copy recurrence pattern for '$($SourceItem.Subject)' - copying as single event. Error: $_"
# Fall back to single event
$newItem.Start = $SourceItem.Start
$newItem.End = $SourceItem.End
}
} else {
# Non-recurring event - simple copy
$newItem.Start = $SourceItem.Start
$newItem.End = $SourceItem.End
}
# Mark as synced
if ($SourceItem.Categories) {
$newItem.Categories = "$($SourceItem.Categories),[SYNCED]"
} else {
$newItem.Categories = "[SYNCED]"
}
$newItem.Save()
# Mark source item as synced too
if ($SourceItem.Categories) {
$SourceItem.Categories = "$($SourceItem.Categories),[SYNCED]"
} else {
$SourceItem.Categories = "[SYNCED]"
}
$SourceItem.Save()
Write-Log "SUCCESS: Copied event '$($SourceItem.Subject)' - $Direction"
return $true
} catch {
Write-Log "ERROR: Failed to copy event '$($SourceItem.Subject)' - $_"
return $false
}
}
function Sync-Calendars {
param(
$Calendar1,
$Calendar2
)
$startDate = (Get-Date).AddDays(-$DaysBack)
$endDate = (Get-Date).AddDays($DaysForward)
$filter = "[Start] >= '$($startDate.ToString("g"))' AND [Start] <= '$($endDate.ToString("g"))'"
$synced = 0
# Sync from Calendar 1 to Calendar 2
Write-Log "Syncing from $Email1 to $Email2..."
$items1 = $Calendar1.Items.Restrict($filter)
$items1.Sort("[Start]")
foreach ($item in $items1) {
if (Copy-CalendarEvent -SourceItem $item -TargetCalendar $Calendar2 -Direction "$Email1 -> $Email2") {
$synced++
}
}
# Sync from Calendar 2 to Calendar 1
Write-Log "Syncing from $Email2 to $Email1..."
$items2 = $Calendar2.Items.Restrict($filter)
$items2.Sort("[Start]")
foreach ($item in $items2) {
if (Copy-CalendarEvent -SourceItem $item -TargetCalendar $Calendar1 -Direction "$Email2 -> $Email1") {
$synced++
}
}
Write-Log "Sync completed. $synced events synchronized."
}
# ============================================
# MAIN EXECUTION
# ============================================
Write-Log "=== Calendar Sync Started ==="
try {
# Create Outlook COM object
$outlook = New-Object -ComObject Outlook.Application
# Get both calendars
$cal1 = Get-Calendar -EmailAddress $Email1
$cal2 = Get-Calendar -EmailAddress $Email2
if ($cal1 -and $cal2) {
Sync-Calendars -Calendar1 $cal1 -Calendar2 $cal2
} else {
Write-Log "ERROR: Could not access one or both calendars"
}
} catch {
Write-Log "ERROR: Script failed - $_"
} finally {
# Clean up
if ($outlook) {
[System.Runtime.Interopservices.Marshal]::ReleaseComObject($outlook) | Out-Null
}
[System.GC]::Collect()
[System.GC]::WaitForPendingFinalizers()
}
Write-Log "=== Calendar Sync Finished ==="
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment