2024-06-02 21:35:03 +02:00
using System ;
2025-12-29 23:13:32 +01:00
using System.Collections.Generic ;
2025-01-16 22:00:05 +01:00
using System.Linq ;
2024-06-02 21:35:03 +02:00
using System.Threading.Tasks ;
2025-01-06 02:15:21 +01:00
using Microsoft.Graph.Models ;
using Serilog ;
using Wino.Core.Domain.Entities.Calendar ;
using Wino.Core.Domain.Entities.Shared ;
using Wino.Core.Domain.Enums ;
2024-05-25 17:00:52 +02:00
using Wino.Core.Domain.Interfaces ;
2026-03-07 17:13:48 +01:00
using Wino.Core.Domain.Extensions ;
2025-01-06 02:15:21 +01:00
using Wino.Core.Extensions ;
2024-11-30 23:05:07 +01:00
using Wino.Services ;
2026-01-01 15:02:40 +01:00
using Reminder = Wino . Core . Domain . Entities . Calendar . Reminder ;
2024-05-25 17:00:52 +02:00
2025-02-16 11:54:23 +01:00
namespace Wino.Core.Integration.Processors ;
public class OutlookChangeProcessor ( IDatabaseService databaseService ,
IFolderService folderService ,
ICalendarService calendarService ,
IMailService mailService ,
IAccountService accountService ,
IMimeFileService mimeFileService ) : DefaultChangeProcessor ( databaseService , folderService , mailService , calendarService , accountService , mimeFileService )
, IOutlookChangeProcessor
2024-05-25 17:00:52 +02:00
{
2024-06-02 21:35:03 +02:00
2025-02-16 11:54:23 +01:00
public Task < string > ResetAccountDeltaTokenAsync ( Guid accountId )
2025-03-19 23:22:57 +01:00
= > AccountService . UpdateSyncIdentifierRawAsync ( accountId , string . Empty ) ;
2024-08-05 00:36:26 +02:00
2025-02-16 11:54:23 +01:00
public async Task < string > ResetFolderDeltaTokenAsync ( Guid folderId )
{
var folder = await FolderService . GetFolderAsync ( folderId ) ;
2024-08-05 00:36:26 +02:00
2025-02-16 11:54:23 +01:00
folder . DeltaToken = null ;
2024-08-05 00:36:26 +02:00
2025-02-16 11:54:23 +01:00
await FolderService . UpdateFolderAsync ( folder ) ;
2024-08-05 00:36:26 +02:00
2025-02-16 11:54:23 +01:00
return string . Empty ;
}
2024-08-05 00:36:26 +02:00
2025-02-16 11:54:23 +01:00
public Task UpdateFolderDeltaSynchronizationIdentifierAsync ( Guid folderId , string synchronizationIdentifier )
= > Connection . ExecuteAsync ( "UPDATE MailItemFolder SET DeltaToken = ? WHERE Id = ?" , synchronizationIdentifier , folderId ) ;
2025-01-06 02:15:21 +01:00
2025-02-16 11:54:23 +01:00
public async Task ManageCalendarEventAsync ( Event calendarEvent , AccountCalendar assignedCalendar , MailAccount organizerAccount )
{
2026-01-06 11:11:37 +01:00
// All event types are now handled: SingleInstance, SeriesMaster, Occurrence, and Exception.
// Occurrences from CalendarView are individual instances that are saved separately.
2025-01-06 02:15:21 +01:00
2025-02-16 11:54:23 +01:00
var savingItem = await CalendarService . GetCalendarItemAsync ( assignedCalendar . Id , calendarEvent . Id ) ;
2025-01-06 02:15:21 +01:00
2025-02-16 11:54:23 +01:00
Guid savingItemId = Guid . Empty ;
2025-12-29 23:13:32 +01:00
bool isNewItem = savingItem = = null ;
2025-01-06 02:15:21 +01:00
2025-02-16 11:54:23 +01:00
if ( savingItem ! = null )
savingItemId = savingItem . Id ;
else
{
savingItemId = Guid . NewGuid ( ) ;
savingItem = new CalendarItem ( ) { Id = savingItemId } ;
}
2025-01-06 02:15:21 +01:00
2026-03-28 01:44:12 +01:00
var eventStartLocalDateTime = OutlookIntegratorExtensions . GetLocalDateTimeFromDateTimeTimeZone ( calendarEvent . Start ) ;
var eventEndLocalDateTime = OutlookIntegratorExtensions . GetLocalDateTimeFromDateTimeTimeZone ( calendarEvent . End ) ;
2025-01-06 02:15:21 +01:00
2026-03-28 01:44:12 +01:00
var durationInSeconds = ( eventEndLocalDateTime - eventStartLocalDateTime ) . TotalSeconds ;
2025-01-06 02:15:21 +01:00
2026-03-28 01:44:12 +01:00
// Store the wall-clock values exactly as Outlook returned them for the event timezone.
// Timed events are converted for display later, while all-day events stay as floating dates.
2026-03-07 17:13:48 +01:00
savingItem . RemoteEventId = calendarEvent . Id . WithClientTrackingId ( calendarEvent . TransactionId . GetClientTrackingId ( ) ) ;
2026-03-28 01:44:12 +01:00
savingItem . StartDate = eventStartLocalDateTime ;
2025-02-16 11:54:23 +01:00
savingItem . DurationInSeconds = durationInSeconds ;
2025-01-06 02:15:21 +01:00
2025-12-26 20:46:48 +01:00
// Store the timezone information from the event
// This preserves the original timezone from Outlook, allowing proper reconstruction later
2025-12-29 14:10:09 +01:00
// If no timezone is provided, null will indicate UTC
2025-12-26 20:46:48 +01:00
savingItem . StartTimeZone = calendarEvent . Start ? . TimeZone ;
savingItem . EndTimeZone = calendarEvent . End ? . TimeZone ;
2025-02-16 11:54:23 +01:00
savingItem . Title = calendarEvent . Subject ;
savingItem . Description = calendarEvent . Body ? . Content ;
savingItem . Location = calendarEvent . Location ? . DisplayName ;
2025-01-06 02:15:21 +01:00
2026-01-06 11:11:37 +01:00
// Handle recurring event relationships for both Exception and Occurrence types
if ( ( calendarEvent . Type = = EventType . Exception | | calendarEvent . Type = = EventType . Occurrence )
& & ! string . IsNullOrEmpty ( calendarEvent . SeriesMasterId ) )
2025-02-16 11:54:23 +01:00
{
2026-01-06 11:11:37 +01:00
// This is a recurring event instance (either an exception or a regular occurrence).
// Link it to the parent series master.
2025-01-06 02:15:21 +01:00
2025-02-16 11:54:23 +01:00
var parentEvent = await CalendarService . GetCalendarItemAsync ( assignedCalendar . Id , calendarEvent . SeriesMasterId ) ;
2025-02-16 11:43:30 +01:00
2025-02-16 11:54:23 +01:00
if ( parentEvent ! = null )
2025-01-06 02:15:21 +01:00
{
2025-02-16 11:54:23 +01:00
savingItem . RecurringCalendarItemId = parentEvent . Id ;
2025-01-06 02:15:21 +01:00
}
else
{
2026-01-06 11:11:37 +01:00
// Parent not found yet - this can happen if occurrences sync before the series master.
// We still save the event but without the parent link for now.
Log . Warning ( $"Parent recurring event (SeriesMasterId: {calendarEvent.SeriesMasterId}) not found for event {calendarEvent.Id}. Event will be saved without parent link." ) ;
2025-01-06 02:15:21 +01:00
}
2025-02-16 11:54:23 +01:00
}
// Convert the recurrence pattern to string for parent recurring events.
2026-01-06 11:11:37 +01:00
// Note: We store this for reference but don't use it to calculate occurrences.
2025-02-16 11:54:23 +01:00
if ( calendarEvent . Type = = EventType . SeriesMaster & & calendarEvent . Recurrence ! = null )
{
savingItem . Recurrence = OutlookIntegratorExtensions . ToRfc5545RecurrenceString ( calendarEvent . Recurrence ) ;
}
2025-01-06 02:15:21 +01:00
2025-02-16 11:54:23 +01:00
savingItem . HtmlLink = calendarEvent . WebLink ;
savingItem . CalendarId = assignedCalendar . Id ;
savingItem . OrganizerEmail = calendarEvent . Organizer ? . EmailAddress ? . Address ;
savingItem . OrganizerDisplayName = calendarEvent . Organizer ? . EmailAddress ? . Name ;
savingItem . IsHidden = false ;
2025-01-16 22:00:05 +01:00
2025-12-26 20:46:48 +01:00
// Set timestamps
if ( calendarEvent . CreatedDateTime . HasValue )
savingItem . CreatedAt = calendarEvent . CreatedDateTime . Value ;
2025-12-29 23:13:32 +01:00
2025-12-26 20:46:48 +01:00
if ( calendarEvent . LastModifiedDateTime . HasValue )
savingItem . UpdatedAt = calendarEvent . LastModifiedDateTime . Value ;
// Set visibility
if ( calendarEvent . Sensitivity ! = null )
{
savingItem . Visibility = calendarEvent . Sensitivity . Value switch
{
Sensitivity . Normal = > CalendarItemVisibility . Public ,
Sensitivity . Personal = > CalendarItemVisibility . Private ,
Sensitivity . Private = > CalendarItemVisibility . Private ,
Sensitivity . Confidential = > CalendarItemVisibility . Confidential ,
_ = > CalendarItemVisibility . Public
} ;
}
else
{
savingItem . Visibility = CalendarItemVisibility . Public ;
}
2026-01-05 00:21:07 +01:00
// Set ShowAs status
if ( calendarEvent . ShowAs ! = null )
{
savingItem . ShowAs = calendarEvent . ShowAs . Value switch
{
Microsoft . Graph . Models . FreeBusyStatus . Free = > CalendarItemShowAs . Free ,
Microsoft . Graph . Models . FreeBusyStatus . Tentative = > CalendarItemShowAs . Tentative ,
Microsoft . Graph . Models . FreeBusyStatus . Busy = > CalendarItemShowAs . Busy ,
Microsoft . Graph . Models . FreeBusyStatus . Oof = > CalendarItemShowAs . OutOfOffice ,
Microsoft . Graph . Models . FreeBusyStatus . WorkingElsewhere = > CalendarItemShowAs . WorkingElsewhere ,
_ = > CalendarItemShowAs . Busy
} ;
}
else
{
savingItem . ShowAs = CalendarItemShowAs . Busy ;
}
2025-12-26 20:46:48 +01:00
// Set IsLocked based on whether the user is the organizer
// Read-only events are those where the current user is not the organizer
savingItem . IsLocked = calendarEvent . IsOrganizer . HasValue & & ! calendarEvent . IsOrganizer . Value ;
2025-02-16 11:54:23 +01:00
if ( calendarEvent . ResponseStatus ? . Response ! = null )
{
switch ( calendarEvent . ResponseStatus . Response . Value )
2025-01-16 22:00:05 +01:00
{
2025-02-16 11:54:23 +01:00
case ResponseType . None :
case ResponseType . NotResponded :
savingItem . Status = CalendarItemStatus . NotResponded ;
break ;
case ResponseType . TentativelyAccepted :
savingItem . Status = CalendarItemStatus . Tentative ;
break ;
case ResponseType . Accepted :
case ResponseType . Organizer :
2026-01-03 19:33:36 +01:00
savingItem . Status = CalendarItemStatus . Accepted ;
2025-02-16 11:54:23 +01:00
break ;
case ResponseType . Declined :
savingItem . Status = CalendarItemStatus . Cancelled ;
savingItem . IsHidden = true ;
break ;
default :
break ;
2025-01-16 22:00:05 +01:00
}
2025-01-06 02:15:21 +01:00
}
2025-02-16 11:54:23 +01:00
else
{
2026-01-03 19:33:36 +01:00
savingItem . Status = CalendarItemStatus . Accepted ;
2025-02-16 11:54:23 +01:00
}
2025-12-29 23:13:32 +01:00
// Prepare attendees list
List < CalendarEventAttendee > attendees = null ;
2025-02-16 11:54:23 +01:00
if ( calendarEvent . Attendees ! = null )
{
2026-01-01 10:07:56 +01:00
// Pass the organizer's email address to properly identify the organizer in the attendees list
string organizerEmail = calendarEvent . Organizer ? . EmailAddress ? . Address ;
attendees = calendarEvent . Attendees . Select ( a = > a . CreateAttendee ( savingItemId , organizerEmail ) ) . ToList ( ) ;
2025-12-29 23:13:32 +01:00
}
2026-01-01 15:02:40 +01:00
// Prepare reminders list from Outlook event
List < Reminder > reminders = null ;
if ( calendarEvent . IsReminderOn . GetValueOrDefault ( ) & & calendarEvent . ReminderMinutesBeforeStart . HasValue )
{
var reminderMinutes = calendarEvent . ReminderMinutesBeforeStart . Value ;
var reminderDurationInSeconds = reminderMinutes * 60 ; // Convert minutes to seconds
reminders = new List < Reminder >
{
new Reminder
{
Id = Guid . NewGuid ( ) ,
CalendarItemId = savingItemId ,
DurationInSeconds = reminderDurationInSeconds ,
ReminderType = CalendarItemReminderType . Popup
}
} ;
}
2026-01-03 23:59:37 +01:00
// Prepare attachments metadata from Outlook event
List < CalendarAttachment > attachments = null ;
if ( calendarEvent . HasAttachments . GetValueOrDefault ( ) & & calendarEvent . Attachments ! = null )
{
attachments = calendarEvent . Attachments
. Where ( a = > a ! = null & & ! string . IsNullOrEmpty ( a . Name ) )
. Select ( a = > new CalendarAttachment
{
Id = Guid . NewGuid ( ) ,
CalendarItemId = savingItemId ,
RemoteAttachmentId = a . Id ,
FileName = a . Name ,
Size = a . Size ? ? 0 ,
ContentType = a . ContentType ? ? "application/octet-stream" ,
IsDownloaded = false ,
LocalFilePath = null ,
LastModified = calendarEvent . LastModifiedDateTime ? ? DateTimeOffset . UtcNow
} )
. ToList ( ) ;
}
2026-01-06 11:11:37 +01:00
// Set assigned calendar for navigation properties to work.
savingItem . AssignedCalendar = assignedCalendar ;
2025-12-29 23:13:32 +01:00
// Use CalendarService to create or update the event
if ( isNewItem )
{
// New item - use CreateNewCalendarItemAsync
await CalendarService . CreateNewCalendarItemAsync ( savingItem , attendees ) . ConfigureAwait ( false ) ;
}
else
{
// Existing item - use UpdateCalendarItemAsync
await CalendarService . UpdateCalendarItemAsync ( savingItem , attendees ) . ConfigureAwait ( false ) ;
2025-02-16 11:54:23 +01:00
}
2026-01-01 15:02:40 +01:00
// Save reminders separately
await CalendarService . SaveRemindersAsync ( savingItemId , reminders ) . ConfigureAwait ( false ) ;
2026-01-03 23:59:37 +01:00
// Save attachments metadata separately
if ( attachments ! = null & & attachments . Count > 0 )
{
await CalendarService . InsertOrReplaceAttachmentsAsync ( attachments ) . ConfigureAwait ( false ) ;
}
2024-05-25 17:00:52 +02:00
}
}