file scoped namespaces (#565)
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -1,15 +1,14 @@
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
|
||||
namespace Wino.Services
|
||||
namespace Wino.Services;
|
||||
|
||||
public class ApplicationConfiguration : IApplicationConfiguration
|
||||
{
|
||||
public class ApplicationConfiguration : IApplicationConfiguration
|
||||
{
|
||||
public const string SharedFolderName = "WinoShared";
|
||||
public const string SharedFolderName = "WinoShared";
|
||||
|
||||
public string ApplicationDataFolderPath { get; set; }
|
||||
public string PublisherSharedFolderPath { get; set; }
|
||||
public string ApplicationTempFolderPath { get; set; }
|
||||
public string ApplicationDataFolderPath { get; set; }
|
||||
public string PublisherSharedFolderPath { get; set; }
|
||||
public string ApplicationTempFolderPath { get; set; }
|
||||
|
||||
public string ApplicationInsightsInstrumentationKey => "a5a07c2f-6e24-4055-bfc9-88e87eef873a";
|
||||
}
|
||||
public string ApplicationInsightsInstrumentationKey => "a5a07c2f-6e24-4055-bfc9-88e87eef873a";
|
||||
}
|
||||
|
||||
@@ -2,21 +2,20 @@
|
||||
using SQLite;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
|
||||
namespace Wino.Services
|
||||
namespace Wino.Services;
|
||||
|
||||
public class BaseDatabaseService
|
||||
{
|
||||
public class BaseDatabaseService
|
||||
protected IMessenger Messenger => WeakReferenceMessenger.Default;
|
||||
protected SQLiteAsyncConnection Connection => _databaseService.Connection;
|
||||
|
||||
private readonly IDatabaseService _databaseService;
|
||||
|
||||
public BaseDatabaseService(IDatabaseService databaseService)
|
||||
{
|
||||
protected IMessenger Messenger => WeakReferenceMessenger.Default;
|
||||
protected SQLiteAsyncConnection Connection => _databaseService.Connection;
|
||||
|
||||
private readonly IDatabaseService _databaseService;
|
||||
|
||||
public BaseDatabaseService(IDatabaseService databaseService)
|
||||
{
|
||||
_databaseService = databaseService;
|
||||
}
|
||||
|
||||
public void ReportUIChange<TMessage>(TMessage message) where TMessage : class, IUIMessage
|
||||
=> Messenger.Send(message);
|
||||
_databaseService = databaseService;
|
||||
}
|
||||
|
||||
public void ReportUIChange<TMessage>(TMessage message) where TMessage : class, IUIMessage
|
||||
=> Messenger.Send(message);
|
||||
}
|
||||
|
||||
@@ -16,291 +16,290 @@ using Wino.Core.Domain.Models.Calendar;
|
||||
using Wino.Messaging.Client.Calendar;
|
||||
using Wino.Services.Extensions;
|
||||
|
||||
namespace Wino.Services
|
||||
namespace Wino.Services;
|
||||
|
||||
public class CalendarService : BaseDatabaseService, ICalendarService
|
||||
{
|
||||
public class CalendarService : BaseDatabaseService, ICalendarService
|
||||
public CalendarService(IDatabaseService databaseService) : base(databaseService)
|
||||
{
|
||||
public CalendarService(IDatabaseService databaseService) : base(databaseService)
|
||||
}
|
||||
|
||||
public Task<List<AccountCalendar>> GetAccountCalendarsAsync(Guid accountId)
|
||||
=> Connection.Table<AccountCalendar>().Where(x => x.AccountId == accountId).OrderByDescending(a => a.IsPrimary).ToListAsync();
|
||||
|
||||
public async Task InsertAccountCalendarAsync(AccountCalendar accountCalendar)
|
||||
{
|
||||
await Connection.InsertAsync(accountCalendar);
|
||||
|
||||
WeakReferenceMessenger.Default.Send(new CalendarListAdded(accountCalendar));
|
||||
}
|
||||
|
||||
public async Task UpdateAccountCalendarAsync(AccountCalendar accountCalendar)
|
||||
{
|
||||
await Connection.UpdateAsync(accountCalendar);
|
||||
|
||||
WeakReferenceMessenger.Default.Send(new CalendarListUpdated(accountCalendar));
|
||||
}
|
||||
|
||||
public async Task DeleteAccountCalendarAsync(AccountCalendar accountCalendar)
|
||||
{
|
||||
var deleteCalendarItemsQuery = new Query()
|
||||
.From(nameof(CalendarItem))
|
||||
.Where(nameof(CalendarItem.CalendarId), accountCalendar.Id)
|
||||
.Where(nameof(AccountCalendar.AccountId), accountCalendar.AccountId);
|
||||
|
||||
var rawQuery = deleteCalendarItemsQuery.GetRawQuery();
|
||||
|
||||
await Connection.ExecuteAsync(rawQuery);
|
||||
await Connection.DeleteAsync(accountCalendar);
|
||||
|
||||
WeakReferenceMessenger.Default.Send(new CalendarListDeleted(accountCalendar));
|
||||
}
|
||||
|
||||
public async Task DeleteCalendarItemAsync(Guid calendarItemId)
|
||||
{
|
||||
var calendarItem = await Connection.GetAsync<CalendarItem>(calendarItemId);
|
||||
|
||||
if (calendarItem == null) return;
|
||||
|
||||
List<CalendarItem> eventsToRemove = new() { calendarItem };
|
||||
|
||||
// In case of parent event, delete all child events as well.
|
||||
if (!string.IsNullOrEmpty(calendarItem.Recurrence))
|
||||
{
|
||||
var recurringEvents = await Connection.Table<CalendarItem>().Where(a => a.RecurringCalendarItemId == calendarItemId).ToListAsync().ConfigureAwait(false);
|
||||
|
||||
eventsToRemove.AddRange(recurringEvents);
|
||||
}
|
||||
|
||||
public Task<List<AccountCalendar>> GetAccountCalendarsAsync(Guid accountId)
|
||||
=> Connection.Table<AccountCalendar>().Where(x => x.AccountId == accountId).OrderByDescending(a => a.IsPrimary).ToListAsync();
|
||||
|
||||
public async Task InsertAccountCalendarAsync(AccountCalendar accountCalendar)
|
||||
foreach (var @event in eventsToRemove)
|
||||
{
|
||||
await Connection.InsertAsync(accountCalendar);
|
||||
await Connection.Table<CalendarItem>().DeleteAsync(x => x.Id == @event.Id).ConfigureAwait(false);
|
||||
await Connection.Table<CalendarEventAttendee>().DeleteAsync(a => a.CalendarItemId == @event.Id).ConfigureAwait(false);
|
||||
|
||||
WeakReferenceMessenger.Default.Send(new CalendarListAdded(accountCalendar));
|
||||
WeakReferenceMessenger.Default.Send(new CalendarItemDeleted(@event));
|
||||
}
|
||||
}
|
||||
|
||||
public async Task UpdateAccountCalendarAsync(AccountCalendar accountCalendar)
|
||||
{
|
||||
await Connection.UpdateAsync(accountCalendar);
|
||||
public async Task CreateNewCalendarItemAsync(CalendarItem calendarItem, List<CalendarEventAttendee> attendees)
|
||||
{
|
||||
await Connection.RunInTransactionAsync((conn) =>
|
||||
{
|
||||
conn.Insert(calendarItem);
|
||||
|
||||
WeakReferenceMessenger.Default.Send(new CalendarListUpdated(accountCalendar));
|
||||
}
|
||||
|
||||
public async Task DeleteAccountCalendarAsync(AccountCalendar accountCalendar)
|
||||
{
|
||||
var deleteCalendarItemsQuery = new Query()
|
||||
.From(nameof(CalendarItem))
|
||||
.Where(nameof(CalendarItem.CalendarId), accountCalendar.Id)
|
||||
.Where(nameof(AccountCalendar.AccountId), accountCalendar.AccountId);
|
||||
|
||||
var rawQuery = deleteCalendarItemsQuery.GetRawQuery();
|
||||
|
||||
await Connection.ExecuteAsync(rawQuery);
|
||||
await Connection.DeleteAsync(accountCalendar);
|
||||
|
||||
WeakReferenceMessenger.Default.Send(new CalendarListDeleted(accountCalendar));
|
||||
}
|
||||
|
||||
public async Task DeleteCalendarItemAsync(Guid calendarItemId)
|
||||
{
|
||||
var calendarItem = await Connection.GetAsync<CalendarItem>(calendarItemId);
|
||||
|
||||
if (calendarItem == null) return;
|
||||
|
||||
List<CalendarItem> eventsToRemove = new() { calendarItem };
|
||||
|
||||
// In case of parent event, delete all child events as well.
|
||||
if (!string.IsNullOrEmpty(calendarItem.Recurrence))
|
||||
{
|
||||
var recurringEvents = await Connection.Table<CalendarItem>().Where(a => a.RecurringCalendarItemId == calendarItemId).ToListAsync().ConfigureAwait(false);
|
||||
|
||||
eventsToRemove.AddRange(recurringEvents);
|
||||
}
|
||||
|
||||
foreach (var @event in eventsToRemove)
|
||||
{
|
||||
await Connection.Table<CalendarItem>().DeleteAsync(x => x.Id == @event.Id).ConfigureAwait(false);
|
||||
await Connection.Table<CalendarEventAttendee>().DeleteAsync(a => a.CalendarItemId == @event.Id).ConfigureAwait(false);
|
||||
|
||||
WeakReferenceMessenger.Default.Send(new CalendarItemDeleted(@event));
|
||||
}
|
||||
}
|
||||
|
||||
public async Task CreateNewCalendarItemAsync(CalendarItem calendarItem, List<CalendarEventAttendee> attendees)
|
||||
{
|
||||
await Connection.RunInTransactionAsync((conn) =>
|
||||
if (attendees != null)
|
||||
{
|
||||
conn.Insert(calendarItem);
|
||||
conn.InsertAll(attendees);
|
||||
}
|
||||
});
|
||||
|
||||
if (attendees != null)
|
||||
{
|
||||
conn.InsertAll(attendees);
|
||||
}
|
||||
});
|
||||
WeakReferenceMessenger.Default.Send(new CalendarItemAdded(calendarItem));
|
||||
}
|
||||
|
||||
WeakReferenceMessenger.Default.Send(new CalendarItemAdded(calendarItem));
|
||||
}
|
||||
public async Task<List<CalendarItem>> GetCalendarEventsAsync(IAccountCalendar calendar, DayRangeRenderModel dayRangeRenderModel)
|
||||
{
|
||||
// TODO: We might need to implement caching here.
|
||||
// I don't know how much of the events we'll have in total, but this logic scans all events every time for given calendar.
|
||||
|
||||
public async Task<List<CalendarItem>> GetCalendarEventsAsync(IAccountCalendar calendar, DayRangeRenderModel dayRangeRenderModel)
|
||||
var accountEvents = await Connection.Table<CalendarItem>()
|
||||
.Where(x => x.CalendarId == calendar.Id && !x.IsHidden).ToListAsync();
|
||||
|
||||
var result = new List<CalendarItem>();
|
||||
|
||||
foreach (var ev in accountEvents)
|
||||
{
|
||||
// TODO: We might need to implement caching here.
|
||||
// I don't know how much of the events we'll have in total, but this logic scans all events every time for given calendar.
|
||||
ev.AssignedCalendar = calendar;
|
||||
|
||||
var accountEvents = await Connection.Table<CalendarItem>()
|
||||
.Where(x => x.CalendarId == calendar.Id && !x.IsHidden).ToListAsync();
|
||||
|
||||
var result = new List<CalendarItem>();
|
||||
|
||||
foreach (var ev in accountEvents)
|
||||
// Parse recurrence rules
|
||||
var calendarEvent = new CalendarEvent
|
||||
{
|
||||
ev.AssignedCalendar = calendar;
|
||||
Start = new CalDateTime(ev.StartDate),
|
||||
End = new CalDateTime(ev.EndDate),
|
||||
};
|
||||
|
||||
// Parse recurrence rules
|
||||
var calendarEvent = new CalendarEvent
|
||||
{
|
||||
Start = new CalDateTime(ev.StartDate),
|
||||
End = new CalDateTime(ev.EndDate),
|
||||
};
|
||||
|
||||
if (string.IsNullOrEmpty(ev.Recurrence))
|
||||
{
|
||||
// No recurrence, only check if we fall into the given period.
|
||||
|
||||
if (ev.Period.OverlapsWith(dayRangeRenderModel.Period))
|
||||
{
|
||||
result.Add(ev);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// This event has recurrences.
|
||||
// Wino stores exceptional recurrent events as a separate calendar item, without the recurrence rule.
|
||||
// Because each instance of recurrent event can have different attendees, properties etc.
|
||||
// Even though the event is recurrent, each updated instance is a separate calendar item.
|
||||
// Calculate the all recurrences, and remove the exceptional instances like hidden ones.
|
||||
|
||||
var recurrenceLines = Regex.Split(ev.Recurrence, Constants.CalendarEventRecurrenceRuleSeperator);
|
||||
|
||||
foreach (var line in recurrenceLines)
|
||||
{
|
||||
calendarEvent.RecurrenceRules.Add(new RecurrencePattern(line));
|
||||
}
|
||||
|
||||
// Calculate occurrences in the range.
|
||||
var occurrences = calendarEvent.GetOccurrences(dayRangeRenderModel.Period.Start, dayRangeRenderModel.Period.End);
|
||||
|
||||
// Get all recurrent exceptional calendar events.
|
||||
var exceptionalRecurrences = await Connection.Table<CalendarItem>()
|
||||
.Where(a => a.RecurringCalendarItemId == ev.Id)
|
||||
.ToListAsync()
|
||||
.ConfigureAwait(false);
|
||||
|
||||
foreach (var occurrence in occurrences)
|
||||
{
|
||||
var exactInstanceCheck = exceptionalRecurrences.FirstOrDefault(a =>
|
||||
a.Period.OverlapsWith(dayRangeRenderModel.Period));
|
||||
|
||||
if (exactInstanceCheck == null)
|
||||
{
|
||||
// There is no exception for the period.
|
||||
// Change the instance StartDate and Duration.
|
||||
|
||||
var recurrence = ev.CreateRecurrence(occurrence.Period.StartTime.Value, occurrence.Period.Duration.TotalSeconds);
|
||||
|
||||
result.Add(recurrence);
|
||||
}
|
||||
else
|
||||
{
|
||||
// There is a single instance of this recurrent event.
|
||||
// It will be added as single item if it's not hidden.
|
||||
// We don't need to do anything here.
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public Task<AccountCalendar> GetAccountCalendarAsync(Guid accountCalendarId)
|
||||
=> Connection.GetAsync<AccountCalendar>(accountCalendarId);
|
||||
|
||||
public Task<CalendarItem> GetCalendarItemAsync(Guid id)
|
||||
{
|
||||
var query = new Query()
|
||||
.From(nameof(CalendarItem))
|
||||
.Where(nameof(CalendarItem.Id), id);
|
||||
|
||||
var rawQuery = query.GetRawQuery();
|
||||
return Connection.FindWithQueryAsync<CalendarItem>(rawQuery);
|
||||
}
|
||||
|
||||
public async Task<CalendarItem> GetCalendarItemAsync(Guid accountCalendarId, string remoteEventId)
|
||||
{
|
||||
var query = new Query()
|
||||
.From(nameof(CalendarItem))
|
||||
.Where(nameof(CalendarItem.CalendarId), accountCalendarId)
|
||||
.Where(nameof(CalendarItem.RemoteEventId), remoteEventId);
|
||||
|
||||
var rawQuery = query.GetRawQuery();
|
||||
|
||||
var calendarItem = await Connection.FindWithQueryAsync<CalendarItem>(rawQuery);
|
||||
|
||||
// Load assigned calendar.
|
||||
if (calendarItem != null)
|
||||
if (string.IsNullOrEmpty(ev.Recurrence))
|
||||
{
|
||||
calendarItem.AssignedCalendar = await Connection.GetAsync<AccountCalendar>(calendarItem.CalendarId);
|
||||
}
|
||||
// No recurrence, only check if we fall into the given period.
|
||||
|
||||
return calendarItem;
|
||||
}
|
||||
|
||||
public Task UpdateCalendarDeltaSynchronizationToken(Guid calendarId, string deltaToken)
|
||||
{
|
||||
var query = new Query()
|
||||
.From(nameof(AccountCalendar))
|
||||
.Where(nameof(AccountCalendar.Id), calendarId)
|
||||
.AsUpdate(new { SynchronizationDeltaToken = deltaToken });
|
||||
|
||||
return Connection.ExecuteAsync(query.GetRawQuery());
|
||||
}
|
||||
|
||||
public Task<List<CalendarEventAttendee>> GetAttendeesAsync(Guid calendarEventTrackingId)
|
||||
=> Connection.Table<CalendarEventAttendee>().Where(x => x.CalendarItemId == calendarEventTrackingId).ToListAsync();
|
||||
|
||||
public async Task<List<CalendarEventAttendee>> ManageEventAttendeesAsync(Guid calendarItemId, List<CalendarEventAttendee> allAttendees)
|
||||
{
|
||||
await Connection.RunInTransactionAsync((connection) =>
|
||||
{
|
||||
// Clear all attendees.
|
||||
var query = new Query()
|
||||
.From(nameof(CalendarEventAttendee))
|
||||
.Where(nameof(CalendarEventAttendee.CalendarItemId), calendarItemId)
|
||||
.AsDelete();
|
||||
|
||||
connection.Execute(query.GetRawQuery());
|
||||
|
||||
// Insert new attendees.
|
||||
connection.InsertAll(allAttendees);
|
||||
});
|
||||
|
||||
return await Connection.Table<CalendarEventAttendee>().Where(a => a.CalendarItemId == calendarItemId).ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<CalendarItem> GetCalendarItemTargetAsync(CalendarItemTarget targetDetails)
|
||||
{
|
||||
var eventId = targetDetails.Item.Id;
|
||||
|
||||
// Get the event by Id first.
|
||||
var item = await GetCalendarItemAsync(eventId).ConfigureAwait(false);
|
||||
|
||||
bool isRecurringChild = targetDetails.Item.IsRecurringChild;
|
||||
bool isRecurringParent = targetDetails.Item.IsRecurringParent;
|
||||
|
||||
if (targetDetails.TargetType == CalendarEventTargetType.Single)
|
||||
{
|
||||
if (isRecurringChild)
|
||||
if (ev.Period.OverlapsWith(dayRangeRenderModel.Period))
|
||||
{
|
||||
if (item == null)
|
||||
{
|
||||
// This is an occurrence of a recurring event.
|
||||
// They don't exist in db.
|
||||
|
||||
return targetDetails.Item;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Single exception occurrence of recurring event.
|
||||
// Return the item.
|
||||
|
||||
return item;
|
||||
}
|
||||
}
|
||||
else if (isRecurringParent)
|
||||
{
|
||||
// Parent recurring events are never listed.
|
||||
Debugger.Break();
|
||||
return null;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Single event.
|
||||
return item;
|
||||
result.Add(ev);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Series.
|
||||
// This event has recurrences.
|
||||
// Wino stores exceptional recurrent events as a separate calendar item, without the recurrence rule.
|
||||
// Because each instance of recurrent event can have different attendees, properties etc.
|
||||
// Even though the event is recurrent, each updated instance is a separate calendar item.
|
||||
// Calculate the all recurrences, and remove the exceptional instances like hidden ones.
|
||||
|
||||
if (isRecurringChild)
|
||||
var recurrenceLines = Regex.Split(ev.Recurrence, Constants.CalendarEventRecurrenceRuleSeperator);
|
||||
|
||||
foreach (var line in recurrenceLines)
|
||||
{
|
||||
// Return the parent.
|
||||
return await GetCalendarItemAsync(targetDetails.Item.RecurringCalendarItemId.Value).ConfigureAwait(false);
|
||||
calendarEvent.RecurrenceRules.Add(new RecurrencePattern(line));
|
||||
}
|
||||
|
||||
// Calculate occurrences in the range.
|
||||
var occurrences = calendarEvent.GetOccurrences(dayRangeRenderModel.Period.Start, dayRangeRenderModel.Period.End);
|
||||
|
||||
// Get all recurrent exceptional calendar events.
|
||||
var exceptionalRecurrences = await Connection.Table<CalendarItem>()
|
||||
.Where(a => a.RecurringCalendarItemId == ev.Id)
|
||||
.ToListAsync()
|
||||
.ConfigureAwait(false);
|
||||
|
||||
foreach (var occurrence in occurrences)
|
||||
{
|
||||
var exactInstanceCheck = exceptionalRecurrences.FirstOrDefault(a =>
|
||||
a.Period.OverlapsWith(dayRangeRenderModel.Period));
|
||||
|
||||
if (exactInstanceCheck == null)
|
||||
{
|
||||
// There is no exception for the period.
|
||||
// Change the instance StartDate and Duration.
|
||||
|
||||
var recurrence = ev.CreateRecurrence(occurrence.Period.StartTime.Value, occurrence.Period.Duration.TotalSeconds);
|
||||
|
||||
result.Add(recurrence);
|
||||
}
|
||||
else
|
||||
{
|
||||
// There is a single instance of this recurrent event.
|
||||
// It will be added as single item if it's not hidden.
|
||||
// We don't need to do anything here.
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public Task<AccountCalendar> GetAccountCalendarAsync(Guid accountCalendarId)
|
||||
=> Connection.GetAsync<AccountCalendar>(accountCalendarId);
|
||||
|
||||
public Task<CalendarItem> GetCalendarItemAsync(Guid id)
|
||||
{
|
||||
var query = new Query()
|
||||
.From(nameof(CalendarItem))
|
||||
.Where(nameof(CalendarItem.Id), id);
|
||||
|
||||
var rawQuery = query.GetRawQuery();
|
||||
return Connection.FindWithQueryAsync<CalendarItem>(rawQuery);
|
||||
}
|
||||
|
||||
public async Task<CalendarItem> GetCalendarItemAsync(Guid accountCalendarId, string remoteEventId)
|
||||
{
|
||||
var query = new Query()
|
||||
.From(nameof(CalendarItem))
|
||||
.Where(nameof(CalendarItem.CalendarId), accountCalendarId)
|
||||
.Where(nameof(CalendarItem.RemoteEventId), remoteEventId);
|
||||
|
||||
var rawQuery = query.GetRawQuery();
|
||||
|
||||
var calendarItem = await Connection.FindWithQueryAsync<CalendarItem>(rawQuery);
|
||||
|
||||
// Load assigned calendar.
|
||||
if (calendarItem != null)
|
||||
{
|
||||
calendarItem.AssignedCalendar = await Connection.GetAsync<AccountCalendar>(calendarItem.CalendarId);
|
||||
}
|
||||
|
||||
return calendarItem;
|
||||
}
|
||||
|
||||
public Task UpdateCalendarDeltaSynchronizationToken(Guid calendarId, string deltaToken)
|
||||
{
|
||||
var query = new Query()
|
||||
.From(nameof(AccountCalendar))
|
||||
.Where(nameof(AccountCalendar.Id), calendarId)
|
||||
.AsUpdate(new { SynchronizationDeltaToken = deltaToken });
|
||||
|
||||
return Connection.ExecuteAsync(query.GetRawQuery());
|
||||
}
|
||||
|
||||
public Task<List<CalendarEventAttendee>> GetAttendeesAsync(Guid calendarEventTrackingId)
|
||||
=> Connection.Table<CalendarEventAttendee>().Where(x => x.CalendarItemId == calendarEventTrackingId).ToListAsync();
|
||||
|
||||
public async Task<List<CalendarEventAttendee>> ManageEventAttendeesAsync(Guid calendarItemId, List<CalendarEventAttendee> allAttendees)
|
||||
{
|
||||
await Connection.RunInTransactionAsync((connection) =>
|
||||
{
|
||||
// Clear all attendees.
|
||||
var query = new Query()
|
||||
.From(nameof(CalendarEventAttendee))
|
||||
.Where(nameof(CalendarEventAttendee.CalendarItemId), calendarItemId)
|
||||
.AsDelete();
|
||||
|
||||
connection.Execute(query.GetRawQuery());
|
||||
|
||||
// Insert new attendees.
|
||||
connection.InsertAll(allAttendees);
|
||||
});
|
||||
|
||||
return await Connection.Table<CalendarEventAttendee>().Where(a => a.CalendarItemId == calendarItemId).ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<CalendarItem> GetCalendarItemTargetAsync(CalendarItemTarget targetDetails)
|
||||
{
|
||||
var eventId = targetDetails.Item.Id;
|
||||
|
||||
// Get the event by Id first.
|
||||
var item = await GetCalendarItemAsync(eventId).ConfigureAwait(false);
|
||||
|
||||
bool isRecurringChild = targetDetails.Item.IsRecurringChild;
|
||||
bool isRecurringParent = targetDetails.Item.IsRecurringParent;
|
||||
|
||||
if (targetDetails.TargetType == CalendarEventTargetType.Single)
|
||||
{
|
||||
if (isRecurringChild)
|
||||
{
|
||||
if (item == null)
|
||||
{
|
||||
// This is an occurrence of a recurring event.
|
||||
// They don't exist in db.
|
||||
|
||||
return targetDetails.Item;
|
||||
}
|
||||
else if (isRecurringParent)
|
||||
return item;
|
||||
else
|
||||
{
|
||||
// NA. Single events don't have series.
|
||||
Debugger.Break();
|
||||
return null;
|
||||
// Single exception occurrence of recurring event.
|
||||
// Return the item.
|
||||
|
||||
return item;
|
||||
}
|
||||
}
|
||||
else if (isRecurringParent)
|
||||
{
|
||||
// Parent recurring events are never listed.
|
||||
Debugger.Break();
|
||||
return null;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Single event.
|
||||
return item;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Series.
|
||||
|
||||
if (isRecurringChild)
|
||||
{
|
||||
// Return the parent.
|
||||
return await GetCalendarItemAsync(targetDetails.Item.RecurringCalendarItemId.Value).ConfigureAwait(false);
|
||||
}
|
||||
else if (isRecurringParent)
|
||||
return item;
|
||||
else
|
||||
{
|
||||
// NA. Single events don't have series.
|
||||
Debugger.Break();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,58 +7,57 @@ using Wino.Core.Domain.Entities.Shared;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Services.Extensions;
|
||||
|
||||
namespace Wino.Services
|
||||
namespace Wino.Services;
|
||||
|
||||
public class ContactService : BaseDatabaseService, IContactService
|
||||
{
|
||||
public class ContactService : BaseDatabaseService, IContactService
|
||||
public ContactService(IDatabaseService databaseService) : base(databaseService) { }
|
||||
|
||||
public async Task<AccountContact> CreateNewContactAsync(string address, string displayName)
|
||||
{
|
||||
public ContactService(IDatabaseService databaseService) : base(databaseService) { }
|
||||
var contact = new AccountContact() { Address = address, Name = displayName };
|
||||
|
||||
public async Task<AccountContact> CreateNewContactAsync(string address, string displayName)
|
||||
await Connection.InsertAsync(contact).ConfigureAwait(false);
|
||||
|
||||
return contact;
|
||||
}
|
||||
|
||||
public Task<List<AccountContact>> GetAddressInformationAsync(string queryText)
|
||||
{
|
||||
if (queryText == null || queryText.Length < 2)
|
||||
return Task.FromResult<List<AccountContact>>(null);
|
||||
|
||||
var query = new Query(nameof(AccountContact));
|
||||
query.WhereContains("Address", queryText);
|
||||
query.OrWhereContains("Name", queryText);
|
||||
|
||||
var rawLikeQuery = query.GetRawQuery();
|
||||
|
||||
return Connection.QueryAsync<AccountContact>(rawLikeQuery);
|
||||
}
|
||||
|
||||
public Task<AccountContact> GetAddressInformationByAddressAsync(string address)
|
||||
=> Connection.Table<AccountContact>().Where(a => a.Address == address).FirstOrDefaultAsync();
|
||||
|
||||
public async Task SaveAddressInformationAsync(MimeMessage message)
|
||||
{
|
||||
var recipients = message
|
||||
.GetRecipients(true)
|
||||
.Where(a => !string.IsNullOrEmpty(a.Name) && !string.IsNullOrEmpty(a.Address));
|
||||
|
||||
var addressInformations = recipients.Select(a => new AccountContact() { Name = a.Name, Address = a.Address });
|
||||
|
||||
foreach (var info in addressInformations)
|
||||
{
|
||||
var contact = new AccountContact() { Address = address, Name = displayName };
|
||||
var currentContact = await GetAddressInformationByAddressAsync(info.Address).ConfigureAwait(false);
|
||||
|
||||
await Connection.InsertAsync(contact).ConfigureAwait(false);
|
||||
|
||||
return contact;
|
||||
}
|
||||
|
||||
public Task<List<AccountContact>> GetAddressInformationAsync(string queryText)
|
||||
{
|
||||
if (queryText == null || queryText.Length < 2)
|
||||
return Task.FromResult<List<AccountContact>>(null);
|
||||
|
||||
var query = new Query(nameof(AccountContact));
|
||||
query.WhereContains("Address", queryText);
|
||||
query.OrWhereContains("Name", queryText);
|
||||
|
||||
var rawLikeQuery = query.GetRawQuery();
|
||||
|
||||
return Connection.QueryAsync<AccountContact>(rawLikeQuery);
|
||||
}
|
||||
|
||||
public Task<AccountContact> GetAddressInformationByAddressAsync(string address)
|
||||
=> Connection.Table<AccountContact>().Where(a => a.Address == address).FirstOrDefaultAsync();
|
||||
|
||||
public async Task SaveAddressInformationAsync(MimeMessage message)
|
||||
{
|
||||
var recipients = message
|
||||
.GetRecipients(true)
|
||||
.Where(a => !string.IsNullOrEmpty(a.Name) && !string.IsNullOrEmpty(a.Address));
|
||||
|
||||
var addressInformations = recipients.Select(a => new AccountContact() { Name = a.Name, Address = a.Address });
|
||||
|
||||
foreach (var info in addressInformations)
|
||||
if (currentContact == null)
|
||||
{
|
||||
var currentContact = await GetAddressInformationByAddressAsync(info.Address).ConfigureAwait(false);
|
||||
|
||||
if (currentContact == null)
|
||||
{
|
||||
await Connection.InsertAsync(info).ConfigureAwait(false);
|
||||
}
|
||||
else if (!currentContact.IsRootContact) // Don't update root contacts. They belong to accounts.
|
||||
{
|
||||
await Connection.InsertOrReplaceAsync(info).ConfigureAwait(false);
|
||||
}
|
||||
await Connection.InsertAsync(info).ConfigureAwait(false);
|
||||
}
|
||||
else if (!currentContact.IsRootContact) // Don't update root contacts. They belong to accounts.
|
||||
{
|
||||
await Connection.InsertOrReplaceAsync(info).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,176 +6,175 @@ using Wino.Core.Domain.Models.Folders;
|
||||
using Wino.Core.Domain.Models.MailItem;
|
||||
using Wino.Core.Domain.Models.Menus;
|
||||
|
||||
namespace Wino.Services
|
||||
namespace Wino.Services;
|
||||
|
||||
public class ContextMenuItemService : IContextMenuItemService
|
||||
{
|
||||
public class ContextMenuItemService : IContextMenuItemService
|
||||
public virtual IEnumerable<FolderOperationMenuItem> GetFolderContextMenuActions(IBaseFolderMenuItem folderInformation)
|
||||
{
|
||||
public virtual IEnumerable<FolderOperationMenuItem> GetFolderContextMenuActions(IBaseFolderMenuItem folderInformation)
|
||||
var list = new List<FolderOperationMenuItem>();
|
||||
|
||||
if (folderInformation.IsSticky)
|
||||
list.Add(FolderOperationMenuItem.Create(FolderOperation.Unpin));
|
||||
else
|
||||
list.Add(FolderOperationMenuItem.Create(FolderOperation.Pin));
|
||||
|
||||
list.Add(FolderOperationMenuItem.Create(FolderOperation.Seperator));
|
||||
|
||||
// Following 4 items are disabled for system folders.
|
||||
|
||||
list.Add(FolderOperationMenuItem.Create(FolderOperation.Rename, !folderInformation.IsSystemFolder));
|
||||
list.Add(FolderOperationMenuItem.Create(FolderOperation.Delete, !folderInformation.IsSystemFolder));
|
||||
list.Add(FolderOperationMenuItem.Create(FolderOperation.CreateSubFolder, !folderInformation.IsSystemFolder));
|
||||
|
||||
list.Add(FolderOperationMenuItem.Create(FolderOperation.Seperator));
|
||||
|
||||
list.Add(FolderOperationMenuItem.Create(FolderOperation.Empty));
|
||||
|
||||
list.Add(FolderOperationMenuItem.Create(FolderOperation.MarkAllAsRead));
|
||||
|
||||
return list;
|
||||
}
|
||||
public virtual IEnumerable<MailOperationMenuItem> GetMailItemContextMenuActions(IEnumerable<IMailItem> selectedMailItems)
|
||||
{
|
||||
if (selectedMailItems == null)
|
||||
return default;
|
||||
|
||||
var operationList = new List<MailOperationMenuItem>();
|
||||
|
||||
// Disable archive button for Archive folder itself.
|
||||
|
||||
bool isArchiveFolder = selectedMailItems.All(a => a.AssignedFolder.SpecialFolderType == SpecialFolderType.Archive);
|
||||
bool isDraftOrSent = selectedMailItems.All(a => a.AssignedFolder.SpecialFolderType == SpecialFolderType.Draft || a.AssignedFolder.SpecialFolderType == SpecialFolderType.Sent);
|
||||
bool isJunkFolder = selectedMailItems.All(a => a.AssignedFolder.SpecialFolderType == SpecialFolderType.Junk);
|
||||
|
||||
bool isSingleItem = selectedMailItems.Count() == 1;
|
||||
|
||||
IMailItem singleItem = selectedMailItems.FirstOrDefault();
|
||||
|
||||
// Archive button.
|
||||
|
||||
if (isArchiveFolder)
|
||||
operationList.Add(MailOperationMenuItem.Create(MailOperation.UnArchive));
|
||||
else
|
||||
operationList.Add(MailOperationMenuItem.Create(MailOperation.Archive));
|
||||
|
||||
// Delete button.
|
||||
operationList.Add(MailOperationMenuItem.Create(MailOperation.SoftDelete));
|
||||
|
||||
// Move button.
|
||||
operationList.Add(MailOperationMenuItem.Create(MailOperation.Move, !isDraftOrSent));
|
||||
|
||||
// Independent flag, read etc.
|
||||
if (isSingleItem)
|
||||
{
|
||||
var list = new List<FolderOperationMenuItem>();
|
||||
|
||||
if (folderInformation.IsSticky)
|
||||
list.Add(FolderOperationMenuItem.Create(FolderOperation.Unpin));
|
||||
if (singleItem.IsFlagged)
|
||||
operationList.Add(MailOperationMenuItem.Create(MailOperation.ClearFlag));
|
||||
else
|
||||
list.Add(FolderOperationMenuItem.Create(FolderOperation.Pin));
|
||||
operationList.Add(MailOperationMenuItem.Create(MailOperation.SetFlag));
|
||||
|
||||
list.Add(FolderOperationMenuItem.Create(FolderOperation.Seperator));
|
||||
|
||||
// Following 4 items are disabled for system folders.
|
||||
|
||||
list.Add(FolderOperationMenuItem.Create(FolderOperation.Rename, !folderInformation.IsSystemFolder));
|
||||
list.Add(FolderOperationMenuItem.Create(FolderOperation.Delete, !folderInformation.IsSystemFolder));
|
||||
list.Add(FolderOperationMenuItem.Create(FolderOperation.CreateSubFolder, !folderInformation.IsSystemFolder));
|
||||
|
||||
list.Add(FolderOperationMenuItem.Create(FolderOperation.Seperator));
|
||||
|
||||
list.Add(FolderOperationMenuItem.Create(FolderOperation.Empty));
|
||||
|
||||
list.Add(FolderOperationMenuItem.Create(FolderOperation.MarkAllAsRead));
|
||||
|
||||
return list;
|
||||
if (singleItem.IsRead)
|
||||
operationList.Add(MailOperationMenuItem.Create(MailOperation.MarkAsUnread));
|
||||
else
|
||||
operationList.Add(MailOperationMenuItem.Create(MailOperation.MarkAsRead));
|
||||
}
|
||||
public virtual IEnumerable<MailOperationMenuItem> GetMailItemContextMenuActions(IEnumerable<IMailItem> selectedMailItems)
|
||||
else
|
||||
{
|
||||
if (selectedMailItems == null)
|
||||
return default;
|
||||
bool isAllRead = selectedMailItems.All(a => a.IsRead);
|
||||
bool isAllUnread = selectedMailItems.All(a => !a.IsRead);
|
||||
bool isAllFlagged = selectedMailItems.All(a => a.IsFlagged);
|
||||
bool isAllNotFlagged = selectedMailItems.All(a => !a.IsFlagged);
|
||||
|
||||
var operationList = new List<MailOperationMenuItem>();
|
||||
|
||||
// Disable archive button for Archive folder itself.
|
||||
|
||||
bool isArchiveFolder = selectedMailItems.All(a => a.AssignedFolder.SpecialFolderType == SpecialFolderType.Archive);
|
||||
bool isDraftOrSent = selectedMailItems.All(a => a.AssignedFolder.SpecialFolderType == SpecialFolderType.Draft || a.AssignedFolder.SpecialFolderType == SpecialFolderType.Sent);
|
||||
bool isJunkFolder = selectedMailItems.All(a => a.AssignedFolder.SpecialFolderType == SpecialFolderType.Junk);
|
||||
|
||||
bool isSingleItem = selectedMailItems.Count() == 1;
|
||||
|
||||
IMailItem singleItem = selectedMailItems.FirstOrDefault();
|
||||
|
||||
// Archive button.
|
||||
|
||||
if (isArchiveFolder)
|
||||
operationList.Add(MailOperationMenuItem.Create(MailOperation.UnArchive));
|
||||
else
|
||||
operationList.Add(MailOperationMenuItem.Create(MailOperation.Archive));
|
||||
|
||||
// Delete button.
|
||||
operationList.Add(MailOperationMenuItem.Create(MailOperation.SoftDelete));
|
||||
|
||||
// Move button.
|
||||
operationList.Add(MailOperationMenuItem.Create(MailOperation.Move, !isDraftOrSent));
|
||||
|
||||
// Independent flag, read etc.
|
||||
if (isSingleItem)
|
||||
List<MailOperationMenuItem> readOperations = (isAllRead, isAllUnread) switch
|
||||
{
|
||||
if (singleItem.IsFlagged)
|
||||
operationList.Add(MailOperationMenuItem.Create(MailOperation.ClearFlag));
|
||||
else
|
||||
operationList.Add(MailOperationMenuItem.Create(MailOperation.SetFlag));
|
||||
(true, false) => [MailOperationMenuItem.Create(MailOperation.MarkAsUnread)],
|
||||
(false, true) => [MailOperationMenuItem.Create(MailOperation.MarkAsRead)],
|
||||
_ => [MailOperationMenuItem.Create(MailOperation.MarkAsRead), MailOperationMenuItem.Create(MailOperation.MarkAsUnread)]
|
||||
};
|
||||
operationList.AddRange(readOperations);
|
||||
|
||||
if (singleItem.IsRead)
|
||||
operationList.Add(MailOperationMenuItem.Create(MailOperation.MarkAsUnread));
|
||||
else
|
||||
operationList.Add(MailOperationMenuItem.Create(MailOperation.MarkAsRead));
|
||||
}
|
||||
else
|
||||
List<MailOperationMenuItem> flagsOperations = (isAllFlagged, isAllNotFlagged) switch
|
||||
{
|
||||
bool isAllRead = selectedMailItems.All(a => a.IsRead);
|
||||
bool isAllUnread = selectedMailItems.All(a => !a.IsRead);
|
||||
bool isAllFlagged = selectedMailItems.All(a => a.IsFlagged);
|
||||
bool isAllNotFlagged = selectedMailItems.All(a => !a.IsFlagged);
|
||||
|
||||
List<MailOperationMenuItem> readOperations = (isAllRead, isAllUnread) switch
|
||||
{
|
||||
(true, false) => [MailOperationMenuItem.Create(MailOperation.MarkAsUnread)],
|
||||
(false, true) => [MailOperationMenuItem.Create(MailOperation.MarkAsRead)],
|
||||
_ => [MailOperationMenuItem.Create(MailOperation.MarkAsRead), MailOperationMenuItem.Create(MailOperation.MarkAsUnread)]
|
||||
};
|
||||
operationList.AddRange(readOperations);
|
||||
|
||||
List<MailOperationMenuItem> flagsOperations = (isAllFlagged, isAllNotFlagged) switch
|
||||
{
|
||||
(true, false) => [MailOperationMenuItem.Create(MailOperation.ClearFlag)],
|
||||
(false, true) => [MailOperationMenuItem.Create(MailOperation.SetFlag)],
|
||||
_ => [MailOperationMenuItem.Create(MailOperation.SetFlag), MailOperationMenuItem.Create(MailOperation.ClearFlag)]
|
||||
};
|
||||
operationList.AddRange(flagsOperations);
|
||||
}
|
||||
|
||||
// Ignore
|
||||
if (!isDraftOrSent)
|
||||
operationList.Add(MailOperationMenuItem.Create(MailOperation.Ignore));
|
||||
|
||||
// Seperator
|
||||
operationList.Add(MailOperationMenuItem.Create(MailOperation.Seperator));
|
||||
|
||||
// Junk folder
|
||||
if (isJunkFolder)
|
||||
operationList.Add(MailOperationMenuItem.Create(MailOperation.MarkAsNotJunk));
|
||||
else if (!isDraftOrSent)
|
||||
operationList.Add(MailOperationMenuItem.Create(MailOperation.MoveToJunk));
|
||||
|
||||
// TODO: Focus folder support.
|
||||
|
||||
// Remove the separator if it's the last item remaining.
|
||||
// It's creating unpleasent UI glitch.
|
||||
|
||||
if (operationList.LastOrDefault()?.Operation == MailOperation.Seperator)
|
||||
operationList.RemoveAt(operationList.Count - 1);
|
||||
|
||||
return operationList;
|
||||
(true, false) => [MailOperationMenuItem.Create(MailOperation.ClearFlag)],
|
||||
(false, true) => [MailOperationMenuItem.Create(MailOperation.SetFlag)],
|
||||
_ => [MailOperationMenuItem.Create(MailOperation.SetFlag), MailOperationMenuItem.Create(MailOperation.ClearFlag)]
|
||||
};
|
||||
operationList.AddRange(flagsOperations);
|
||||
}
|
||||
public virtual IEnumerable<MailOperationMenuItem> GetMailItemRenderMenuActions(IMailItem mailItem, bool isDarkEditor)
|
||||
|
||||
// Ignore
|
||||
if (!isDraftOrSent)
|
||||
operationList.Add(MailOperationMenuItem.Create(MailOperation.Ignore));
|
||||
|
||||
// Seperator
|
||||
operationList.Add(MailOperationMenuItem.Create(MailOperation.Seperator));
|
||||
|
||||
// Junk folder
|
||||
if (isJunkFolder)
|
||||
operationList.Add(MailOperationMenuItem.Create(MailOperation.MarkAsNotJunk));
|
||||
else if (!isDraftOrSent)
|
||||
operationList.Add(MailOperationMenuItem.Create(MailOperation.MoveToJunk));
|
||||
|
||||
// TODO: Focus folder support.
|
||||
|
||||
// Remove the separator if it's the last item remaining.
|
||||
// It's creating unpleasent UI glitch.
|
||||
|
||||
if (operationList.LastOrDefault()?.Operation == MailOperation.Seperator)
|
||||
operationList.RemoveAt(operationList.Count - 1);
|
||||
|
||||
return operationList;
|
||||
}
|
||||
public virtual IEnumerable<MailOperationMenuItem> GetMailItemRenderMenuActions(IMailItem mailItem, bool isDarkEditor)
|
||||
{
|
||||
var actionList = new List<MailOperationMenuItem>();
|
||||
|
||||
bool isArchiveFolder = mailItem.AssignedFolder.SpecialFolderType == SpecialFolderType.Archive;
|
||||
|
||||
// Add light/dark editor theme switch.
|
||||
if (isDarkEditor)
|
||||
actionList.Add(MailOperationMenuItem.Create(MailOperation.LightEditor));
|
||||
else
|
||||
actionList.Add(MailOperationMenuItem.Create(MailOperation.DarkEditor));
|
||||
|
||||
actionList.Add(MailOperationMenuItem.Create(MailOperation.Seperator));
|
||||
|
||||
// You can't do these to draft items.
|
||||
if (!mailItem.IsDraft)
|
||||
{
|
||||
var actionList = new List<MailOperationMenuItem>();
|
||||
// Reply
|
||||
actionList.Add(MailOperationMenuItem.Create(MailOperation.Reply));
|
||||
|
||||
bool isArchiveFolder = mailItem.AssignedFolder.SpecialFolderType == SpecialFolderType.Archive;
|
||||
// Reply All
|
||||
actionList.Add(MailOperationMenuItem.Create(MailOperation.ReplyAll));
|
||||
|
||||
// Add light/dark editor theme switch.
|
||||
if (isDarkEditor)
|
||||
actionList.Add(MailOperationMenuItem.Create(MailOperation.LightEditor));
|
||||
else
|
||||
actionList.Add(MailOperationMenuItem.Create(MailOperation.DarkEditor));
|
||||
|
||||
actionList.Add(MailOperationMenuItem.Create(MailOperation.Seperator));
|
||||
|
||||
// You can't do these to draft items.
|
||||
if (!mailItem.IsDraft)
|
||||
{
|
||||
// Reply
|
||||
actionList.Add(MailOperationMenuItem.Create(MailOperation.Reply));
|
||||
|
||||
// Reply All
|
||||
actionList.Add(MailOperationMenuItem.Create(MailOperation.ReplyAll));
|
||||
|
||||
// Forward
|
||||
actionList.Add(MailOperationMenuItem.Create(MailOperation.Forward));
|
||||
}
|
||||
|
||||
// Archive - Unarchive
|
||||
if (isArchiveFolder)
|
||||
actionList.Add(MailOperationMenuItem.Create(MailOperation.UnArchive));
|
||||
else
|
||||
actionList.Add(MailOperationMenuItem.Create(MailOperation.Archive));
|
||||
|
||||
// Delete
|
||||
actionList.Add(MailOperationMenuItem.Create(MailOperation.SoftDelete));
|
||||
|
||||
// Flag - Clear Flag
|
||||
if (mailItem.IsFlagged)
|
||||
actionList.Add(MailOperationMenuItem.Create(MailOperation.ClearFlag));
|
||||
else
|
||||
actionList.Add(MailOperationMenuItem.Create(MailOperation.SetFlag));
|
||||
|
||||
// Secondary items.
|
||||
|
||||
// Read - Unread
|
||||
if (mailItem.IsRead)
|
||||
actionList.Add(MailOperationMenuItem.Create(MailOperation.MarkAsUnread, true, false));
|
||||
else
|
||||
actionList.Add(MailOperationMenuItem.Create(MailOperation.MarkAsRead, true, false));
|
||||
|
||||
return actionList;
|
||||
// Forward
|
||||
actionList.Add(MailOperationMenuItem.Create(MailOperation.Forward));
|
||||
}
|
||||
|
||||
// Archive - Unarchive
|
||||
if (isArchiveFolder)
|
||||
actionList.Add(MailOperationMenuItem.Create(MailOperation.UnArchive));
|
||||
else
|
||||
actionList.Add(MailOperationMenuItem.Create(MailOperation.Archive));
|
||||
|
||||
// Delete
|
||||
actionList.Add(MailOperationMenuItem.Create(MailOperation.SoftDelete));
|
||||
|
||||
// Flag - Clear Flag
|
||||
if (mailItem.IsFlagged)
|
||||
actionList.Add(MailOperationMenuItem.Create(MailOperation.ClearFlag));
|
||||
else
|
||||
actionList.Add(MailOperationMenuItem.Create(MailOperation.SetFlag));
|
||||
|
||||
// Secondary items.
|
||||
|
||||
// Read - Unread
|
||||
if (mailItem.IsRead)
|
||||
actionList.Add(MailOperationMenuItem.Create(MailOperation.MarkAsUnread, true, false));
|
||||
else
|
||||
actionList.Add(MailOperationMenuItem.Create(MailOperation.MarkAsRead, true, false));
|
||||
|
||||
return actionList;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,59 +6,58 @@ using Wino.Core.Domain.Entities.Mail;
|
||||
using Wino.Core.Domain.Entities.Shared;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
|
||||
namespace Wino.Services
|
||||
namespace Wino.Services;
|
||||
|
||||
public interface IDatabaseService : IInitializeAsync
|
||||
{
|
||||
public interface IDatabaseService : IInitializeAsync
|
||||
SQLiteAsyncConnection Connection { get; }
|
||||
}
|
||||
|
||||
public class DatabaseService : IDatabaseService
|
||||
{
|
||||
private const string DatabaseName = "Wino180.db";
|
||||
|
||||
private bool _isInitialized = false;
|
||||
private readonly IApplicationConfiguration _folderConfiguration;
|
||||
|
||||
public SQLiteAsyncConnection Connection { get; private set; }
|
||||
|
||||
public DatabaseService(IApplicationConfiguration folderConfiguration)
|
||||
{
|
||||
SQLiteAsyncConnection Connection { get; }
|
||||
_folderConfiguration = folderConfiguration;
|
||||
}
|
||||
|
||||
public class DatabaseService : IDatabaseService
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
private const string DatabaseName = "Wino180.db";
|
||||
if (_isInitialized)
|
||||
return;
|
||||
|
||||
private bool _isInitialized = false;
|
||||
private readonly IApplicationConfiguration _folderConfiguration;
|
||||
var publisherCacheFolder = _folderConfiguration.PublisherSharedFolderPath;
|
||||
var databaseFileName = Path.Combine(publisherCacheFolder, DatabaseName);
|
||||
|
||||
public SQLiteAsyncConnection Connection { get; private set; }
|
||||
Connection = new SQLiteAsyncConnection(databaseFileName);
|
||||
|
||||
public DatabaseService(IApplicationConfiguration folderConfiguration)
|
||||
{
|
||||
_folderConfiguration = folderConfiguration;
|
||||
}
|
||||
await CreateTablesAsync();
|
||||
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
if (_isInitialized)
|
||||
return;
|
||||
_isInitialized = true;
|
||||
}
|
||||
|
||||
var publisherCacheFolder = _folderConfiguration.PublisherSharedFolderPath;
|
||||
var databaseFileName = Path.Combine(publisherCacheFolder, DatabaseName);
|
||||
|
||||
Connection = new SQLiteAsyncConnection(databaseFileName);
|
||||
|
||||
await CreateTablesAsync();
|
||||
|
||||
_isInitialized = true;
|
||||
}
|
||||
|
||||
private async Task CreateTablesAsync()
|
||||
{
|
||||
await Connection.CreateTablesAsync(CreateFlags.None,
|
||||
typeof(MailCopy),
|
||||
typeof(MailItemFolder),
|
||||
typeof(MailAccount),
|
||||
typeof(AccountContact),
|
||||
typeof(CustomServerInformation),
|
||||
typeof(AccountSignature),
|
||||
typeof(MergedInbox),
|
||||
typeof(MailAccountPreferences),
|
||||
typeof(MailAccountAlias),
|
||||
typeof(AccountCalendar),
|
||||
typeof(CalendarEventAttendee),
|
||||
typeof(CalendarItem),
|
||||
typeof(Reminder)
|
||||
);
|
||||
}
|
||||
private async Task CreateTablesAsync()
|
||||
{
|
||||
await Connection.CreateTablesAsync(CreateFlags.None,
|
||||
typeof(MailCopy),
|
||||
typeof(MailItemFolder),
|
||||
typeof(MailAccount),
|
||||
typeof(AccountContact),
|
||||
typeof(CustomServerInformation),
|
||||
typeof(AccountSignature),
|
||||
typeof(MergedInbox),
|
||||
typeof(MailAccountPreferences),
|
||||
typeof(MailAccountAlias),
|
||||
typeof(AccountCalendar),
|
||||
typeof(CalendarEventAttendee),
|
||||
typeof(CalendarItem),
|
||||
typeof(Reminder)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,118 +3,117 @@ using System.IO;
|
||||
using System.Linq;
|
||||
using HtmlAgilityPack;
|
||||
|
||||
namespace Wino.Services.Extensions
|
||||
namespace Wino.Services.Extensions;
|
||||
|
||||
public static class HtmlAgilityPackExtensions
|
||||
{
|
||||
public static class HtmlAgilityPackExtensions
|
||||
/// <summary>
|
||||
/// Clears out the src attribute for all `img` and `v:fill` tags.
|
||||
/// </summary>
|
||||
/// <param name="document"></param>
|
||||
public static void ClearImages(this HtmlDocument document)
|
||||
{
|
||||
/// <summary>
|
||||
/// Clears out the src attribute for all `img` and `v:fill` tags.
|
||||
/// </summary>
|
||||
/// <param name="document"></param>
|
||||
public static void ClearImages(this HtmlDocument document)
|
||||
if (document.DocumentNode.InnerHtml.Contains("<img"))
|
||||
{
|
||||
if (document.DocumentNode.InnerHtml.Contains("<img"))
|
||||
foreach (var eachNode in document.DocumentNode.SelectNodes("//img"))
|
||||
{
|
||||
foreach (var eachNode in document.DocumentNode.SelectNodes("//img"))
|
||||
{
|
||||
eachNode.Attributes.Remove("src");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes `style` tags from the document.
|
||||
/// </summary>
|
||||
/// <param name="document"></param>
|
||||
public static void ClearStyles(this HtmlDocument document)
|
||||
{
|
||||
document.DocumentNode
|
||||
.Descendants()
|
||||
.Where(n => n.Name.Equals("script", StringComparison.OrdinalIgnoreCase)
|
||||
|| n.Name.Equals("style", StringComparison.OrdinalIgnoreCase)
|
||||
|| n.Name.Equals("#comment", StringComparison.OrdinalIgnoreCase))
|
||||
.ToList()
|
||||
.ForEach(n => n.Remove());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns plain text from the HTML content.
|
||||
/// </summary>
|
||||
/// <param name="htmlContent">Content to get preview from.</param>
|
||||
/// <returns>Text body for the html.</returns>
|
||||
public static string GetPreviewText(string htmlContent)
|
||||
{
|
||||
if (string.IsNullOrEmpty(htmlContent)) return string.Empty;
|
||||
|
||||
HtmlDocument doc = new HtmlDocument();
|
||||
doc.LoadHtml(htmlContent);
|
||||
|
||||
StringWriter sw = new StringWriter();
|
||||
ConvertTo(doc.DocumentNode, sw);
|
||||
sw.Flush();
|
||||
|
||||
return sw.ToString().Replace(Environment.NewLine, "");
|
||||
}
|
||||
|
||||
private static void ConvertContentTo(HtmlNode node, TextWriter outText)
|
||||
{
|
||||
foreach (HtmlNode subnode in node.ChildNodes)
|
||||
{
|
||||
ConvertTo(subnode, outText);
|
||||
}
|
||||
}
|
||||
|
||||
private static void ConvertTo(HtmlNode node, TextWriter outText)
|
||||
{
|
||||
string html;
|
||||
switch (node.NodeType)
|
||||
{
|
||||
case HtmlNodeType.Comment:
|
||||
// don't output comments
|
||||
break;
|
||||
|
||||
case HtmlNodeType.Document:
|
||||
ConvertContentTo(node, outText);
|
||||
break;
|
||||
|
||||
case HtmlNodeType.Text:
|
||||
// script and style must not be output
|
||||
string parentName = node.ParentNode.Name;
|
||||
if (parentName == "script" || parentName == "style")
|
||||
break;
|
||||
|
||||
// get text
|
||||
html = ((HtmlTextNode)node).Text;
|
||||
|
||||
// is it in fact a special closing node output as text?
|
||||
if (HtmlNode.IsOverlappedClosingElement(html))
|
||||
break;
|
||||
|
||||
// check the text is meaningful and not a bunch of whitespaces
|
||||
if (html.Trim().Length > 0)
|
||||
{
|
||||
outText.Write(HtmlEntity.DeEntitize(html));
|
||||
}
|
||||
break;
|
||||
|
||||
case HtmlNodeType.Element:
|
||||
switch (node.Name)
|
||||
{
|
||||
case "p":
|
||||
// treat paragraphs as crlf
|
||||
outText.Write("\r\n");
|
||||
break;
|
||||
case "br":
|
||||
outText.Write("\r\n");
|
||||
break;
|
||||
}
|
||||
|
||||
if (node.HasChildNodes)
|
||||
{
|
||||
ConvertContentTo(node, outText);
|
||||
}
|
||||
break;
|
||||
eachNode.Attributes.Remove("src");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes `style` tags from the document.
|
||||
/// </summary>
|
||||
/// <param name="document"></param>
|
||||
public static void ClearStyles(this HtmlDocument document)
|
||||
{
|
||||
document.DocumentNode
|
||||
.Descendants()
|
||||
.Where(n => n.Name.Equals("script", StringComparison.OrdinalIgnoreCase)
|
||||
|| n.Name.Equals("style", StringComparison.OrdinalIgnoreCase)
|
||||
|| n.Name.Equals("#comment", StringComparison.OrdinalIgnoreCase))
|
||||
.ToList()
|
||||
.ForEach(n => n.Remove());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns plain text from the HTML content.
|
||||
/// </summary>
|
||||
/// <param name="htmlContent">Content to get preview from.</param>
|
||||
/// <returns>Text body for the html.</returns>
|
||||
public static string GetPreviewText(string htmlContent)
|
||||
{
|
||||
if (string.IsNullOrEmpty(htmlContent)) return string.Empty;
|
||||
|
||||
HtmlDocument doc = new HtmlDocument();
|
||||
doc.LoadHtml(htmlContent);
|
||||
|
||||
StringWriter sw = new StringWriter();
|
||||
ConvertTo(doc.DocumentNode, sw);
|
||||
sw.Flush();
|
||||
|
||||
return sw.ToString().Replace(Environment.NewLine, "");
|
||||
}
|
||||
|
||||
private static void ConvertContentTo(HtmlNode node, TextWriter outText)
|
||||
{
|
||||
foreach (HtmlNode subnode in node.ChildNodes)
|
||||
{
|
||||
ConvertTo(subnode, outText);
|
||||
}
|
||||
}
|
||||
|
||||
private static void ConvertTo(HtmlNode node, TextWriter outText)
|
||||
{
|
||||
string html;
|
||||
switch (node.NodeType)
|
||||
{
|
||||
case HtmlNodeType.Comment:
|
||||
// don't output comments
|
||||
break;
|
||||
|
||||
case HtmlNodeType.Document:
|
||||
ConvertContentTo(node, outText);
|
||||
break;
|
||||
|
||||
case HtmlNodeType.Text:
|
||||
// script and style must not be output
|
||||
string parentName = node.ParentNode.Name;
|
||||
if (parentName == "script" || parentName == "style")
|
||||
break;
|
||||
|
||||
// get text
|
||||
html = ((HtmlTextNode)node).Text;
|
||||
|
||||
// is it in fact a special closing node output as text?
|
||||
if (HtmlNode.IsOverlappedClosingElement(html))
|
||||
break;
|
||||
|
||||
// check the text is meaningful and not a bunch of whitespaces
|
||||
if (html.Trim().Length > 0)
|
||||
{
|
||||
outText.Write(HtmlEntity.DeEntitize(html));
|
||||
}
|
||||
break;
|
||||
|
||||
case HtmlNodeType.Element:
|
||||
switch (node.Name)
|
||||
{
|
||||
case "p":
|
||||
// treat paragraphs as crlf
|
||||
outText.Write("\r\n");
|
||||
break;
|
||||
case "br":
|
||||
outText.Write("\r\n");
|
||||
break;
|
||||
}
|
||||
|
||||
if (node.HasChildNodes)
|
||||
{
|
||||
ConvertContentTo(node, outText);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,188 +6,187 @@ using Wino.Core.Domain;
|
||||
using Wino.Core.Domain.Entities.Mail;
|
||||
using Wino.Core.Domain.Enums;
|
||||
|
||||
namespace Wino.Services.Extensions
|
||||
namespace Wino.Services.Extensions;
|
||||
|
||||
public static class MailkitClientExtensions
|
||||
{
|
||||
public static class MailkitClientExtensions
|
||||
public static char MailCopyUidSeparator = '_';
|
||||
|
||||
public static uint ResolveUid(string mailCopyId)
|
||||
{
|
||||
public static char MailCopyUidSeparator = '_';
|
||||
var splitted = mailCopyId.Split(MailCopyUidSeparator);
|
||||
|
||||
public static uint ResolveUid(string mailCopyId)
|
||||
if (splitted.Length > 1 && uint.TryParse(splitted[1], out uint parsedUint)) return parsedUint;
|
||||
|
||||
throw new ArgumentOutOfRangeException(nameof(mailCopyId), mailCopyId, "Invalid mailCopyId format.");
|
||||
}
|
||||
|
||||
public static UniqueId ResolveUidStruct(string mailCopyId)
|
||||
=> new UniqueId(ResolveUid(mailCopyId));
|
||||
|
||||
public static string CreateUid(Guid folderId, uint messageUid)
|
||||
=> $"{folderId}{MailCopyUidSeparator}{messageUid}";
|
||||
|
||||
public static MailImportance GetImportance(this MimeMessage messageSummary)
|
||||
{
|
||||
if (messageSummary.Headers != null && messageSummary.Headers.Contains(HeaderId.Importance))
|
||||
{
|
||||
var splitted = mailCopyId.Split(MailCopyUidSeparator);
|
||||
var rawImportance = messageSummary.Headers[HeaderId.Importance];
|
||||
|
||||
if (splitted.Length > 1 && uint.TryParse(splitted[1], out uint parsedUint)) return parsedUint;
|
||||
|
||||
throw new ArgumentOutOfRangeException(nameof(mailCopyId), mailCopyId, "Invalid mailCopyId format.");
|
||||
}
|
||||
|
||||
public static UniqueId ResolveUidStruct(string mailCopyId)
|
||||
=> new UniqueId(ResolveUid(mailCopyId));
|
||||
|
||||
public static string CreateUid(Guid folderId, uint messageUid)
|
||||
=> $"{folderId}{MailCopyUidSeparator}{messageUid}";
|
||||
|
||||
public static MailImportance GetImportance(this MimeMessage messageSummary)
|
||||
{
|
||||
if (messageSummary.Headers != null && messageSummary.Headers.Contains(HeaderId.Importance))
|
||||
return rawImportance switch
|
||||
{
|
||||
var rawImportance = messageSummary.Headers[HeaderId.Importance];
|
||||
|
||||
return rawImportance switch
|
||||
{
|
||||
"Low" => MailImportance.Low,
|
||||
"High" => MailImportance.High,
|
||||
_ => MailImportance.Normal,
|
||||
};
|
||||
}
|
||||
|
||||
return MailImportance.Normal;
|
||||
}
|
||||
|
||||
public static bool GetIsRead(this MessageFlags? flags)
|
||||
=> flags.GetValueOrDefault().HasFlag(MessageFlags.Seen);
|
||||
|
||||
public static bool GetIsFlagged(this MessageFlags? flags)
|
||||
=> flags.GetValueOrDefault().HasFlag(MessageFlags.Flagged);
|
||||
|
||||
public static string GetThreadId(this IMessageSummary messageSummary)
|
||||
{
|
||||
// First check whether we have the default values.
|
||||
|
||||
if (!string.IsNullOrEmpty(messageSummary.ThreadId))
|
||||
return messageSummary.ThreadId;
|
||||
|
||||
if (messageSummary.GMailThreadId != null)
|
||||
return messageSummary.GMailThreadId.ToString();
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
public static string GetMessageId(this MimeMessage mimeMessage)
|
||||
=> mimeMessage.MessageId;
|
||||
|
||||
public static string GetReferences(this MessageIdList messageIdList)
|
||||
=> string.Join(";", messageIdList);
|
||||
|
||||
public static string GetInReplyTo(this MimeMessage mimeMessage)
|
||||
{
|
||||
if (mimeMessage.Headers.Contains(HeaderId.InReplyTo))
|
||||
{
|
||||
// Normalize if <> brackets are there.
|
||||
var inReplyTo = mimeMessage.Headers[HeaderId.InReplyTo];
|
||||
|
||||
if (inReplyTo.StartsWith("<") && inReplyTo.EndsWith(">"))
|
||||
return inReplyTo.Substring(1, inReplyTo.Length - 2);
|
||||
|
||||
return inReplyTo;
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
private static string GetPreviewText(this MimeMessage message)
|
||||
{
|
||||
if (string.IsNullOrEmpty(message.HtmlBody))
|
||||
return message.TextBody;
|
||||
else
|
||||
return HtmlAgilityPackExtensions.GetPreviewText(message.HtmlBody);
|
||||
}
|
||||
|
||||
public static MailCopy GetMailDetails(this IMessageSummary messageSummary, MailItemFolder folder, MimeMessage mime)
|
||||
{
|
||||
// MessageSummary will only have UniqueId, Flags, ThreadId.
|
||||
// Other properties are extracted directly from the MimeMessage.
|
||||
|
||||
// IMAP doesn't have unique id for mails.
|
||||
// All mails are mapped to specific folders with incremental Id.
|
||||
// Uid 1 may belong to different messages in different folders, but can never be
|
||||
// same for different messages in same folders.
|
||||
// Here we create arbitrary Id that maps the Id of the message with Folder UniqueId.
|
||||
// When folder becomes invalid, we'll clear out these MailCopies as well.
|
||||
|
||||
var messageUid = CreateUid(folder.Id, messageSummary.UniqueId.Id);
|
||||
var previewText = mime.GetPreviewText();
|
||||
|
||||
var copy = new MailCopy()
|
||||
{
|
||||
Id = messageUid,
|
||||
CreationDate = mime.Date.UtcDateTime,
|
||||
ThreadId = messageSummary.GetThreadId(),
|
||||
MessageId = mime.GetMessageId(),
|
||||
Subject = mime.Subject,
|
||||
IsRead = messageSummary.Flags.GetIsRead(),
|
||||
IsFlagged = messageSummary.Flags.GetIsFlagged(),
|
||||
PreviewText = previewText,
|
||||
FromAddress = GetActualSenderAddress(mime),
|
||||
FromName = GetActualSenderName(mime),
|
||||
IsFocused = false,
|
||||
Importance = mime.GetImportance(),
|
||||
References = mime.References?.GetReferences(),
|
||||
InReplyTo = mime.GetInReplyTo(),
|
||||
HasAttachments = mime.Attachments.Any(),
|
||||
FileId = Guid.NewGuid()
|
||||
"Low" => MailImportance.Low,
|
||||
"High" => MailImportance.High,
|
||||
_ => MailImportance.Normal,
|
||||
};
|
||||
|
||||
return copy;
|
||||
}
|
||||
|
||||
// TODO: Name and Address parsing should be handled better.
|
||||
// At some point Wino needs better contact management.
|
||||
return MailImportance.Normal;
|
||||
}
|
||||
|
||||
public static string GetActualSenderName(MimeMessage message)
|
||||
public static bool GetIsRead(this MessageFlags? flags)
|
||||
=> flags.GetValueOrDefault().HasFlag(MessageFlags.Seen);
|
||||
|
||||
public static bool GetIsFlagged(this MessageFlags? flags)
|
||||
=> flags.GetValueOrDefault().HasFlag(MessageFlags.Flagged);
|
||||
|
||||
public static string GetThreadId(this IMessageSummary messageSummary)
|
||||
{
|
||||
// First check whether we have the default values.
|
||||
|
||||
if (!string.IsNullOrEmpty(messageSummary.ThreadId))
|
||||
return messageSummary.ThreadId;
|
||||
|
||||
if (messageSummary.GMailThreadId != null)
|
||||
return messageSummary.GMailThreadId.ToString();
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
public static string GetMessageId(this MimeMessage mimeMessage)
|
||||
=> mimeMessage.MessageId;
|
||||
|
||||
public static string GetReferences(this MessageIdList messageIdList)
|
||||
=> string.Join(";", messageIdList);
|
||||
|
||||
public static string GetInReplyTo(this MimeMessage mimeMessage)
|
||||
{
|
||||
if (mimeMessage.Headers.Contains(HeaderId.InReplyTo))
|
||||
{
|
||||
if (message == null)
|
||||
return string.Empty;
|
||||
// Normalize if <> brackets are there.
|
||||
var inReplyTo = mimeMessage.Headers[HeaderId.InReplyTo];
|
||||
|
||||
return message.From.Mailboxes.FirstOrDefault()?.Name ?? message.Sender?.Name ?? Translator.UnknownSender;
|
||||
if (inReplyTo.StartsWith("<") && inReplyTo.EndsWith(">"))
|
||||
return inReplyTo.Substring(1, inReplyTo.Length - 2);
|
||||
|
||||
// From MimeKit
|
||||
|
||||
// The "From" header specifies the author(s) of the message.
|
||||
// If more than one MimeKit.MailboxAddress is added to the list of "From" addresses,
|
||||
// the MimeKit.MimeMessage.Sender should be set to the single MimeKit.MailboxAddress
|
||||
// of the personal actually sending the message.
|
||||
|
||||
// Also handle: https://stackoverflow.com/questions/46474030/mailkit-from-address
|
||||
|
||||
//if (message.Sender != null)
|
||||
// return string.IsNullOrEmpty(message.Sender.Name) ? message.Sender.Address : message.Sender.Name;
|
||||
//else if (message.From?.Mailboxes != null)
|
||||
//{
|
||||
// var firstAvailableName = message.From.Mailboxes.FirstOrDefault(a => !string.IsNullOrEmpty(a.Name))?.Name;
|
||||
|
||||
// if (string.IsNullOrEmpty(firstAvailableName))
|
||||
// {
|
||||
// var firstAvailableAddress = message.From.Mailboxes.FirstOrDefault(a => !string.IsNullOrEmpty(a.Address))?.Address;
|
||||
|
||||
// if (!string.IsNullOrEmpty(firstAvailableAddress))
|
||||
// {
|
||||
// return firstAvailableAddress;
|
||||
// }
|
||||
// }
|
||||
|
||||
// return firstAvailableName;
|
||||
//}
|
||||
|
||||
//// No sender, no from, I don't know what to do.
|
||||
//return Translator.UnknownSender;
|
||||
return inReplyTo;
|
||||
}
|
||||
|
||||
// TODO: This is wrong.
|
||||
public static string GetActualSenderAddress(MimeMessage message)
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
private static string GetPreviewText(this MimeMessage message)
|
||||
{
|
||||
if (string.IsNullOrEmpty(message.HtmlBody))
|
||||
return message.TextBody;
|
||||
else
|
||||
return HtmlAgilityPackExtensions.GetPreviewText(message.HtmlBody);
|
||||
}
|
||||
|
||||
public static MailCopy GetMailDetails(this IMessageSummary messageSummary, MailItemFolder folder, MimeMessage mime)
|
||||
{
|
||||
// MessageSummary will only have UniqueId, Flags, ThreadId.
|
||||
// Other properties are extracted directly from the MimeMessage.
|
||||
|
||||
// IMAP doesn't have unique id for mails.
|
||||
// All mails are mapped to specific folders with incremental Id.
|
||||
// Uid 1 may belong to different messages in different folders, but can never be
|
||||
// same for different messages in same folders.
|
||||
// Here we create arbitrary Id that maps the Id of the message with Folder UniqueId.
|
||||
// When folder becomes invalid, we'll clear out these MailCopies as well.
|
||||
|
||||
var messageUid = CreateUid(folder.Id, messageSummary.UniqueId.Id);
|
||||
var previewText = mime.GetPreviewText();
|
||||
|
||||
var copy = new MailCopy()
|
||||
{
|
||||
return message.From.Mailboxes.FirstOrDefault()?.Address ?? message.Sender?.Address ?? Translator.UnknownSender;
|
||||
//if (mime == null)
|
||||
// return string.Empty;
|
||||
Id = messageUid,
|
||||
CreationDate = mime.Date.UtcDateTime,
|
||||
ThreadId = messageSummary.GetThreadId(),
|
||||
MessageId = mime.GetMessageId(),
|
||||
Subject = mime.Subject,
|
||||
IsRead = messageSummary.Flags.GetIsRead(),
|
||||
IsFlagged = messageSummary.Flags.GetIsFlagged(),
|
||||
PreviewText = previewText,
|
||||
FromAddress = GetActualSenderAddress(mime),
|
||||
FromName = GetActualSenderName(mime),
|
||||
IsFocused = false,
|
||||
Importance = mime.GetImportance(),
|
||||
References = mime.References?.GetReferences(),
|
||||
InReplyTo = mime.GetInReplyTo(),
|
||||
HasAttachments = mime.Attachments.Any(),
|
||||
FileId = Guid.NewGuid()
|
||||
};
|
||||
|
||||
//bool hasSingleFromMailbox = mime.From.Mailboxes.Count() == 1;
|
||||
return copy;
|
||||
}
|
||||
|
||||
//if (hasSingleFromMailbox)
|
||||
// return mime.From.Mailboxes.First().GetAddress(idnEncode: true);
|
||||
//else if (mime.Sender != null)
|
||||
// return mime.Sender.GetAddress(idnEncode: true);
|
||||
//else
|
||||
// return Translator.UnknownSender;
|
||||
}
|
||||
// TODO: Name and Address parsing should be handled better.
|
||||
// At some point Wino needs better contact management.
|
||||
|
||||
public static string GetActualSenderName(MimeMessage message)
|
||||
{
|
||||
if (message == null)
|
||||
return string.Empty;
|
||||
|
||||
return message.From.Mailboxes.FirstOrDefault()?.Name ?? message.Sender?.Name ?? Translator.UnknownSender;
|
||||
|
||||
// From MimeKit
|
||||
|
||||
// The "From" header specifies the author(s) of the message.
|
||||
// If more than one MimeKit.MailboxAddress is added to the list of "From" addresses,
|
||||
// the MimeKit.MimeMessage.Sender should be set to the single MimeKit.MailboxAddress
|
||||
// of the personal actually sending the message.
|
||||
|
||||
// Also handle: https://stackoverflow.com/questions/46474030/mailkit-from-address
|
||||
|
||||
//if (message.Sender != null)
|
||||
// return string.IsNullOrEmpty(message.Sender.Name) ? message.Sender.Address : message.Sender.Name;
|
||||
//else if (message.From?.Mailboxes != null)
|
||||
//{
|
||||
// var firstAvailableName = message.From.Mailboxes.FirstOrDefault(a => !string.IsNullOrEmpty(a.Name))?.Name;
|
||||
|
||||
// if (string.IsNullOrEmpty(firstAvailableName))
|
||||
// {
|
||||
// var firstAvailableAddress = message.From.Mailboxes.FirstOrDefault(a => !string.IsNullOrEmpty(a.Address))?.Address;
|
||||
|
||||
// if (!string.IsNullOrEmpty(firstAvailableAddress))
|
||||
// {
|
||||
// return firstAvailableAddress;
|
||||
// }
|
||||
// }
|
||||
|
||||
// return firstAvailableName;
|
||||
//}
|
||||
|
||||
//// No sender, no from, I don't know what to do.
|
||||
//return Translator.UnknownSender;
|
||||
}
|
||||
|
||||
// TODO: This is wrong.
|
||||
public static string GetActualSenderAddress(MimeMessage message)
|
||||
{
|
||||
return message.From.Mailboxes.FirstOrDefault()?.Address ?? message.Sender?.Address ?? Translator.UnknownSender;
|
||||
//if (mime == null)
|
||||
// return string.Empty;
|
||||
|
||||
//bool hasSingleFromMailbox = mime.From.Mailboxes.Count() == 1;
|
||||
|
||||
//if (hasSingleFromMailbox)
|
||||
// return mime.From.Mailboxes.First().GetAddress(idnEncode: true);
|
||||
//else if (mime.Sender != null)
|
||||
// return mime.Sender.GetAddress(idnEncode: true);
|
||||
//else
|
||||
// return Translator.UnknownSender;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
using SqlKata;
|
||||
using SqlKata.Compilers;
|
||||
|
||||
namespace Wino.Services.Extensions
|
||||
{
|
||||
public static class SqlKataExtensions
|
||||
{
|
||||
private static SqliteCompiler Compiler = new SqliteCompiler();
|
||||
namespace Wino.Services.Extensions;
|
||||
|
||||
public static string GetRawQuery(this Query query)
|
||||
{
|
||||
return Compiler.Compile(query).ToString();
|
||||
}
|
||||
public static class SqlKataExtensions
|
||||
{
|
||||
private static SqliteCompiler Compiler = new SqliteCompiler();
|
||||
|
||||
public static string GetRawQuery(this Query query)
|
||||
{
|
||||
return Compiler.Compile(query).ToString();
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,11 +1,10 @@
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Core.Domain.Models.Launch;
|
||||
|
||||
namespace Wino.Services
|
||||
namespace Wino.Services;
|
||||
|
||||
public class LaunchProtocolService : ILaunchProtocolService
|
||||
{
|
||||
public class LaunchProtocolService : ILaunchProtocolService
|
||||
{
|
||||
public object LaunchParameter { get; set; }
|
||||
public MailToUri MailToUri { get; set; }
|
||||
}
|
||||
public object LaunchParameter { get; set; }
|
||||
public MailToUri MailToUri { get; set; }
|
||||
}
|
||||
|
||||
@@ -5,46 +5,45 @@ using Serilog.Exceptions;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Services.Misc;
|
||||
|
||||
namespace Wino.Services
|
||||
namespace Wino.Services;
|
||||
|
||||
public class LogInitializer : ILogInitializer
|
||||
{
|
||||
public class LogInitializer : ILogInitializer
|
||||
private readonly LoggingLevelSwitch _levelSwitch = new LoggingLevelSwitch();
|
||||
private readonly IPreferencesService _preferencesService;
|
||||
private readonly IApplicationConfiguration _applicationConfiguration;
|
||||
private readonly TelemetryConfiguration _telemetryConfiguration;
|
||||
|
||||
public LogInitializer(IPreferencesService preferencesService, IApplicationConfiguration applicationConfiguration)
|
||||
{
|
||||
private readonly LoggingLevelSwitch _levelSwitch = new LoggingLevelSwitch();
|
||||
private readonly IPreferencesService _preferencesService;
|
||||
private readonly IApplicationConfiguration _applicationConfiguration;
|
||||
private readonly TelemetryConfiguration _telemetryConfiguration;
|
||||
_preferencesService = preferencesService;
|
||||
_applicationConfiguration = applicationConfiguration;
|
||||
|
||||
public LogInitializer(IPreferencesService preferencesService, IApplicationConfiguration applicationConfiguration)
|
||||
{
|
||||
_preferencesService = preferencesService;
|
||||
_applicationConfiguration = applicationConfiguration;
|
||||
_telemetryConfiguration = new TelemetryConfiguration(applicationConfiguration.ApplicationInsightsInstrumentationKey);
|
||||
|
||||
_telemetryConfiguration = new TelemetryConfiguration(applicationConfiguration.ApplicationInsightsInstrumentationKey);
|
||||
RefreshLoggingLevel();
|
||||
}
|
||||
|
||||
RefreshLoggingLevel();
|
||||
}
|
||||
|
||||
public void RefreshLoggingLevel()
|
||||
{
|
||||
public void RefreshLoggingLevel()
|
||||
{
|
||||
#if DEBUG
|
||||
_levelSwitch.MinimumLevel = Serilog.Events.LogEventLevel.Debug;
|
||||
_levelSwitch.MinimumLevel = Serilog.Events.LogEventLevel.Debug;
|
||||
#else
|
||||
_levelSwitch.MinimumLevel = _preferencesService.IsLoggingEnabled ? Serilog.Events.LogEventLevel.Information : Serilog.Events.LogEventLevel.Fatal;
|
||||
_levelSwitch.MinimumLevel = _preferencesService.IsLoggingEnabled ? Serilog.Events.LogEventLevel.Information : Serilog.Events.LogEventLevel.Fatal;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
public void SetupLogger(string fullLogFilePath)
|
||||
{
|
||||
var insightsTelemetryConverter = new WinoTelemetryConverter(_preferencesService.DiagnosticId);
|
||||
public void SetupLogger(string fullLogFilePath)
|
||||
{
|
||||
var insightsTelemetryConverter = new WinoTelemetryConverter(_preferencesService.DiagnosticId);
|
||||
|
||||
Log.Logger = new LoggerConfiguration()
|
||||
.MinimumLevel.ControlledBy(_levelSwitch)
|
||||
.WriteTo.File(fullLogFilePath, retainedFileCountLimit: 3, rollOnFileSizeLimit: true, rollingInterval: RollingInterval.Day)
|
||||
.WriteTo.Debug()
|
||||
.WriteTo.ApplicationInsights(_telemetryConfiguration, insightsTelemetryConverter, restrictedToMinimumLevel: Serilog.Events.LogEventLevel.Error)
|
||||
.Enrich.FromLogContext()
|
||||
.Enrich.WithExceptionDetails()
|
||||
.CreateLogger();
|
||||
}
|
||||
Log.Logger = new LoggerConfiguration()
|
||||
.MinimumLevel.ControlledBy(_levelSwitch)
|
||||
.WriteTo.File(fullLogFilePath, retainedFileCountLimit: 3, rollOnFileSizeLimit: true, rollingInterval: RollingInterval.Day)
|
||||
.WriteTo.Debug()
|
||||
.WriteTo.ApplicationInsights(_telemetryConfiguration, insightsTelemetryConverter, restrictedToMinimumLevel: Serilog.Events.LogEventLevel.Error)
|
||||
.Enrich.FromLogContext()
|
||||
.Enrich.WithExceptionDetails()
|
||||
.CreateLogger();
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -10,171 +10,170 @@ using Wino.Core.Domain.Models.MailItem;
|
||||
using Wino.Core.Domain.Models.Reader;
|
||||
using Wino.Services.Extensions;
|
||||
|
||||
namespace Wino.Services
|
||||
namespace Wino.Services;
|
||||
|
||||
public class MimeFileService : IMimeFileService
|
||||
{
|
||||
public class MimeFileService : IMimeFileService
|
||||
private readonly INativeAppService _nativeAppService;
|
||||
private ILogger _logger = Log.ForContext<MimeFileService>();
|
||||
|
||||
public MimeFileService(INativeAppService nativeAppService)
|
||||
{
|
||||
private readonly INativeAppService _nativeAppService;
|
||||
private ILogger _logger = Log.ForContext<MimeFileService>();
|
||||
_nativeAppService = nativeAppService;
|
||||
}
|
||||
|
||||
public MimeFileService(INativeAppService nativeAppService)
|
||||
{
|
||||
_nativeAppService = nativeAppService;
|
||||
}
|
||||
public async Task<MimeMessageInformation> GetMimeMessageInformationAsync(Guid fileId, Guid accountId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var resourcePath = await GetMimeResourcePathAsync(accountId, fileId).ConfigureAwait(false);
|
||||
var mimeFilePath = GetEMLPath(resourcePath);
|
||||
|
||||
public async Task<MimeMessageInformation> GetMimeMessageInformationAsync(Guid fileId, Guid accountId, CancellationToken cancellationToken = default)
|
||||
var loadedMimeMessage = await MimeMessage.LoadAsync(mimeFilePath, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return new MimeMessageInformation(loadedMimeMessage, resourcePath);
|
||||
}
|
||||
|
||||
public async Task<MimeMessageInformation> GetMimeMessageInformationAsync(byte[] fileBytes, string emlDirectoryPath, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var memoryStream = new MemoryStream(fileBytes);
|
||||
|
||||
var loadedMimeMessage = await MimeMessage.LoadAsync(memoryStream, cancellationToken).ConfigureAwait(false);
|
||||
return new MimeMessageInformation(loadedMimeMessage, emlDirectoryPath);
|
||||
}
|
||||
|
||||
public async Task<bool> SaveMimeMessageAsync(Guid fileId, MimeMessage mimeMessage, Guid accountId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var resourcePath = await GetMimeResourcePathAsync(accountId, fileId).ConfigureAwait(false);
|
||||
var mimeFilePath = GetEMLPath(resourcePath);
|
||||
var completeFilePath = GetEMLPath(resourcePath);
|
||||
|
||||
var loadedMimeMessage = await MimeMessage.LoadAsync(mimeFilePath, cancellationToken).ConfigureAwait(false);
|
||||
using var fileStream = File.Open(completeFilePath, FileMode.OpenOrCreate);
|
||||
|
||||
return new MimeMessageInformation(loadedMimeMessage, resourcePath);
|
||||
await mimeMessage.WriteToAsync(fileStream).ConfigureAwait(false);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<MimeMessageInformation> GetMimeMessageInformationAsync(byte[] fileBytes, string emlDirectoryPath, CancellationToken cancellationToken = default)
|
||||
catch (Exception ex)
|
||||
{
|
||||
var memoryStream = new MemoryStream(fileBytes);
|
||||
|
||||
var loadedMimeMessage = await MimeMessage.LoadAsync(memoryStream, cancellationToken).ConfigureAwait(false);
|
||||
return new MimeMessageInformation(loadedMimeMessage, emlDirectoryPath);
|
||||
_logger.Error(ex, "Could not save mime file for FileId: {FileId}", fileId);
|
||||
}
|
||||
|
||||
public async Task<bool> SaveMimeMessageAsync(Guid fileId, MimeMessage mimeMessage, Guid accountId)
|
||||
return false;
|
||||
}
|
||||
|
||||
private string GetEMLPath(string resourcePath) => $"{resourcePath}\\mail.eml";
|
||||
|
||||
public async Task<string> GetMimeResourcePathAsync(Guid accountId, Guid fileId)
|
||||
{
|
||||
var mimeFolderPath = await _nativeAppService.GetMimeMessageStoragePath().ConfigureAwait(false);
|
||||
var mimeDirectory = Path.Combine(mimeFolderPath, accountId.ToString(), fileId.ToString());
|
||||
|
||||
if (!Directory.Exists(mimeDirectory))
|
||||
Directory.CreateDirectory(mimeDirectory);
|
||||
|
||||
return mimeDirectory;
|
||||
}
|
||||
|
||||
public async Task<bool> IsMimeExistAsync(Guid accountId, Guid fileId)
|
||||
{
|
||||
var resourcePath = await GetMimeResourcePathAsync(accountId, fileId);
|
||||
var completeFilePath = GetEMLPath(resourcePath);
|
||||
|
||||
return File.Exists(completeFilePath);
|
||||
}
|
||||
|
||||
public HtmlPreviewVisitor CreateHTMLPreviewVisitor(MimeMessage message, string mimeLocalPath)
|
||||
{
|
||||
var visitor = new HtmlPreviewVisitor(mimeLocalPath);
|
||||
|
||||
message.Accept(visitor);
|
||||
|
||||
// TODO: Match cid with attachments if any.
|
||||
|
||||
return visitor;
|
||||
}
|
||||
|
||||
public async Task<bool> DeleteMimeMessageAsync(Guid accountId, Guid fileId)
|
||||
{
|
||||
var resourcePath = await GetMimeResourcePathAsync(accountId, fileId);
|
||||
var completeFilePath = GetEMLPath(resourcePath);
|
||||
|
||||
if (File.Exists(completeFilePath))
|
||||
{
|
||||
try
|
||||
{
|
||||
var resourcePath = await GetMimeResourcePathAsync(accountId, fileId).ConfigureAwait(false);
|
||||
var completeFilePath = GetEMLPath(resourcePath);
|
||||
File.Delete(completeFilePath);
|
||||
|
||||
using var fileStream = File.Open(completeFilePath, FileMode.OpenOrCreate);
|
||||
|
||||
await mimeMessage.WriteToAsync(fileStream).ConfigureAwait(false);
|
||||
_logger.Information("Mime file deleted for {FileId}", fileId);
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, "Could not save mime file for FileId: {FileId}", fileId);
|
||||
_logger.Error(ex, "Could not delete mime file for {FileId}", fileId);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private string GetEMLPath(string resourcePath) => $"{resourcePath}\\mail.eml";
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<string> GetMimeResourcePathAsync(Guid accountId, Guid fileId)
|
||||
public MailRenderModel GetMailRenderModel(MimeMessage message, string mimeLocalPath, MailRenderingOptions options = null)
|
||||
{
|
||||
var visitor = CreateHTMLPreviewVisitor(message, mimeLocalPath);
|
||||
|
||||
string finalRenderHtml = visitor.HtmlBody;
|
||||
|
||||
// Check whether we need to purify the generated HTML from visitor.
|
||||
// No need to create HtmlDocument if not required.
|
||||
|
||||
if (options != null && options.IsPurifyingNeeded())
|
||||
{
|
||||
var mimeFolderPath = await _nativeAppService.GetMimeMessageStoragePath().ConfigureAwait(false);
|
||||
var mimeDirectory = Path.Combine(mimeFolderPath, accountId.ToString(), fileId.ToString());
|
||||
var document = new HtmlAgilityPack.HtmlDocument();
|
||||
document.LoadHtml(visitor.HtmlBody);
|
||||
|
||||
if (!Directory.Exists(mimeDirectory))
|
||||
Directory.CreateDirectory(mimeDirectory);
|
||||
// Clear <img> src attribute.
|
||||
|
||||
return mimeDirectory;
|
||||
if (!options.LoadImages)
|
||||
document.ClearImages();
|
||||
|
||||
if (!options.LoadStyles)
|
||||
document.ClearStyles();
|
||||
|
||||
// Update final HTML.
|
||||
finalRenderHtml = document.DocumentNode.OuterHtml;
|
||||
}
|
||||
|
||||
public async Task<bool> IsMimeExistAsync(Guid accountId, Guid fileId)
|
||||
var renderingModel = new MailRenderModel(finalRenderHtml, options);
|
||||
|
||||
// Create attachments.
|
||||
|
||||
foreach (var attachment in visitor.Attachments)
|
||||
{
|
||||
var resourcePath = await GetMimeResourcePathAsync(accountId, fileId);
|
||||
var completeFilePath = GetEMLPath(resourcePath);
|
||||
|
||||
return File.Exists(completeFilePath);
|
||||
}
|
||||
|
||||
public HtmlPreviewVisitor CreateHTMLPreviewVisitor(MimeMessage message, string mimeLocalPath)
|
||||
{
|
||||
var visitor = new HtmlPreviewVisitor(mimeLocalPath);
|
||||
|
||||
message.Accept(visitor);
|
||||
|
||||
// TODO: Match cid with attachments if any.
|
||||
|
||||
return visitor;
|
||||
}
|
||||
|
||||
public async Task<bool> DeleteMimeMessageAsync(Guid accountId, Guid fileId)
|
||||
{
|
||||
var resourcePath = await GetMimeResourcePathAsync(accountId, fileId);
|
||||
var completeFilePath = GetEMLPath(resourcePath);
|
||||
|
||||
if (File.Exists(completeFilePath))
|
||||
if (attachment.IsAttachment && attachment is MimePart attachmentPart)
|
||||
{
|
||||
try
|
||||
{
|
||||
File.Delete(completeFilePath);
|
||||
|
||||
_logger.Information("Mime file deleted for {FileId}", fileId);
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, "Could not delete mime file for {FileId}", fileId);
|
||||
}
|
||||
|
||||
return false;
|
||||
renderingModel.Attachments.Add(attachmentPart);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public MailRenderModel GetMailRenderModel(MimeMessage message, string mimeLocalPath, MailRenderingOptions options = null)
|
||||
if (message.Headers.Contains(HeaderId.ListUnsubscribe))
|
||||
{
|
||||
var visitor = CreateHTMLPreviewVisitor(message, mimeLocalPath);
|
||||
var unsubscribeLinks = message.Headers[HeaderId.ListUnsubscribe]
|
||||
.Normalize()
|
||||
.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
|
||||
.Select(x => x.Trim([' ', '<', '>']));
|
||||
|
||||
string finalRenderHtml = visitor.HtmlBody;
|
||||
|
||||
// Check whether we need to purify the generated HTML from visitor.
|
||||
// No need to create HtmlDocument if not required.
|
||||
|
||||
if (options != null && options.IsPurifyingNeeded())
|
||||
// Only two types of unsubscribe links are possible.
|
||||
// So each has it's own property to simplify the usage.
|
||||
renderingModel.UnsubscribeInfo = new UnsubscribeInfo()
|
||||
{
|
||||
var document = new HtmlAgilityPack.HtmlDocument();
|
||||
document.LoadHtml(visitor.HtmlBody);
|
||||
|
||||
// Clear <img> src attribute.
|
||||
|
||||
if (!options.LoadImages)
|
||||
document.ClearImages();
|
||||
|
||||
if (!options.LoadStyles)
|
||||
document.ClearStyles();
|
||||
|
||||
// Update final HTML.
|
||||
finalRenderHtml = document.DocumentNode.OuterHtml;
|
||||
}
|
||||
|
||||
var renderingModel = new MailRenderModel(finalRenderHtml, options);
|
||||
|
||||
// Create attachments.
|
||||
|
||||
foreach (var attachment in visitor.Attachments)
|
||||
{
|
||||
if (attachment.IsAttachment && attachment is MimePart attachmentPart)
|
||||
{
|
||||
renderingModel.Attachments.Add(attachmentPart);
|
||||
}
|
||||
}
|
||||
|
||||
if (message.Headers.Contains(HeaderId.ListUnsubscribe))
|
||||
{
|
||||
var unsubscribeLinks = message.Headers[HeaderId.ListUnsubscribe]
|
||||
.Normalize()
|
||||
.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
|
||||
.Select(x => x.Trim([' ', '<', '>']));
|
||||
|
||||
// Only two types of unsubscribe links are possible.
|
||||
// So each has it's own property to simplify the usage.
|
||||
renderingModel.UnsubscribeInfo = new UnsubscribeInfo()
|
||||
{
|
||||
HttpLink = unsubscribeLinks.FirstOrDefault(x => x.StartsWith("http", StringComparison.OrdinalIgnoreCase)),
|
||||
MailToLink = unsubscribeLinks.FirstOrDefault(x => x.StartsWith("mailto", StringComparison.OrdinalIgnoreCase)),
|
||||
IsOneClick = message.Headers.Contains(HeaderId.ListUnsubscribePost)
|
||||
};
|
||||
}
|
||||
|
||||
return renderingModel;
|
||||
HttpLink = unsubscribeLinks.FirstOrDefault(x => x.StartsWith("http", StringComparison.OrdinalIgnoreCase)),
|
||||
MailToLink = unsubscribeLinks.FirstOrDefault(x => x.StartsWith("mailto", StringComparison.OrdinalIgnoreCase)),
|
||||
IsOneClick = message.Headers.Contains(HeaderId.ListUnsubscribePost)
|
||||
};
|
||||
}
|
||||
|
||||
return renderingModel;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,34 +5,33 @@ using Microsoft.ApplicationInsights.DataContracts;
|
||||
using Serilog.Events;
|
||||
using Serilog.Sinks.ApplicationInsights.TelemetryConverters;
|
||||
|
||||
namespace Wino.Services.Misc
|
||||
namespace Wino.Services.Misc;
|
||||
|
||||
internal class WinoTelemetryConverter : EventTelemetryConverter
|
||||
{
|
||||
internal class WinoTelemetryConverter : EventTelemetryConverter
|
||||
private readonly string _userDiagnosticId;
|
||||
|
||||
public WinoTelemetryConverter(string userDiagnosticId)
|
||||
{
|
||||
private readonly string _userDiagnosticId;
|
||||
_userDiagnosticId = userDiagnosticId;
|
||||
}
|
||||
|
||||
public WinoTelemetryConverter(string userDiagnosticId)
|
||||
public override IEnumerable<ITelemetry> Convert(LogEvent logEvent, IFormatProvider formatProvider)
|
||||
{
|
||||
foreach (ITelemetry telemetry in base.Convert(logEvent, formatProvider))
|
||||
{
|
||||
_userDiagnosticId = userDiagnosticId;
|
||||
}
|
||||
// Assign diagnostic id as user id.
|
||||
telemetry.Context.User.Id = _userDiagnosticId;
|
||||
|
||||
public override IEnumerable<ITelemetry> Convert(LogEvent logEvent, IFormatProvider formatProvider)
|
||||
{
|
||||
foreach (ITelemetry telemetry in base.Convert(logEvent, formatProvider))
|
||||
{
|
||||
// Assign diagnostic id as user id.
|
||||
telemetry.Context.User.Id = _userDiagnosticId;
|
||||
|
||||
yield return telemetry;
|
||||
}
|
||||
}
|
||||
|
||||
public override void ForwardPropertiesToTelemetryProperties(LogEvent logEvent, ISupportProperties telemetryProperties, IFormatProvider formatProvider)
|
||||
{
|
||||
ForwardPropertiesToTelemetryProperties(logEvent, telemetryProperties, formatProvider,
|
||||
includeLogLevel: true,
|
||||
includeRenderedMessage: true,
|
||||
includeMessageTemplate: false);
|
||||
yield return telemetry;
|
||||
}
|
||||
}
|
||||
|
||||
public override void ForwardPropertiesToTelemetryProperties(LogEvent logEvent, ISupportProperties telemetryProperties, IFormatProvider formatProvider)
|
||||
{
|
||||
ForwardPropertiesToTelemetryProperties(logEvent, telemetryProperties, formatProvider,
|
||||
includeLogLevel: true,
|
||||
includeRenderedMessage: true,
|
||||
includeMessageTemplate: false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,64 +1,63 @@
|
||||
using System.Collections.Generic;
|
||||
using Wino.Core.Domain.Enums;
|
||||
|
||||
namespace Wino.Services
|
||||
namespace Wino.Services;
|
||||
|
||||
public static class ServiceConstants
|
||||
{
|
||||
public static class ServiceConstants
|
||||
#region Gmail Constants
|
||||
|
||||
public const string INBOX_LABEL_ID = "INBOX";
|
||||
public const string UNREAD_LABEL_ID = "UNREAD";
|
||||
public const string IMPORTANT_LABEL_ID = "IMPORTANT";
|
||||
public const string STARRED_LABEL_ID = "STARRED";
|
||||
public const string DRAFT_LABEL_ID = "DRAFT";
|
||||
public const string SENT_LABEL_ID = "SENT";
|
||||
public const string SPAM_LABEL_ID = "SPAM";
|
||||
public const string CHAT_LABEL_ID = "CHAT";
|
||||
public const string TRASH_LABEL_ID = "TRASH";
|
||||
|
||||
// Category labels.
|
||||
public const string FORUMS_LABEL_ID = "FORUMS";
|
||||
public const string UPDATES_LABEL_ID = "UPDATES";
|
||||
public const string PROMOTIONS_LABEL_ID = "PROMOTIONS";
|
||||
public const string SOCIAL_LABEL_ID = "SOCIAL";
|
||||
public const string PERSONAL_LABEL_ID = "PERSONAL";
|
||||
|
||||
// Label visibility identifiers.
|
||||
public const string SYSTEM_FOLDER_IDENTIFIER = "system";
|
||||
public const string FOLDER_HIDE_IDENTIFIER = "labelHide";
|
||||
|
||||
public const string CATEGORY_PREFIX = "CATEGORY_";
|
||||
public const string FOLDER_SEPERATOR_STRING = "/";
|
||||
public const char FOLDER_SEPERATOR_CHAR = '/';
|
||||
|
||||
public static Dictionary<string, SpecialFolderType> KnownFolderDictionary = new Dictionary<string, SpecialFolderType>()
|
||||
{
|
||||
#region Gmail Constants
|
||||
{ INBOX_LABEL_ID, SpecialFolderType.Inbox },
|
||||
{ CHAT_LABEL_ID, SpecialFolderType.Chat },
|
||||
{ IMPORTANT_LABEL_ID, SpecialFolderType.Important },
|
||||
{ TRASH_LABEL_ID, SpecialFolderType.Deleted },
|
||||
{ DRAFT_LABEL_ID, SpecialFolderType.Draft },
|
||||
{ SENT_LABEL_ID, SpecialFolderType.Sent },
|
||||
{ SPAM_LABEL_ID, SpecialFolderType.Junk },
|
||||
{ STARRED_LABEL_ID, SpecialFolderType.Starred },
|
||||
{ UNREAD_LABEL_ID, SpecialFolderType.Unread },
|
||||
{ FORUMS_LABEL_ID, SpecialFolderType.Forums },
|
||||
{ UPDATES_LABEL_ID, SpecialFolderType.Updates },
|
||||
{ PROMOTIONS_LABEL_ID, SpecialFolderType.Promotions },
|
||||
{ SOCIAL_LABEL_ID, SpecialFolderType.Social},
|
||||
{ PERSONAL_LABEL_ID, SpecialFolderType.Personal},
|
||||
};
|
||||
|
||||
public const string INBOX_LABEL_ID = "INBOX";
|
||||
public const string UNREAD_LABEL_ID = "UNREAD";
|
||||
public const string IMPORTANT_LABEL_ID = "IMPORTANT";
|
||||
public const string STARRED_LABEL_ID = "STARRED";
|
||||
public const string DRAFT_LABEL_ID = "DRAFT";
|
||||
public const string SENT_LABEL_ID = "SENT";
|
||||
public const string SPAM_LABEL_ID = "SPAM";
|
||||
public const string CHAT_LABEL_ID = "CHAT";
|
||||
public const string TRASH_LABEL_ID = "TRASH";
|
||||
public static string[] SubCategoryFolderLabelIds =
|
||||
[
|
||||
FORUMS_LABEL_ID,
|
||||
UPDATES_LABEL_ID,
|
||||
PROMOTIONS_LABEL_ID,
|
||||
SOCIAL_LABEL_ID,
|
||||
PERSONAL_LABEL_ID
|
||||
];
|
||||
|
||||
// Category labels.
|
||||
public const string FORUMS_LABEL_ID = "FORUMS";
|
||||
public const string UPDATES_LABEL_ID = "UPDATES";
|
||||
public const string PROMOTIONS_LABEL_ID = "PROMOTIONS";
|
||||
public const string SOCIAL_LABEL_ID = "SOCIAL";
|
||||
public const string PERSONAL_LABEL_ID = "PERSONAL";
|
||||
|
||||
// Label visibility identifiers.
|
||||
public const string SYSTEM_FOLDER_IDENTIFIER = "system";
|
||||
public const string FOLDER_HIDE_IDENTIFIER = "labelHide";
|
||||
|
||||
public const string CATEGORY_PREFIX = "CATEGORY_";
|
||||
public const string FOLDER_SEPERATOR_STRING = "/";
|
||||
public const char FOLDER_SEPERATOR_CHAR = '/';
|
||||
|
||||
public static Dictionary<string, SpecialFolderType> KnownFolderDictionary = new Dictionary<string, SpecialFolderType>()
|
||||
{
|
||||
{ INBOX_LABEL_ID, SpecialFolderType.Inbox },
|
||||
{ CHAT_LABEL_ID, SpecialFolderType.Chat },
|
||||
{ IMPORTANT_LABEL_ID, SpecialFolderType.Important },
|
||||
{ TRASH_LABEL_ID, SpecialFolderType.Deleted },
|
||||
{ DRAFT_LABEL_ID, SpecialFolderType.Draft },
|
||||
{ SENT_LABEL_ID, SpecialFolderType.Sent },
|
||||
{ SPAM_LABEL_ID, SpecialFolderType.Junk },
|
||||
{ STARRED_LABEL_ID, SpecialFolderType.Starred },
|
||||
{ UNREAD_LABEL_ID, SpecialFolderType.Unread },
|
||||
{ FORUMS_LABEL_ID, SpecialFolderType.Forums },
|
||||
{ UPDATES_LABEL_ID, SpecialFolderType.Updates },
|
||||
{ PROMOTIONS_LABEL_ID, SpecialFolderType.Promotions },
|
||||
{ SOCIAL_LABEL_ID, SpecialFolderType.Social},
|
||||
{ PERSONAL_LABEL_ID, SpecialFolderType.Personal},
|
||||
};
|
||||
|
||||
public static string[] SubCategoryFolderLabelIds =
|
||||
[
|
||||
FORUMS_LABEL_ID,
|
||||
UPDATES_LABEL_ID,
|
||||
PROMOTIONS_LABEL_ID,
|
||||
SOCIAL_LABEL_ID,
|
||||
PERSONAL_LABEL_ID
|
||||
];
|
||||
|
||||
#endregion
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
|
||||
@@ -2,35 +2,34 @@
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Services.Threading;
|
||||
|
||||
namespace Wino.Services
|
||||
namespace Wino.Services;
|
||||
|
||||
public static class ServicesContainerSetup
|
||||
{
|
||||
public static class ServicesContainerSetup
|
||||
public static void RegisterSharedServices(this IServiceCollection services)
|
||||
{
|
||||
public static void RegisterSharedServices(this IServiceCollection services)
|
||||
{
|
||||
services.AddSingleton<ITranslationService, TranslationService>();
|
||||
services.AddSingleton<IDatabaseService, DatabaseService>();
|
||||
services.AddSingleton<ITranslationService, TranslationService>();
|
||||
services.AddSingleton<IDatabaseService, DatabaseService>();
|
||||
|
||||
services.AddSingleton<IApplicationConfiguration, ApplicationConfiguration>();
|
||||
services.AddSingleton<ILogInitializer, LogInitializer>();
|
||||
services.AddSingleton<ILaunchProtocolService, LaunchProtocolService>();
|
||||
services.AddSingleton<IMimeFileService, MimeFileService>();
|
||||
services.AddSingleton<IApplicationConfiguration, ApplicationConfiguration>();
|
||||
services.AddSingleton<ILogInitializer, LogInitializer>();
|
||||
services.AddSingleton<ILaunchProtocolService, LaunchProtocolService>();
|
||||
services.AddSingleton<IMimeFileService, MimeFileService>();
|
||||
|
||||
services.AddTransient<ICalendarService, CalendarService>();
|
||||
services.AddTransient<IMailService, MailService>();
|
||||
services.AddTransient<IFolderService, FolderService>();
|
||||
services.AddTransient<IAccountService, AccountService>();
|
||||
services.AddTransient<IContactService, ContactService>();
|
||||
services.AddTransient<ISignatureService, SignatureService>();
|
||||
services.AddTransient<IContextMenuItemService, ContextMenuItemService>();
|
||||
services.AddTransient<ISpecialImapProviderConfigResolver, SpecialImapProviderConfigResolver>();
|
||||
services.AddTransient<ICalendarService, CalendarService>();
|
||||
services.AddTransient<IMailService, MailService>();
|
||||
services.AddTransient<IFolderService, FolderService>();
|
||||
services.AddTransient<IAccountService, AccountService>();
|
||||
services.AddTransient<IContactService, ContactService>();
|
||||
services.AddTransient<ISignatureService, SignatureService>();
|
||||
services.AddTransient<IContextMenuItemService, ContextMenuItemService>();
|
||||
services.AddTransient<ISpecialImapProviderConfigResolver, SpecialImapProviderConfigResolver>();
|
||||
|
||||
services.AddSingleton<IThreadingStrategyProvider, ThreadingStrategyProvider>();
|
||||
services.AddTransient<IOutlookThreadingStrategy, OutlookThreadingStrategy>();
|
||||
services.AddTransient<IGmailThreadingStrategy, GmailThreadingStrategy>();
|
||||
services.AddTransient<IImapThreadingStrategy, ImapThreadingStrategy>();
|
||||
services.AddSingleton<IThreadingStrategyProvider, ThreadingStrategyProvider>();
|
||||
services.AddTransient<IOutlookThreadingStrategy, OutlookThreadingStrategy>();
|
||||
services.AddTransient<IGmailThreadingStrategy, GmailThreadingStrategy>();
|
||||
services.AddTransient<IImapThreadingStrategy, ImapThreadingStrategy>();
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,55 +4,54 @@ using System.Threading.Tasks;
|
||||
using Wino.Core.Domain.Entities.Mail;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
|
||||
namespace Wino.Services
|
||||
namespace Wino.Services;
|
||||
|
||||
public class SignatureService(IDatabaseService databaseService) : BaseDatabaseService(databaseService), ISignatureService
|
||||
{
|
||||
public class SignatureService(IDatabaseService databaseService) : BaseDatabaseService(databaseService), ISignatureService
|
||||
public async Task<AccountSignature> GetSignatureAsync(Guid signatureId)
|
||||
{
|
||||
public async Task<AccountSignature> GetSignatureAsync(Guid signatureId)
|
||||
return await Connection.Table<AccountSignature>().FirstAsync(s => s.Id == signatureId);
|
||||
}
|
||||
|
||||
public async Task<List<AccountSignature>> GetSignaturesAsync(Guid accountId)
|
||||
{
|
||||
return await Connection.Table<AccountSignature>().Where(s => s.MailAccountId == accountId).ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<AccountSignature> CreateSignatureAsync(AccountSignature signature)
|
||||
{
|
||||
await Connection.InsertAsync(signature);
|
||||
|
||||
return signature;
|
||||
}
|
||||
|
||||
public async Task<AccountSignature> CreateDefaultSignatureAsync(Guid accountId)
|
||||
{
|
||||
var defaultSignature = new AccountSignature()
|
||||
{
|
||||
return await Connection.Table<AccountSignature>().FirstAsync(s => s.Id == signatureId);
|
||||
}
|
||||
Id = Guid.NewGuid(),
|
||||
MailAccountId = accountId,
|
||||
// TODO: Should be translated?
|
||||
Name = "Wino Default Signature",
|
||||
HtmlBody = @"<p>Sent from <a href=""https://github.com/bkaankose/Wino-Mail/"">Wino Mail</a> for Windows</p>"
|
||||
};
|
||||
|
||||
public async Task<List<AccountSignature>> GetSignaturesAsync(Guid accountId)
|
||||
{
|
||||
return await Connection.Table<AccountSignature>().Where(s => s.MailAccountId == accountId).ToListAsync();
|
||||
}
|
||||
await Connection.InsertAsync(defaultSignature);
|
||||
|
||||
public async Task<AccountSignature> CreateSignatureAsync(AccountSignature signature)
|
||||
{
|
||||
await Connection.InsertAsync(signature);
|
||||
return defaultSignature;
|
||||
}
|
||||
|
||||
return signature;
|
||||
}
|
||||
public async Task<AccountSignature> UpdateSignatureAsync(AccountSignature signature)
|
||||
{
|
||||
await Connection.UpdateAsync(signature);
|
||||
|
||||
public async Task<AccountSignature> CreateDefaultSignatureAsync(Guid accountId)
|
||||
{
|
||||
var defaultSignature = new AccountSignature()
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
MailAccountId = accountId,
|
||||
// TODO: Should be translated?
|
||||
Name = "Wino Default Signature",
|
||||
HtmlBody = @"<p>Sent from <a href=""https://github.com/bkaankose/Wino-Mail/"">Wino Mail</a> for Windows</p>"
|
||||
};
|
||||
return signature;
|
||||
}
|
||||
|
||||
await Connection.InsertAsync(defaultSignature);
|
||||
public async Task<AccountSignature> DeleteSignatureAsync(AccountSignature signature)
|
||||
{
|
||||
await Connection.DeleteAsync(signature);
|
||||
|
||||
return defaultSignature;
|
||||
}
|
||||
|
||||
public async Task<AccountSignature> UpdateSignatureAsync(AccountSignature signature)
|
||||
{
|
||||
await Connection.UpdateAsync(signature);
|
||||
|
||||
return signature;
|
||||
}
|
||||
|
||||
public async Task<AccountSignature> DeleteSignatureAsync(AccountSignature signature)
|
||||
{
|
||||
await Connection.DeleteAsync(signature);
|
||||
|
||||
return signature;
|
||||
}
|
||||
return signature;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,66 +3,65 @@ using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Core.Domain.Models.Accounts;
|
||||
|
||||
namespace Wino.Services
|
||||
namespace Wino.Services;
|
||||
|
||||
public class SpecialImapProviderConfigResolver : ISpecialImapProviderConfigResolver
|
||||
{
|
||||
public class SpecialImapProviderConfigResolver : ISpecialImapProviderConfigResolver
|
||||
private readonly CustomServerInformation iCloudServerConfig = new CustomServerInformation()
|
||||
{
|
||||
private readonly CustomServerInformation iCloudServerConfig = new CustomServerInformation()
|
||||
IncomingServer = "imap.mail.me.com",
|
||||
IncomingServerPort = "993",
|
||||
IncomingServerType = CustomIncomingServerType.IMAP4,
|
||||
IncomingServerSocketOption = ImapConnectionSecurity.Auto,
|
||||
IncomingAuthenticationMethod = ImapAuthenticationMethod.Auto,
|
||||
OutgoingServer = "smtp.mail.me.com",
|
||||
OutgoingServerPort = "587",
|
||||
OutgoingServerSocketOption = ImapConnectionSecurity.Auto,
|
||||
OutgoingAuthenticationMethod = ImapAuthenticationMethod.Auto,
|
||||
MaxConcurrentClients = 5,
|
||||
};
|
||||
|
||||
private readonly CustomServerInformation yahooServerConfig = new CustomServerInformation()
|
||||
{
|
||||
IncomingServer = "imap.mail.yahoo.com",
|
||||
IncomingServerPort = "993",
|
||||
IncomingServerType = CustomIncomingServerType.IMAP4,
|
||||
IncomingServerSocketOption = ImapConnectionSecurity.Auto,
|
||||
IncomingAuthenticationMethod = ImapAuthenticationMethod.Auto,
|
||||
OutgoingServer = "smtp.mail.yahoo.com",
|
||||
OutgoingServerPort = "587",
|
||||
OutgoingServerSocketOption = ImapConnectionSecurity.Auto,
|
||||
OutgoingAuthenticationMethod = ImapAuthenticationMethod.Auto,
|
||||
MaxConcurrentClients = 5,
|
||||
};
|
||||
|
||||
|
||||
public CustomServerInformation GetServerInformation(MailAccount account, AccountCreationDialogResult dialogResult)
|
||||
{
|
||||
CustomServerInformation resolvedConfig = null;
|
||||
|
||||
if (dialogResult.SpecialImapProviderDetails.SpecialImapProvider == SpecialImapProvider.iCloud)
|
||||
{
|
||||
IncomingServer = "imap.mail.me.com",
|
||||
IncomingServerPort = "993",
|
||||
IncomingServerType = CustomIncomingServerType.IMAP4,
|
||||
IncomingServerSocketOption = ImapConnectionSecurity.Auto,
|
||||
IncomingAuthenticationMethod = ImapAuthenticationMethod.Auto,
|
||||
OutgoingServer = "smtp.mail.me.com",
|
||||
OutgoingServerPort = "587",
|
||||
OutgoingServerSocketOption = ImapConnectionSecurity.Auto,
|
||||
OutgoingAuthenticationMethod = ImapAuthenticationMethod.Auto,
|
||||
MaxConcurrentClients = 5,
|
||||
};
|
||||
resolvedConfig = iCloudServerConfig;
|
||||
|
||||
private readonly CustomServerInformation yahooServerConfig = new CustomServerInformation()
|
||||
{
|
||||
IncomingServer = "imap.mail.yahoo.com",
|
||||
IncomingServerPort = "993",
|
||||
IncomingServerType = CustomIncomingServerType.IMAP4,
|
||||
IncomingServerSocketOption = ImapConnectionSecurity.Auto,
|
||||
IncomingAuthenticationMethod = ImapAuthenticationMethod.Auto,
|
||||
OutgoingServer = "smtp.mail.yahoo.com",
|
||||
OutgoingServerPort = "587",
|
||||
OutgoingServerSocketOption = ImapConnectionSecurity.Auto,
|
||||
OutgoingAuthenticationMethod = ImapAuthenticationMethod.Auto,
|
||||
MaxConcurrentClients = 5,
|
||||
};
|
||||
|
||||
|
||||
public CustomServerInformation GetServerInformation(MailAccount account, AccountCreationDialogResult dialogResult)
|
||||
{
|
||||
CustomServerInformation resolvedConfig = null;
|
||||
|
||||
if (dialogResult.SpecialImapProviderDetails.SpecialImapProvider == SpecialImapProvider.iCloud)
|
||||
{
|
||||
resolvedConfig = iCloudServerConfig;
|
||||
|
||||
// iCloud takes username before the @icloud part for incoming, but full address as outgoing.
|
||||
resolvedConfig.IncomingServerUsername = dialogResult.SpecialImapProviderDetails.Address.Split('@')[0];
|
||||
resolvedConfig.OutgoingServerUsername = dialogResult.SpecialImapProviderDetails.Address;
|
||||
}
|
||||
else if (dialogResult.SpecialImapProviderDetails.SpecialImapProvider == SpecialImapProvider.Yahoo)
|
||||
{
|
||||
resolvedConfig = yahooServerConfig;
|
||||
|
||||
// Yahoo uses full address for both incoming and outgoing.
|
||||
resolvedConfig.IncomingServerUsername = dialogResult.SpecialImapProviderDetails.Address;
|
||||
resolvedConfig.OutgoingServerUsername = dialogResult.SpecialImapProviderDetails.Address;
|
||||
}
|
||||
|
||||
// Fill in account details.
|
||||
resolvedConfig.Address = dialogResult.SpecialImapProviderDetails.Address;
|
||||
resolvedConfig.IncomingServerPassword = dialogResult.SpecialImapProviderDetails.Password;
|
||||
resolvedConfig.OutgoingServerPassword = dialogResult.SpecialImapProviderDetails.Password;
|
||||
resolvedConfig.DisplayName = dialogResult.SpecialImapProviderDetails.SenderName;
|
||||
return resolvedConfig;
|
||||
// iCloud takes username before the @icloud part for incoming, but full address as outgoing.
|
||||
resolvedConfig.IncomingServerUsername = dialogResult.SpecialImapProviderDetails.Address.Split('@')[0];
|
||||
resolvedConfig.OutgoingServerUsername = dialogResult.SpecialImapProviderDetails.Address;
|
||||
}
|
||||
else if (dialogResult.SpecialImapProviderDetails.SpecialImapProvider == SpecialImapProvider.Yahoo)
|
||||
{
|
||||
resolvedConfig = yahooServerConfig;
|
||||
|
||||
// Yahoo uses full address for both incoming and outgoing.
|
||||
resolvedConfig.IncomingServerUsername = dialogResult.SpecialImapProviderDetails.Address;
|
||||
resolvedConfig.OutgoingServerUsername = dialogResult.SpecialImapProviderDetails.Address;
|
||||
}
|
||||
|
||||
// Fill in account details.
|
||||
resolvedConfig.Address = dialogResult.SpecialImapProviderDetails.Address;
|
||||
resolvedConfig.IncomingServerPassword = dialogResult.SpecialImapProviderDetails.Password;
|
||||
resolvedConfig.OutgoingServerPassword = dialogResult.SpecialImapProviderDetails.Password;
|
||||
resolvedConfig.DisplayName = dialogResult.SpecialImapProviderDetails.SenderName;
|
||||
return resolvedConfig;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,130 +9,129 @@ using Wino.Core.Domain.Models.Folders;
|
||||
using Wino.Core.Domain.Models.MailItem;
|
||||
using Wino.Services;
|
||||
|
||||
namespace Wino.Services.Threading
|
||||
namespace Wino.Services.Threading;
|
||||
|
||||
public class APIThreadingStrategy : IThreadingStrategy
|
||||
{
|
||||
public class APIThreadingStrategy : IThreadingStrategy
|
||||
private readonly IDatabaseService _databaseService;
|
||||
private readonly IFolderService _folderService;
|
||||
|
||||
public APIThreadingStrategy(IDatabaseService databaseService, IFolderService folderService)
|
||||
{
|
||||
private readonly IDatabaseService _databaseService;
|
||||
private readonly IFolderService _folderService;
|
||||
_databaseService = databaseService;
|
||||
_folderService = folderService;
|
||||
}
|
||||
|
||||
public APIThreadingStrategy(IDatabaseService databaseService, IFolderService folderService)
|
||||
public virtual bool ShouldThreadWithItem(IMailItem originalItem, IMailItem targetItem)
|
||||
{
|
||||
return originalItem.ThreadId != null && originalItem.ThreadId == targetItem.ThreadId;
|
||||
}
|
||||
|
||||
///<inheritdoc/>
|
||||
public async Task<List<IMailItem>> ThreadItemsAsync(List<MailCopy> items, IMailItemFolder threadingForFolder)
|
||||
{
|
||||
var assignedAccount = items[0].AssignedAccount;
|
||||
|
||||
var sentFolder = await _folderService.GetSpecialFolderByAccountIdAsync(assignedAccount.Id, SpecialFolderType.Sent);
|
||||
var draftFolder = await _folderService.GetSpecialFolderByAccountIdAsync(assignedAccount.Id, SpecialFolderType.Draft);
|
||||
|
||||
if (sentFolder == null || draftFolder == null) return default;
|
||||
|
||||
// True: Non threaded items.
|
||||
// False: Potentially threaded items.
|
||||
var nonThreadedOrThreadedMails = items
|
||||
.Distinct()
|
||||
.GroupBy(x => string.IsNullOrEmpty(x.ThreadId))
|
||||
.ToDictionary(x => x.Key, x => x);
|
||||
|
||||
_ = nonThreadedOrThreadedMails.TryGetValue(true, out var nonThreadedMails);
|
||||
var isThreadedItems = nonThreadedOrThreadedMails.TryGetValue(false, out var potentiallyThreadedMails);
|
||||
|
||||
List<IMailItem> resultList = nonThreadedMails is null ? [] : [.. nonThreadedMails];
|
||||
|
||||
if (isThreadedItems)
|
||||
{
|
||||
_databaseService = databaseService;
|
||||
_folderService = folderService;
|
||||
}
|
||||
var threadItems = (await GetThreadItemsAsync(potentiallyThreadedMails.Select(x => (x.ThreadId, x.AssignedFolder)).ToList(), assignedAccount.Id, sentFolder.Id, draftFolder.Id))
|
||||
.GroupBy(x => x.ThreadId);
|
||||
|
||||
public virtual bool ShouldThreadWithItem(IMailItem originalItem, IMailItem targetItem)
|
||||
{
|
||||
return originalItem.ThreadId != null && originalItem.ThreadId == targetItem.ThreadId;
|
||||
}
|
||||
|
||||
///<inheritdoc/>
|
||||
public async Task<List<IMailItem>> ThreadItemsAsync(List<MailCopy> items, IMailItemFolder threadingForFolder)
|
||||
{
|
||||
var assignedAccount = items[0].AssignedAccount;
|
||||
|
||||
var sentFolder = await _folderService.GetSpecialFolderByAccountIdAsync(assignedAccount.Id, SpecialFolderType.Sent);
|
||||
var draftFolder = await _folderService.GetSpecialFolderByAccountIdAsync(assignedAccount.Id, SpecialFolderType.Draft);
|
||||
|
||||
if (sentFolder == null || draftFolder == null) return default;
|
||||
|
||||
// True: Non threaded items.
|
||||
// False: Potentially threaded items.
|
||||
var nonThreadedOrThreadedMails = items
|
||||
.Distinct()
|
||||
.GroupBy(x => string.IsNullOrEmpty(x.ThreadId))
|
||||
.ToDictionary(x => x.Key, x => x);
|
||||
|
||||
_ = nonThreadedOrThreadedMails.TryGetValue(true, out var nonThreadedMails);
|
||||
var isThreadedItems = nonThreadedOrThreadedMails.TryGetValue(false, out var potentiallyThreadedMails);
|
||||
|
||||
List<IMailItem> resultList = nonThreadedMails is null ? [] : [.. nonThreadedMails];
|
||||
|
||||
if (isThreadedItems)
|
||||
foreach (var threadItem in threadItems)
|
||||
{
|
||||
var threadItems = (await GetThreadItemsAsync(potentiallyThreadedMails.Select(x => (x.ThreadId, x.AssignedFolder)).ToList(), assignedAccount.Id, sentFolder.Id, draftFolder.Id))
|
||||
.GroupBy(x => x.ThreadId);
|
||||
|
||||
foreach (var threadItem in threadItems)
|
||||
if (threadItem.Count() == 1)
|
||||
{
|
||||
if (threadItem.Count() == 1)
|
||||
resultList.Add(threadItem.First());
|
||||
continue;
|
||||
}
|
||||
|
||||
var thread = new ThreadMailItem();
|
||||
|
||||
foreach (var childThreadItem in threadItem)
|
||||
{
|
||||
if (thread.ThreadItems.Any(a => a.Id == childThreadItem.Id))
|
||||
{
|
||||
resultList.Add(threadItem.First());
|
||||
continue;
|
||||
}
|
||||
// Mail already exist in the thread.
|
||||
// There should be only 1 instance of the mail in the thread.
|
||||
// Make sure we add the correct one.
|
||||
|
||||
var thread = new ThreadMailItem();
|
||||
// Add the one with threading folder.
|
||||
var threadingFolderItem = threadItem.FirstOrDefault(a => a.Id == childThreadItem.Id && a.FolderId == threadingForFolder.Id);
|
||||
|
||||
foreach (var childThreadItem in threadItem)
|
||||
{
|
||||
if (thread.ThreadItems.Any(a => a.Id == childThreadItem.Id))
|
||||
{
|
||||
// Mail already exist in the thread.
|
||||
// There should be only 1 instance of the mail in the thread.
|
||||
// Make sure we add the correct one.
|
||||
if (threadingFolderItem == null) continue;
|
||||
|
||||
// Add the one with threading folder.
|
||||
var threadingFolderItem = threadItem.FirstOrDefault(a => a.Id == childThreadItem.Id && a.FolderId == threadingForFolder.Id);
|
||||
// Remove the existing one.
|
||||
thread.ThreadItems.Remove(thread.ThreadItems.First(a => a.Id == childThreadItem.Id));
|
||||
|
||||
if (threadingFolderItem == null) continue;
|
||||
|
||||
// Remove the existing one.
|
||||
thread.ThreadItems.Remove(thread.ThreadItems.First(a => a.Id == childThreadItem.Id));
|
||||
|
||||
// Add the correct one for listing.
|
||||
thread.AddThreadItem(threadingFolderItem);
|
||||
}
|
||||
else
|
||||
{
|
||||
thread.AddThreadItem(childThreadItem);
|
||||
}
|
||||
}
|
||||
|
||||
if (thread.ThreadItems.Count > 1)
|
||||
{
|
||||
resultList.Add(thread);
|
||||
// Add the correct one for listing.
|
||||
thread.AddThreadItem(threadingFolderItem);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Don't make threads if the thread has only one item.
|
||||
// Gmail has may have multiple assignments for the same item.
|
||||
|
||||
resultList.Add(thread.ThreadItems.First());
|
||||
thread.AddThreadItem(childThreadItem);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return resultList;
|
||||
if (thread.ThreadItems.Count > 1)
|
||||
{
|
||||
resultList.Add(thread);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Don't make threads if the thread has only one item.
|
||||
// Gmail has may have multiple assignments for the same item.
|
||||
|
||||
resultList.Add(thread.ThreadItems.First());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<List<MailCopy>> GetThreadItemsAsync(List<(string threadId, MailItemFolder threadingFolder)> potentialThread,
|
||||
Guid accountId,
|
||||
Guid sentFolderId,
|
||||
Guid draftFolderId)
|
||||
{
|
||||
// Only items from the folder that we are threading for, sent and draft folder items must be included.
|
||||
// This is important because deleted items or item assignments that belongs to different folder is
|
||||
// affecting the thread creation here.
|
||||
return resultList;
|
||||
}
|
||||
|
||||
// If the threading is done from Sent or Draft folder, include everything...
|
||||
private async Task<List<MailCopy>> GetThreadItemsAsync(List<(string threadId, MailItemFolder threadingFolder)> potentialThread,
|
||||
Guid accountId,
|
||||
Guid sentFolderId,
|
||||
Guid draftFolderId)
|
||||
{
|
||||
// Only items from the folder that we are threading for, sent and draft folder items must be included.
|
||||
// This is important because deleted items or item assignments that belongs to different folder is
|
||||
// affecting the thread creation here.
|
||||
|
||||
// TODO: Convert to SQLKata query.
|
||||
// If the threading is done from Sent or Draft folder, include everything...
|
||||
|
||||
var query = @$"SELECT DISTINCT MC.* FROM MailCopy MC
|
||||
// TODO: Convert to SQLKata query.
|
||||
|
||||
var query = @$"SELECT DISTINCT MC.* FROM MailCopy MC
|
||||
INNER JOIN MailItemFolder MF on MF.Id = MC.FolderId
|
||||
WHERE MF.MailAccountId == '{accountId}' AND
|
||||
({string.Join(" OR ", potentialThread.Select(x => ConditionForItem(x, sentFolderId, draftFolderId)))})";
|
||||
|
||||
return await _databaseService.Connection.QueryAsync<MailCopy>(query);
|
||||
return await _databaseService.Connection.QueryAsync<MailCopy>(query);
|
||||
|
||||
static string ConditionForItem((string threadId, MailItemFolder threadingFolder) potentialThread, Guid sentFolderId, Guid draftFolderId)
|
||||
{
|
||||
if (potentialThread.threadingFolder.SpecialFolderType == SpecialFolderType.Draft || potentialThread.threadingFolder.SpecialFolderType == SpecialFolderType.Sent)
|
||||
return $"(MC.ThreadId = '{potentialThread.threadId}')";
|
||||
static string ConditionForItem((string threadId, MailItemFolder threadingFolder) potentialThread, Guid sentFolderId, Guid draftFolderId)
|
||||
{
|
||||
if (potentialThread.threadingFolder.SpecialFolderType == SpecialFolderType.Draft || potentialThread.threadingFolder.SpecialFolderType == SpecialFolderType.Sent)
|
||||
return $"(MC.ThreadId = '{potentialThread.threadId}')";
|
||||
|
||||
return $"(MC.ThreadId = '{potentialThread.threadId}' AND MC.FolderId IN ('{potentialThread.threadingFolder.Id}','{sentFolderId}','{draftFolderId}'))";
|
||||
}
|
||||
return $"(MC.ThreadId = '{potentialThread.threadId}' AND MC.FolderId IN ('{potentialThread.threadingFolder.Id}','{sentFolderId}','{draftFolderId}'))";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
|
||||
namespace Wino.Services.Threading
|
||||
namespace Wino.Services.Threading;
|
||||
|
||||
public class GmailThreadingStrategy : APIThreadingStrategy, IGmailThreadingStrategy
|
||||
{
|
||||
public class GmailThreadingStrategy : APIThreadingStrategy, IGmailThreadingStrategy
|
||||
{
|
||||
public GmailThreadingStrategy(IDatabaseService databaseService, IFolderService folderService) : base(databaseService, folderService) { }
|
||||
}
|
||||
public GmailThreadingStrategy(IDatabaseService databaseService, IFolderService folderService) : base(databaseService, folderService) { }
|
||||
}
|
||||
|
||||
@@ -10,170 +10,169 @@ using Wino.Core.Domain.Models.Folders;
|
||||
using Wino.Core.Domain.Models.MailItem;
|
||||
using Wino.Services.Extensions;
|
||||
|
||||
namespace Wino.Services.Threading
|
||||
namespace Wino.Services.Threading;
|
||||
|
||||
public class ImapThreadingStrategy : BaseDatabaseService, IImapThreadingStrategy
|
||||
{
|
||||
public class ImapThreadingStrategy : BaseDatabaseService, IImapThreadingStrategy
|
||||
private readonly IFolderService _folderService;
|
||||
|
||||
public ImapThreadingStrategy(IDatabaseService databaseService, IFolderService folderService) : base(databaseService)
|
||||
{
|
||||
private readonly IFolderService _folderService;
|
||||
_folderService = folderService;
|
||||
}
|
||||
|
||||
public ImapThreadingStrategy(IDatabaseService databaseService, IFolderService folderService) : base(databaseService)
|
||||
private Task<MailCopy> GetReplyParentAsync(IMailItem replyItem, Guid accountId, Guid threadingFolderId, Guid sentFolderId, Guid draftFolderId)
|
||||
{
|
||||
if (string.IsNullOrEmpty(replyItem?.MessageId)) return Task.FromResult<MailCopy>(null);
|
||||
|
||||
var query = new Query("MailCopy")
|
||||
.Distinct()
|
||||
.Take(1)
|
||||
.Join("MailItemFolder", "MailItemFolder.Id", "MailCopy.FolderId")
|
||||
.Where("MailItemFolder.MailAccountId", accountId)
|
||||
.WhereIn("MailItemFolder.Id", new List<Guid> { threadingFolderId, sentFolderId, draftFolderId })
|
||||
.Where("MailCopy.MessageId", replyItem.InReplyTo)
|
||||
.WhereNot("MailCopy.Id", replyItem.Id)
|
||||
.Select("MailCopy.*");
|
||||
|
||||
return Connection.FindWithQueryAsync<MailCopy>(query.GetRawQuery());
|
||||
}
|
||||
|
||||
private Task<MailCopy> GetInReplyToReplyAsync(IMailItem originalItem, Guid accountId, Guid threadingFolderId, Guid sentFolderId, Guid draftFolderId)
|
||||
{
|
||||
if (string.IsNullOrEmpty(originalItem?.MessageId)) return Task.FromResult<MailCopy>(null);
|
||||
|
||||
var query = new Query("MailCopy")
|
||||
.Distinct()
|
||||
.Take(1)
|
||||
.Join("MailItemFolder", "MailItemFolder.Id", "MailCopy.FolderId")
|
||||
.WhereNot("MailCopy.Id", originalItem.Id)
|
||||
.Where("MailItemFolder.MailAccountId", accountId)
|
||||
.Where("MailCopy.InReplyTo", originalItem.MessageId)
|
||||
.WhereIn("MailItemFolder.Id", new List<Guid> { threadingFolderId, sentFolderId, draftFolderId })
|
||||
.Select("MailCopy.*");
|
||||
|
||||
var raq = query.GetRawQuery();
|
||||
|
||||
return Connection.FindWithQueryAsync<MailCopy>(query.GetRawQuery());
|
||||
}
|
||||
|
||||
public async Task<List<IMailItem>> ThreadItemsAsync(List<MailCopy> items, IMailItemFolder threadingForFolder)
|
||||
{
|
||||
var threads = new List<ThreadMailItem>();
|
||||
|
||||
var account = items.First().AssignedAccount;
|
||||
var accountId = account.Id;
|
||||
|
||||
// Child -> Parent approach.
|
||||
|
||||
var mailLookupTable = new Dictionary<string, bool>();
|
||||
|
||||
// Fill up the mail lookup table to prevent double thread creation.
|
||||
foreach (var mail in items)
|
||||
if (!mailLookupTable.ContainsKey(mail.Id))
|
||||
mailLookupTable.Add(mail.Id, false);
|
||||
|
||||
var sentFolder = await _folderService.GetSpecialFolderByAccountIdAsync(accountId, SpecialFolderType.Sent);
|
||||
var draftFolder = await _folderService.GetSpecialFolderByAccountIdAsync(accountId, SpecialFolderType.Draft);
|
||||
|
||||
// Threading is not possible. Return items as it is.
|
||||
|
||||
if (sentFolder == null || draftFolder == null) return new List<IMailItem>(items);
|
||||
|
||||
foreach (var replyItem in items)
|
||||
{
|
||||
_folderService = folderService;
|
||||
}
|
||||
if (mailLookupTable[replyItem.Id])
|
||||
continue;
|
||||
|
||||
private Task<MailCopy> GetReplyParentAsync(IMailItem replyItem, Guid accountId, Guid threadingFolderId, Guid sentFolderId, Guid draftFolderId)
|
||||
{
|
||||
if (string.IsNullOrEmpty(replyItem?.MessageId)) return Task.FromResult<MailCopy>(null);
|
||||
mailLookupTable[replyItem.Id] = true;
|
||||
|
||||
var query = new Query("MailCopy")
|
||||
.Distinct()
|
||||
.Take(1)
|
||||
.Join("MailItemFolder", "MailItemFolder.Id", "MailCopy.FolderId")
|
||||
.Where("MailItemFolder.MailAccountId", accountId)
|
||||
.WhereIn("MailItemFolder.Id", new List<Guid> { threadingFolderId, sentFolderId, draftFolderId })
|
||||
.Where("MailCopy.MessageId", replyItem.InReplyTo)
|
||||
.WhereNot("MailCopy.Id", replyItem.Id)
|
||||
.Select("MailCopy.*");
|
||||
var threadItem = new ThreadMailItem();
|
||||
|
||||
return Connection.FindWithQueryAsync<MailCopy>(query.GetRawQuery());
|
||||
}
|
||||
threadItem.AddThreadItem(replyItem);
|
||||
|
||||
private Task<MailCopy> GetInReplyToReplyAsync(IMailItem originalItem, Guid accountId, Guid threadingFolderId, Guid sentFolderId, Guid draftFolderId)
|
||||
{
|
||||
if (string.IsNullOrEmpty(originalItem?.MessageId)) return Task.FromResult<MailCopy>(null);
|
||||
var replyToChild = await GetReplyParentAsync(replyItem, accountId, replyItem.AssignedFolder.Id, sentFolder.Id, draftFolder.Id);
|
||||
|
||||
var query = new Query("MailCopy")
|
||||
.Distinct()
|
||||
.Take(1)
|
||||
.Join("MailItemFolder", "MailItemFolder.Id", "MailCopy.FolderId")
|
||||
.WhereNot("MailCopy.Id", originalItem.Id)
|
||||
.Where("MailItemFolder.MailAccountId", accountId)
|
||||
.Where("MailCopy.InReplyTo", originalItem.MessageId)
|
||||
.WhereIn("MailItemFolder.Id", new List<Guid> { threadingFolderId, sentFolderId, draftFolderId })
|
||||
.Select("MailCopy.*");
|
||||
|
||||
var raq = query.GetRawQuery();
|
||||
|
||||
return Connection.FindWithQueryAsync<MailCopy>(query.GetRawQuery());
|
||||
}
|
||||
|
||||
public async Task<List<IMailItem>> ThreadItemsAsync(List<MailCopy> items, IMailItemFolder threadingForFolder)
|
||||
{
|
||||
var threads = new List<ThreadMailItem>();
|
||||
|
||||
var account = items.First().AssignedAccount;
|
||||
var accountId = account.Id;
|
||||
|
||||
// Child -> Parent approach.
|
||||
|
||||
var mailLookupTable = new Dictionary<string, bool>();
|
||||
|
||||
// Fill up the mail lookup table to prevent double thread creation.
|
||||
foreach (var mail in items)
|
||||
if (!mailLookupTable.ContainsKey(mail.Id))
|
||||
mailLookupTable.Add(mail.Id, false);
|
||||
|
||||
var sentFolder = await _folderService.GetSpecialFolderByAccountIdAsync(accountId, SpecialFolderType.Sent);
|
||||
var draftFolder = await _folderService.GetSpecialFolderByAccountIdAsync(accountId, SpecialFolderType.Draft);
|
||||
|
||||
// Threading is not possible. Return items as it is.
|
||||
|
||||
if (sentFolder == null || draftFolder == null) return new List<IMailItem>(items);
|
||||
|
||||
foreach (var replyItem in items)
|
||||
// Build up
|
||||
while (replyToChild != null)
|
||||
{
|
||||
if (mailLookupTable[replyItem.Id])
|
||||
continue;
|
||||
replyToChild.AssignedAccount = account;
|
||||
|
||||
mailLookupTable[replyItem.Id] = true;
|
||||
if (replyToChild.FolderId == draftFolder.Id)
|
||||
replyToChild.AssignedFolder = draftFolder;
|
||||
|
||||
var threadItem = new ThreadMailItem();
|
||||
if (replyToChild.FolderId == sentFolder.Id)
|
||||
replyToChild.AssignedFolder = sentFolder;
|
||||
|
||||
threadItem.AddThreadItem(replyItem);
|
||||
if (replyToChild.FolderId == replyItem.AssignedFolder.Id)
|
||||
replyToChild.AssignedFolder = replyItem.AssignedFolder;
|
||||
|
||||
var replyToChild = await GetReplyParentAsync(replyItem, accountId, replyItem.AssignedFolder.Id, sentFolder.Id, draftFolder.Id);
|
||||
threadItem.AddThreadItem(replyToChild);
|
||||
|
||||
// Build up
|
||||
while (replyToChild != null)
|
||||
{
|
||||
replyToChild.AssignedAccount = account;
|
||||
if (mailLookupTable.ContainsKey(replyToChild.Id))
|
||||
mailLookupTable[replyToChild.Id] = true;
|
||||
|
||||
if (replyToChild.FolderId == draftFolder.Id)
|
||||
replyToChild.AssignedFolder = draftFolder;
|
||||
|
||||
if (replyToChild.FolderId == sentFolder.Id)
|
||||
replyToChild.AssignedFolder = sentFolder;
|
||||
|
||||
if (replyToChild.FolderId == replyItem.AssignedFolder.Id)
|
||||
replyToChild.AssignedFolder = replyItem.AssignedFolder;
|
||||
|
||||
threadItem.AddThreadItem(replyToChild);
|
||||
|
||||
if (mailLookupTable.ContainsKey(replyToChild.Id))
|
||||
mailLookupTable[replyToChild.Id] = true;
|
||||
|
||||
replyToChild = await GetReplyParentAsync(replyToChild, accountId, replyToChild.AssignedFolder.Id, sentFolder.Id, draftFolder.Id);
|
||||
}
|
||||
|
||||
// Build down
|
||||
var replyToParent = await GetInReplyToReplyAsync(replyItem, accountId, replyItem.AssignedFolder.Id, sentFolder.Id, draftFolder.Id);
|
||||
|
||||
while (replyToParent != null)
|
||||
{
|
||||
replyToParent.AssignedAccount = account;
|
||||
|
||||
if (replyToParent.FolderId == draftFolder.Id)
|
||||
replyToParent.AssignedFolder = draftFolder;
|
||||
|
||||
if (replyToParent.FolderId == sentFolder.Id)
|
||||
replyToParent.AssignedFolder = sentFolder;
|
||||
|
||||
if (replyToParent.FolderId == replyItem.AssignedFolder.Id)
|
||||
replyToParent.AssignedFolder = replyItem.AssignedFolder;
|
||||
|
||||
threadItem.AddThreadItem(replyToParent);
|
||||
|
||||
if (mailLookupTable.ContainsKey(replyToParent.Id))
|
||||
mailLookupTable[replyToParent.Id] = true;
|
||||
|
||||
replyToParent = await GetInReplyToReplyAsync(replyToParent, accountId, replyToParent.AssignedFolder.Id, sentFolder.Id, draftFolder.Id);
|
||||
}
|
||||
|
||||
// It's a thread item.
|
||||
|
||||
if (threadItem.ThreadItems.Count > 1 && !threads.Exists(a => a.Id == threadItem.Id))
|
||||
{
|
||||
threads.Add(threadItem);
|
||||
}
|
||||
else
|
||||
{
|
||||
// False alert. This is not a thread item.
|
||||
mailLookupTable[replyItem.Id] = false;
|
||||
|
||||
// TODO: Here potentially check other algorithms for threading like References.
|
||||
}
|
||||
replyToChild = await GetReplyParentAsync(replyToChild, accountId, replyToChild.AssignedFolder.Id, sentFolder.Id, draftFolder.Id);
|
||||
}
|
||||
|
||||
// At this points all mails in the list belong to single items.
|
||||
// Merge with threads.
|
||||
// Last sorting will be done later on in MailService.
|
||||
// Build down
|
||||
var replyToParent = await GetInReplyToReplyAsync(replyItem, accountId, replyItem.AssignedFolder.Id, sentFolder.Id, draftFolder.Id);
|
||||
|
||||
// Remove single mails that are included in thread.
|
||||
items.RemoveAll(a => mailLookupTable.ContainsKey(a.Id) && mailLookupTable[a.Id]);
|
||||
while (replyToParent != null)
|
||||
{
|
||||
replyToParent.AssignedAccount = account;
|
||||
|
||||
var finalList = new List<IMailItem>(items);
|
||||
if (replyToParent.FolderId == draftFolder.Id)
|
||||
replyToParent.AssignedFolder = draftFolder;
|
||||
|
||||
finalList.AddRange(threads);
|
||||
if (replyToParent.FolderId == sentFolder.Id)
|
||||
replyToParent.AssignedFolder = sentFolder;
|
||||
|
||||
return finalList;
|
||||
if (replyToParent.FolderId == replyItem.AssignedFolder.Id)
|
||||
replyToParent.AssignedFolder = replyItem.AssignedFolder;
|
||||
|
||||
threadItem.AddThreadItem(replyToParent);
|
||||
|
||||
if (mailLookupTable.ContainsKey(replyToParent.Id))
|
||||
mailLookupTable[replyToParent.Id] = true;
|
||||
|
||||
replyToParent = await GetInReplyToReplyAsync(replyToParent, accountId, replyToParent.AssignedFolder.Id, sentFolder.Id, draftFolder.Id);
|
||||
}
|
||||
|
||||
// It's a thread item.
|
||||
|
||||
if (threadItem.ThreadItems.Count > 1 && !threads.Exists(a => a.Id == threadItem.Id))
|
||||
{
|
||||
threads.Add(threadItem);
|
||||
}
|
||||
else
|
||||
{
|
||||
// False alert. This is not a thread item.
|
||||
mailLookupTable[replyItem.Id] = false;
|
||||
|
||||
// TODO: Here potentially check other algorithms for threading like References.
|
||||
}
|
||||
}
|
||||
|
||||
public bool ShouldThreadWithItem(IMailItem originalItem, IMailItem targetItem)
|
||||
{
|
||||
bool isChild = originalItem.InReplyTo != null && originalItem.InReplyTo == targetItem.MessageId;
|
||||
bool isParent = originalItem.MessageId != null && originalItem.MessageId == targetItem.InReplyTo;
|
||||
// At this points all mails in the list belong to single items.
|
||||
// Merge with threads.
|
||||
// Last sorting will be done later on in MailService.
|
||||
|
||||
return isChild || isParent;
|
||||
}
|
||||
// Remove single mails that are included in thread.
|
||||
items.RemoveAll(a => mailLookupTable.ContainsKey(a.Id) && mailLookupTable[a.Id]);
|
||||
|
||||
var finalList = new List<IMailItem>(items);
|
||||
|
||||
finalList.AddRange(threads);
|
||||
|
||||
return finalList;
|
||||
}
|
||||
|
||||
public bool ShouldThreadWithItem(IMailItem originalItem, IMailItem targetItem)
|
||||
{
|
||||
bool isChild = originalItem.InReplyTo != null && originalItem.InReplyTo == targetItem.MessageId;
|
||||
bool isParent = originalItem.MessageId != null && originalItem.MessageId == targetItem.InReplyTo;
|
||||
|
||||
return isChild || isParent;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
|
||||
namespace Wino.Services.Threading
|
||||
{
|
||||
// Outlook and Gmail is using the same threading strategy.
|
||||
// Outlook: ConversationId -> it's set as ThreadId
|
||||
// Gmail: ThreadId
|
||||
namespace Wino.Services.Threading;
|
||||
|
||||
public class OutlookThreadingStrategy : APIThreadingStrategy, IOutlookThreadingStrategy
|
||||
{
|
||||
public OutlookThreadingStrategy(IDatabaseService databaseService, IFolderService folderService) : base(databaseService, folderService) { }
|
||||
}
|
||||
// Outlook and Gmail is using the same threading strategy.
|
||||
// Outlook: ConversationId -> it's set as ThreadId
|
||||
// Gmail: ThreadId
|
||||
|
||||
public class OutlookThreadingStrategy : APIThreadingStrategy, IOutlookThreadingStrategy
|
||||
{
|
||||
public OutlookThreadingStrategy(IDatabaseService databaseService, IFolderService folderService) : base(databaseService, folderService) { }
|
||||
}
|
||||
|
||||
@@ -1,31 +1,30 @@
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
|
||||
namespace Wino.Services
|
||||
namespace Wino.Services;
|
||||
|
||||
public class ThreadingStrategyProvider : IThreadingStrategyProvider
|
||||
{
|
||||
public class ThreadingStrategyProvider : IThreadingStrategyProvider
|
||||
private readonly IOutlookThreadingStrategy _outlookThreadingStrategy;
|
||||
private readonly IGmailThreadingStrategy _gmailThreadingStrategy;
|
||||
private readonly IImapThreadingStrategy _imapThreadStrategy;
|
||||
|
||||
public ThreadingStrategyProvider(IOutlookThreadingStrategy outlookThreadingStrategy,
|
||||
IGmailThreadingStrategy gmailThreadingStrategy,
|
||||
IImapThreadingStrategy imapThreadStrategy)
|
||||
{
|
||||
private readonly IOutlookThreadingStrategy _outlookThreadingStrategy;
|
||||
private readonly IGmailThreadingStrategy _gmailThreadingStrategy;
|
||||
private readonly IImapThreadingStrategy _imapThreadStrategy;
|
||||
_outlookThreadingStrategy = outlookThreadingStrategy;
|
||||
_gmailThreadingStrategy = gmailThreadingStrategy;
|
||||
_imapThreadStrategy = imapThreadStrategy;
|
||||
}
|
||||
|
||||
public ThreadingStrategyProvider(IOutlookThreadingStrategy outlookThreadingStrategy,
|
||||
IGmailThreadingStrategy gmailThreadingStrategy,
|
||||
IImapThreadingStrategy imapThreadStrategy)
|
||||
public IThreadingStrategy GetStrategy(MailProviderType mailProviderType)
|
||||
{
|
||||
return mailProviderType switch
|
||||
{
|
||||
_outlookThreadingStrategy = outlookThreadingStrategy;
|
||||
_gmailThreadingStrategy = gmailThreadingStrategy;
|
||||
_imapThreadStrategy = imapThreadStrategy;
|
||||
}
|
||||
|
||||
public IThreadingStrategy GetStrategy(MailProviderType mailProviderType)
|
||||
{
|
||||
return mailProviderType switch
|
||||
{
|
||||
MailProviderType.Outlook => _outlookThreadingStrategy,
|
||||
MailProviderType.Gmail => _gmailThreadingStrategy,
|
||||
_ => _imapThreadStrategy,
|
||||
};
|
||||
}
|
||||
MailProviderType.Outlook => _outlookThreadingStrategy,
|
||||
MailProviderType.Gmail => _gmailThreadingStrategy,
|
||||
_ => _imapThreadStrategy,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,76 +10,75 @@ using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Core.Domain.Models.Translations;
|
||||
using Wino.Messaging.Client.Shell;
|
||||
|
||||
namespace Wino.Services
|
||||
namespace Wino.Services;
|
||||
|
||||
public class TranslationService : ITranslationService
|
||||
{
|
||||
public class TranslationService : ITranslationService
|
||||
public const AppLanguage DefaultAppLanguage = AppLanguage.English;
|
||||
|
||||
private ILogger _logger = Log.ForContext<TranslationService>();
|
||||
private readonly IPreferencesService _preferencesService;
|
||||
private bool isInitialized = false;
|
||||
|
||||
public TranslationService(IPreferencesService preferencesService)
|
||||
{
|
||||
public const AppLanguage DefaultAppLanguage = AppLanguage.English;
|
||||
_preferencesService = preferencesService;
|
||||
}
|
||||
|
||||
private ILogger _logger = Log.ForContext<TranslationService>();
|
||||
private readonly IPreferencesService _preferencesService;
|
||||
private bool isInitialized = false;
|
||||
// Initialize default language with ignoring current language check.
|
||||
public Task InitializeAsync() => InitializeLanguageAsync(_preferencesService.CurrentLanguage, ignoreCurrentLanguageCheck: true);
|
||||
|
||||
public TranslationService(IPreferencesService preferencesService)
|
||||
public async Task InitializeLanguageAsync(AppLanguage language, bool ignoreCurrentLanguageCheck = false)
|
||||
{
|
||||
if (!ignoreCurrentLanguageCheck && _preferencesService.CurrentLanguage == language)
|
||||
{
|
||||
_preferencesService = preferencesService;
|
||||
_logger.Warning("Changing language is ignored because current language and requested language are same.");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Initialize default language with ignoring current language check.
|
||||
public Task InitializeAsync() => InitializeLanguageAsync(_preferencesService.CurrentLanguage, ignoreCurrentLanguageCheck: true);
|
||||
if (ignoreCurrentLanguageCheck && isInitialized) return;
|
||||
|
||||
public async Task InitializeLanguageAsync(AppLanguage language, bool ignoreCurrentLanguageCheck = false)
|
||||
var currentDictionary = Translator.Resources;
|
||||
await using var resourceStream = Core.Domain.Translations.WinoTranslationDictionary.GetLanguageStream(language);
|
||||
|
||||
var streamValue = await new StreamReader(resourceStream).ReadToEndAsync().ConfigureAwait(false);
|
||||
|
||||
var translationLookups = JsonSerializer.Deserialize(streamValue, BasicTypesJsonContext.Default.DictionaryStringString);
|
||||
|
||||
// Insert new translation key-value pairs.
|
||||
// Overwrite existing values for the same keys.
|
||||
|
||||
foreach (var pair in translationLookups)
|
||||
{
|
||||
if (!ignoreCurrentLanguageCheck && _preferencesService.CurrentLanguage == language)
|
||||
{
|
||||
_logger.Warning("Changing language is ignored because current language and requested language are same.");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (ignoreCurrentLanguageCheck && isInitialized) return;
|
||||
|
||||
var currentDictionary = Translator.Resources;
|
||||
await using var resourceStream = Core.Domain.Translations.WinoTranslationDictionary.GetLanguageStream(language);
|
||||
|
||||
var streamValue = await new StreamReader(resourceStream).ReadToEndAsync().ConfigureAwait(false);
|
||||
|
||||
var translationLookups = JsonSerializer.Deserialize(streamValue, BasicTypesJsonContext.Default.DictionaryStringString);
|
||||
|
||||
// Insert new translation key-value pairs.
|
||||
// Overwrite existing values for the same keys.
|
||||
|
||||
foreach (var pair in translationLookups)
|
||||
{
|
||||
// Replace existing value.
|
||||
currentDictionary[pair.Key] = pair.Value;
|
||||
}
|
||||
|
||||
_preferencesService.CurrentLanguage = language;
|
||||
|
||||
isInitialized = true;
|
||||
WeakReferenceMessenger.Default.Send(new LanguageChanged());
|
||||
// Replace existing value.
|
||||
currentDictionary[pair.Key] = pair.Value;
|
||||
}
|
||||
|
||||
public List<AppLanguageModel> GetAvailableLanguages()
|
||||
{
|
||||
return
|
||||
[
|
||||
new AppLanguageModel(AppLanguage.Chinese, "Chinese"),
|
||||
new AppLanguageModel(AppLanguage.Czech, "Czech"),
|
||||
new AppLanguageModel(AppLanguage.Deutsch, "Deutsch"),
|
||||
new AppLanguageModel(AppLanguage.English, "English"),
|
||||
new AppLanguageModel(AppLanguage.French, "French"),
|
||||
new AppLanguageModel(AppLanguage.Italian, "Italian"),
|
||||
new AppLanguageModel(AppLanguage.Greek, "Greek"),
|
||||
new AppLanguageModel(AppLanguage.Indonesian, "Indonesian"),
|
||||
new AppLanguageModel(AppLanguage.Polish, "Polski"),
|
||||
new AppLanguageModel(AppLanguage.PortugeseBrazil, "Portugese-Brazil"),
|
||||
new AppLanguageModel(AppLanguage.Russian, "Russian"),
|
||||
new AppLanguageModel(AppLanguage.Romanian, "Romanian"),
|
||||
new AppLanguageModel(AppLanguage.Spanish, "Spanish"),
|
||||
new AppLanguageModel(AppLanguage.Turkish, "Turkish")
|
||||
];
|
||||
}
|
||||
_preferencesService.CurrentLanguage = language;
|
||||
|
||||
isInitialized = true;
|
||||
WeakReferenceMessenger.Default.Send(new LanguageChanged());
|
||||
}
|
||||
|
||||
public List<AppLanguageModel> GetAvailableLanguages()
|
||||
{
|
||||
return
|
||||
[
|
||||
new AppLanguageModel(AppLanguage.Chinese, "Chinese"),
|
||||
new AppLanguageModel(AppLanguage.Czech, "Czech"),
|
||||
new AppLanguageModel(AppLanguage.Deutsch, "Deutsch"),
|
||||
new AppLanguageModel(AppLanguage.English, "English"),
|
||||
new AppLanguageModel(AppLanguage.French, "French"),
|
||||
new AppLanguageModel(AppLanguage.Italian, "Italian"),
|
||||
new AppLanguageModel(AppLanguage.Greek, "Greek"),
|
||||
new AppLanguageModel(AppLanguage.Indonesian, "Indonesian"),
|
||||
new AppLanguageModel(AppLanguage.Polish, "Polski"),
|
||||
new AppLanguageModel(AppLanguage.PortugeseBrazil, "Portugese-Brazil"),
|
||||
new AppLanguageModel(AppLanguage.Russian, "Russian"),
|
||||
new AppLanguageModel(AppLanguage.Romanian, "Romanian"),
|
||||
new AppLanguageModel(AppLanguage.Spanish, "Spanish"),
|
||||
new AppLanguageModel(AppLanguage.Turkish, "Turkish")
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user