Calendar attachments.
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Google.Apis.Calendar.v3.Data;
|
||||
using Serilog;
|
||||
@@ -250,10 +251,37 @@ public class GmailChangeProcessor : DefaultChangeProcessor, IGmailChangeProcesso
|
||||
}
|
||||
}
|
||||
|
||||
// Prepare attachments metadata from Gmail event
|
||||
List<CalendarAttachment> attachments = null;
|
||||
if (calendarEvent.Attachments != null && calendarEvent.Attachments.Count > 0)
|
||||
{
|
||||
attachments = calendarEvent.Attachments
|
||||
.Where(a => a != null && !string.IsNullOrEmpty(a.Title))
|
||||
.Select(a => new CalendarAttachment
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
CalendarItemId = calendarItem.Id,
|
||||
RemoteAttachmentId = a.FileId ?? a.FileUrl, // Gmail uses FileId or FileUrl
|
||||
FileName = a.Title,
|
||||
Size = 0, // Gmail API doesn't provide size in Event.Attachment
|
||||
ContentType = a.MimeType ?? "application/octet-stream",
|
||||
IsDownloaded = false,
|
||||
LocalFilePath = null,
|
||||
LastModified = DateTimeOffset.UtcNow
|
||||
})
|
||||
.ToList();
|
||||
}
|
||||
|
||||
await CalendarService.CreateNewCalendarItemAsync(calendarItem, attendees);
|
||||
|
||||
// Save reminders separately
|
||||
await CalendarService.SaveRemindersAsync(calendarItem.Id, reminders).ConfigureAwait(false);
|
||||
|
||||
// Save attachments metadata separately
|
||||
if (attachments != null && attachments.Count > 0)
|
||||
{
|
||||
await CalendarService.InsertOrReplaceAttachmentsAsync(attachments).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -315,6 +343,33 @@ public class GmailChangeProcessor : DefaultChangeProcessor, IGmailChangeProcesso
|
||||
|
||||
// Save reminders
|
||||
await CalendarService.SaveRemindersAsync(existingCalendarItem.Id, reminders).ConfigureAwait(false);
|
||||
|
||||
// Prepare attachments metadata from Gmail event for update
|
||||
List<CalendarAttachment> attachments = null;
|
||||
if (calendarEvent.Attachments != null && calendarEvent.Attachments.Count > 0)
|
||||
{
|
||||
attachments = calendarEvent.Attachments
|
||||
.Where(a => a != null && !string.IsNullOrEmpty(a.Title))
|
||||
.Select(a => new CalendarAttachment
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
CalendarItemId = existingCalendarItem.Id,
|
||||
RemoteAttachmentId = a.FileId ?? a.FileUrl,
|
||||
FileName = a.Title,
|
||||
Size = 0,
|
||||
ContentType = a.MimeType ?? "application/octet-stream",
|
||||
IsDownloaded = false,
|
||||
LocalFilePath = null,
|
||||
LastModified = DateTimeOffset.UtcNow
|
||||
})
|
||||
.ToList();
|
||||
}
|
||||
|
||||
// Save attachments metadata
|
||||
if (attachments != null && attachments.Count > 0)
|
||||
{
|
||||
await CalendarService.InsertOrReplaceAttachmentsAsync(attachments).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
// Upsert the event.
|
||||
|
||||
@@ -195,6 +195,27 @@ public class OutlookChangeProcessor(IDatabaseService databaseService,
|
||||
};
|
||||
}
|
||||
|
||||
// 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();
|
||||
}
|
||||
|
||||
// Use CalendarService to create or update the event
|
||||
if (isNewItem)
|
||||
{
|
||||
@@ -209,5 +230,11 @@ public class OutlookChangeProcessor(IDatabaseService databaseService,
|
||||
|
||||
// Save reminders separately
|
||||
await CalendarService.SaveRemindersAsync(savingItemId, reminders).ConfigureAwait(false);
|
||||
|
||||
// Save attachments metadata separately
|
||||
if (attachments != null && attachments.Count > 0)
|
||||
{
|
||||
await CalendarService.InsertOrReplaceAttachmentsAsync(attachments).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -412,6 +412,53 @@ public class SynchronizationManager : ISynchronizationManager
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Downloads a calendar attachment using the appropriate synchronizer.
|
||||
/// </summary>
|
||||
public async Task DownloadCalendarAttachmentAsync(
|
||||
Wino.Core.Domain.Entities.Calendar.CalendarItem calendarItem,
|
||||
Wino.Core.Domain.Entities.Calendar.CalendarAttachment attachment,
|
||||
string localFilePath,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
EnsureInitialized();
|
||||
|
||||
if (calendarItem == null)
|
||||
throw new ArgumentNullException(nameof(calendarItem));
|
||||
|
||||
if (attachment == null)
|
||||
throw new ArgumentNullException(nameof(attachment));
|
||||
|
||||
var accountId = calendarItem.AssignedCalendar?.AccountId ?? Guid.Empty;
|
||||
if (accountId == Guid.Empty)
|
||||
throw new InvalidOperationException("Calendar item does not have an assigned account.");
|
||||
|
||||
var synchronizer = await GetOrCreateSynchronizerAsync(accountId);
|
||||
|
||||
if (synchronizer == null)
|
||||
{
|
||||
_logger.Error("Could not find or create synchronizer for account {AccountId} to download calendar attachment", accountId);
|
||||
throw new InvalidOperationException("No synchronizer available for downloading calendar attachment.");
|
||||
}
|
||||
|
||||
_logger.Debug("Downloading calendar attachment {AttachmentId} for calendar item {CalendarItemId}",
|
||||
attachment.Id, calendarItem.Id);
|
||||
|
||||
try
|
||||
{
|
||||
await synchronizer.DownloadCalendarAttachmentAsync(
|
||||
calendarItem,
|
||||
attachment,
|
||||
localFilePath,
|
||||
cancellationToken);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, "Failed to download calendar attachment {AttachmentId}", attachment.Id);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new synchronizer for a newly added account.
|
||||
/// </summary>
|
||||
|
||||
@@ -1242,6 +1242,39 @@ public class GmailSynchronizer : WinoSynchronizer<IClientServiceRequest, Message
|
||||
await _gmailChangeProcessor.SaveMimeFileAsync(mailItem.FileId, mimeMessage, Account.Id).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public override async Task DownloadCalendarAttachmentAsync(
|
||||
Wino.Core.Domain.Entities.Calendar.CalendarItem calendarItem,
|
||||
Wino.Core.Domain.Entities.Calendar.CalendarAttachment attachment,
|
||||
string localFilePath,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Gmail calendar attachments are stored in Google Drive
|
||||
// RemoteAttachmentId contains either FileId or FileUrl
|
||||
// For simplicity, we'll try to download from the FileId/FileUrl
|
||||
|
||||
if (string.IsNullOrEmpty(attachment.RemoteAttachmentId))
|
||||
{
|
||||
_logger.Error("RemoteAttachmentId is empty for attachment {AttachmentId}", attachment.Id);
|
||||
throw new InvalidOperationException("RemoteAttachmentId is required to download Gmail calendar attachment.");
|
||||
}
|
||||
|
||||
// Gmail calendar attachments are links to Google Drive files
|
||||
// The attachment.RemoteAttachmentId is either a FileId or FileUrl
|
||||
// Since we can't directly download from Calendar API, this would require Drive API access
|
||||
// For now, throw NotSupportedException as Gmail attachments require additional Drive API setup
|
||||
|
||||
_logger.Warning("Gmail calendar attachment download requires Google Drive API access. FileId/URL: {RemoteId}", attachment.RemoteAttachmentId);
|
||||
throw new NotSupportedException("Gmail calendar attachments are stored in Google Drive and require additional API configuration to download.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, "Error downloading Gmail calendar attachment {AttachmentId}", attachment.Id);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public override List<IRequestBundle<IClientServiceRequest>> RenameFolder(RenameFolderRequest request)
|
||||
{
|
||||
var label = new Label()
|
||||
|
||||
@@ -243,6 +243,18 @@ public class ImapSynchronizer : WinoSynchronizer<ImapRequest, ImapMessageCreatio
|
||||
_clientPool.Release(client);
|
||||
}
|
||||
|
||||
public override Task DownloadCalendarAttachmentAsync(
|
||||
Wino.Core.Domain.Entities.Calendar.CalendarItem calendarItem,
|
||||
Wino.Core.Domain.Entities.Calendar.CalendarAttachment attachment,
|
||||
string localFilePath,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
// IMAP protocol doesn't support calendar operations natively
|
||||
// Calendar functionality would require CalDAV protocol
|
||||
_logger.Warning("IMAP protocol does not support calendar attachments. CalDAV would be required.");
|
||||
throw new NotSupportedException("IMAP does not support calendar attachments. Use Outlook or Gmail for calendar functionality.");
|
||||
}
|
||||
|
||||
public override List<IRequestBundle<ImapRequest>> RenameFolder(RenameFolderRequest request)
|
||||
{
|
||||
return CreateSingleTaskBundle(async (client, item) =>
|
||||
|
||||
@@ -1337,6 +1337,61 @@ public class OutlookSynchronizer : WinoSynchronizer<RequestInformation, Message,
|
||||
await _outlookChangeProcessor.SaveMimeFileAsync(mailItem.FileId, mimeMessage, Account.Id).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public override async Task DownloadCalendarAttachmentAsync(
|
||||
Wino.Core.Domain.Entities.Calendar.CalendarItem calendarItem,
|
||||
Wino.Core.Domain.Entities.Calendar.CalendarAttachment attachment,
|
||||
string localFilePath,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
var calendar = calendarItem.AssignedCalendar;
|
||||
|
||||
// First, get the attachment metadata to retrieve contentBytes for FileAttachment
|
||||
var attachmentItem = await _graphClient.Me
|
||||
.Calendars[calendar.RemoteCalendarId]
|
||||
.Events[calendarItem.RemoteEventId]
|
||||
.Attachments[attachment.RemoteAttachmentId]
|
||||
.GetAsync(cancellationToken: cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (attachmentItem == null)
|
||||
{
|
||||
_logger.Error("Failed to retrieve attachment {AttachmentId} for event {EventId}", attachment.RemoteAttachmentId, calendarItem.RemoteEventId);
|
||||
throw new InvalidOperationException("Failed to retrieve attachment.");
|
||||
}
|
||||
|
||||
byte[] contentBytes = null;
|
||||
|
||||
// Handle FileAttachment (has ContentBytes property)
|
||||
if (attachmentItem is FileAttachment fileAttachment && fileAttachment.ContentBytes != null)
|
||||
{
|
||||
contentBytes = fileAttachment.ContentBytes;
|
||||
}
|
||||
// Handle ItemAttachment (embedded items like emails)
|
||||
else if (attachmentItem is ItemAttachment)
|
||||
{
|
||||
_logger.Warning("ItemAttachment type not supported for download. AttachmentId: {AttachmentId}", attachment.RemoteAttachmentId);
|
||||
throw new NotSupportedException("ItemAttachment downloads are not currently supported.");
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.Error("Unknown attachment type or missing content for {AttachmentId}", attachment.RemoteAttachmentId);
|
||||
throw new InvalidOperationException("Attachment content is not available.");
|
||||
}
|
||||
|
||||
// Save to local file
|
||||
await System.IO.File.WriteAllBytesAsync(localFilePath, contentBytes, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
_logger.Information("Downloaded calendar attachment {FileName} to {LocalPath}", attachment.FileName, localFilePath);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, "Error downloading calendar attachment {AttachmentId}", attachment.Id);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public override List<IRequestBundle<RequestInformation>> RenameFolder(RenameFolderRequest request)
|
||||
{
|
||||
var requestBody = new MailFolder
|
||||
@@ -1693,9 +1748,12 @@ public class OutlookSynchronizer : WinoSynchronizer<RequestInformation, Message,
|
||||
{
|
||||
await _handleCalendarEventRetrievalSemaphore.WaitAsync();
|
||||
|
||||
// Check if the event has complete information
|
||||
// Sometimes delta sync returns events with only Id available
|
||||
Event fullEvent = await _graphClient.Me.Calendars[calendar.RemoteCalendarId].Events[item.Id].GetAsync(cancellationToken: cancellationToken).ConfigureAwait(false); ;
|
||||
Event fullEvent = await _graphClient.Me.Calendars[calendar.RemoteCalendarId].Events[item.Id]
|
||||
.GetAsync(requestConfiguration =>
|
||||
{
|
||||
// Expand attachments but only get metadata, not the full content
|
||||
requestConfiguration.QueryParameters.Expand = new[] { "attachments($select=id,name,contentType,size,isInline)" };
|
||||
}, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
await _outlookChangeProcessor.ManageCalendarEventAsync(fullEvent, calendar, Account).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
||||
@@ -526,6 +526,19 @@ public abstract class WinoSynchronizer<TBaseRequest, TMessageType, TCalendarEven
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
public virtual Task DownloadMissingMimeMessageAsync(MailCopy mailItem, ITransferProgress transferProgress = null, CancellationToken cancellationToken = default) => throw new NotSupportedException(string.Format(Translator.Exception_UnsupportedSynchronizerOperation, this.GetType()));
|
||||
|
||||
/// <summary>
|
||||
/// Downloads a calendar attachment from the provider.
|
||||
/// </summary>
|
||||
/// <param name="calendarItem">Calendar item the attachment belongs to.</param>
|
||||
/// <param name="attachment">Attachment metadata to download.</param>
|
||||
/// <param name="localFilePath">Local file path to save the attachment to.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
public virtual Task DownloadCalendarAttachmentAsync(
|
||||
Wino.Core.Domain.Entities.Calendar.CalendarItem calendarItem,
|
||||
Wino.Core.Domain.Entities.Calendar.CalendarAttachment attachment,
|
||||
string localFilePath,
|
||||
CancellationToken cancellationToken = default) => throw new NotSupportedException(string.Format(Translator.Exception_UnsupportedSynchronizerOperation, this.GetType()));
|
||||
|
||||
/// <summary>
|
||||
/// Performs an online search for the given query text in the given folders.
|
||||
/// Downloads the missing messages from the server.
|
||||
|
||||
Reference in New Issue
Block a user