Merged feature/vNext. Initial commit for Wino Mail 2.0

This commit is contained in:
Burak Kaan Köse
2026-04-05 16:30:26 +02:00
1513 changed files with 93788 additions and 26896 deletions
+10
View File
@@ -0,0 +1,10 @@
namespace Wino.Core.Domain;
public static class AppUrls
{
public const string Website = "https://www.winomail.app";
public const string Discord = "https://discord.gg/windows-apps-hub-714581497222398064";
public const string GitHub = "https://github.com/bkaankose/Wino-Mail/";
public const string PrivacyPolicy = "https://www.winomail.app/support/privacy";
public const string Paypal = "https://paypal.me/bkaankose?country.x=PL&locale.x=en_US";
}
@@ -1,5 +1,6 @@
using System.Collections.Generic;
using System.Text.Json.Serialization;
using Wino.Core.Domain.Models.Updates;
namespace Wino.Core.Domain;
@@ -8,4 +9,6 @@ namespace Wino.Core.Domain;
[JsonSerializable(typeof(int))]
[JsonSerializable(typeof(List<string>))]
[JsonSerializable(typeof(bool))]
[JsonSerializable(typeof(UpdateNotes))]
[JsonSerializable(typeof(List<UpdateNoteSection>))]
public partial class BasicTypesJsonContext : JsonSerializerContext;
@@ -0,0 +1,126 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Models.Calendar;
namespace Wino.Core.Domain;
public static class CalendarRecurrenceSummaryFormatter
{
private static readonly DayOfWeek[] OrderedDays =
[
DayOfWeek.Monday,
DayOfWeek.Tuesday,
DayOfWeek.Wednesday,
DayOfWeek.Thursday,
DayOfWeek.Friday,
DayOfWeek.Saturday,
DayOfWeek.Sunday
];
public static string BuildSummary(
bool isRecurring,
DateTimeOffset effectiveStart,
DateTimeOffset effectiveEnd,
bool isAllDay,
CalendarSettings settings,
int interval,
CalendarItemRecurrenceFrequency frequency,
IReadOnlyCollection<DayOfWeek> daysOfWeek,
DateTimeOffset? recurrenceEndDate)
{
var culture = settings?.CultureInfo ?? CultureInfo.CurrentCulture;
var timeSummary = isAllDay
? Translator.CalendarItemAllDay
: string.Format(
culture,
Translator.CalendarEventCompose_TimeRangeSummary,
effectiveStart.ToString(settings?.DayHeaderDisplayType == DayHeaderDisplayType.TwentyFourHour ? "HH:mm" : "h:mm tt", culture),
effectiveEnd.ToString(settings?.DayHeaderDisplayType == DayHeaderDisplayType.TwentyFourHour ? "HH:mm" : "h:mm tt", culture));
if (!isRecurring)
{
return string.Format(
culture,
Translator.CalendarEventCompose_SingleOccurrenceSummary,
effectiveStart.ToString("dddd yyyy-MM-dd", culture),
timeSummary);
}
var normalizedDays = NormalizeDays(daysOfWeek);
var isEveryDay = (frequency == CalendarItemRecurrenceFrequency.Daily && interval == 1) ||
(frequency == CalendarItemRecurrenceFrequency.Weekly && interval == 1 && normalizedDays.Count == 7);
var cadenceSummary = isEveryDay
? $"{Translator.CalendarEventCompose_Every} {Translator.CalendarEventCompose_FrequencyDay}"
: interval == 1
? $"{Translator.CalendarEventCompose_Every} {GetSingularFrequencyLabel(frequency)}"
: $"{Translator.CalendarEventCompose_Every} {interval.ToString(culture)} {GetPluralFrequencyLabel(frequency)}";
var weekdaySummary = string.Empty;
if (frequency == CalendarItemRecurrenceFrequency.Weekly && normalizedDays.Count > 0 && normalizedDays.Count < 7)
{
weekdaySummary = string.Format(
culture,
Translator.CalendarEventCompose_WeekdaySummary,
string.Join(", ", normalizedDays.Select(day => culture.DateTimeFormat.GetDayName(day))));
}
var untilSummary = recurrenceEndDate.HasValue
? string.Format(
culture,
Translator.CalendarEventCompose_UntilSummary,
recurrenceEndDate.Value.ToString("ddd yyyy-MM-dd", culture))
: string.Empty;
return string.Format(
culture,
Translator.CalendarEventCompose_RecurringSummarySmart,
cadenceSummary,
weekdaySummary,
timeSummary,
effectiveStart.ToString("dddd yyyy-MM-dd", culture),
untilSummary).Trim();
}
private static IReadOnlyList<DayOfWeek> NormalizeDays(IReadOnlyCollection<DayOfWeek> daysOfWeek)
{
if (daysOfWeek == null || daysOfWeek.Count == 0)
{
return [];
}
return daysOfWeek
.Distinct()
.OrderBy(day => Array.IndexOf(OrderedDays, day))
.ToList();
}
private static string GetSingularFrequencyLabel(CalendarItemRecurrenceFrequency frequency)
{
return frequency switch
{
CalendarItemRecurrenceFrequency.Daily => Translator.CalendarEventCompose_FrequencyDay,
CalendarItemRecurrenceFrequency.Weekly => Translator.CalendarEventCompose_FrequencyWeek,
CalendarItemRecurrenceFrequency.Monthly => Translator.CalendarEventCompose_FrequencyMonth,
CalendarItemRecurrenceFrequency.Yearly => Translator.CalendarEventCompose_FrequencyYear,
_ => Translator.CalendarEventCompose_FrequencyWeek
};
}
private static string GetPluralFrequencyLabel(CalendarItemRecurrenceFrequency frequency)
{
return frequency switch
{
CalendarItemRecurrenceFrequency.Daily => Translator.CalendarEventCompose_FrequencyDayPlural,
CalendarItemRecurrenceFrequency.Weekly => Translator.CalendarEventCompose_FrequencyWeekPlural,
CalendarItemRecurrenceFrequency.Monthly => Translator.CalendarEventCompose_FrequencyMonthPlural,
CalendarItemRecurrenceFrequency.Yearly => Translator.CalendarEventCompose_FrequencyYearPlural,
_ => Translator.CalendarEventCompose_FrequencyWeekPlural
};
}
}
@@ -0,0 +1,29 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace Wino.Core.Domain;
public static class CalendarReminderSnoozeOptions
{
private static readonly int[] SupportedSnoozeMinutes = [5, 10, 15, 30];
public static IReadOnlyList<int> GetSupportedSnoozeMinutes()
=> SupportedSnoozeMinutes;
public static IReadOnlyList<int> GetAllowedSnoozeMinutes(long reminderDurationInSeconds, long defaultReminderDurationInSeconds)
{
var reminderMinutes = (int)Math.Max(0, reminderDurationInSeconds / 60);
if (reminderMinutes <= 0)
return [];
var maxSnoozeMinutes = reminderMinutes;
var defaultReminderMinutes = (int)Math.Max(0, defaultReminderDurationInSeconds / 60);
if (defaultReminderMinutes > 0)
maxSnoozeMinutes = Math.Min(maxSnoozeMinutes, defaultReminderMinutes);
return SupportedSnoozeMinutes.Where(minutes => minutes <= maxSnoozeMinutes).ToArray();
}
}
@@ -13,6 +13,7 @@ public class CalendarEventCollection
{
public event EventHandler<ICalendarItem> CalendarItemAdded;
public event EventHandler<ICalendarItem> CalendarItemRemoved;
public event EventHandler<ICalendarItem> CalendarItemUpdated;
public event EventHandler CalendarItemsCleared;
@@ -114,11 +115,27 @@ public class CalendarEventCollection
}
}
public void RemoveCalendarItems(Func<ICalendarItem, bool> predicate)
{
if (predicate == null) return;
var itemsToRemove = _allItems.Where(predicate).ToList();
foreach (var item in itemsToRemove)
{
RemoveCalendarItem(item);
}
}
private void AddCalendarItemInternal(ObservableRangeCollection<ICalendarItem> collection, ICalendarItem calendarItem, bool create = true)
{
if (calendarItem is not ICalendarItemViewModel)
if (calendarItem is not ICalendarItemViewModel viewModel)
throw new ArgumentException("CalendarItem must be of type ICalendarItemViewModel", nameof(calendarItem));
// Set the displaying context for proper title calculation
viewModel.DisplayingPeriod = Period;
viewModel.CalendarSettings = Settings;
collection.Add(calendarItem);
if (create)
@@ -144,6 +161,53 @@ public class CalendarEventCollection
CalendarItemRemoved?.Invoke(this, calendarItem);
}
/// <summary>
/// Updates an existing calendar item in-place. If the item's type changed (all-day vs regular),
/// it will be moved to the appropriate collection.
/// </summary>
/// <param name="calendarItem">The updated calendar item data.</param>
/// <returns>True if the item was found and updated; false otherwise.</returns>
public bool UpdateCalendarItem(CalendarItem calendarItem)
{
var existingItem = _allItems.FirstOrDefault(x => x.Id == calendarItem.Id);
if (existingItem == null)
return false;
// Get the collections this item is currently in (before update)
var oldCollections = GetProperCollectionsForCalendarItem(existingItem).ToList();
// Update the underlying data
if (existingItem is ICalendarItemViewModel viewModel)
{
viewModel.UpdateFrom(calendarItem);
}
// Get the collections this item should be in (after update)
var newCollections = GetProperCollectionsForCalendarItem(existingItem).ToList();
// Check if the collections changed
var collectionsToRemoveFrom = oldCollections.Except(newCollections).ToList();
var collectionsToAddTo = newCollections.Except(oldCollections).ToList();
// Remove from old collections that are no longer applicable
foreach (var collection in collectionsToRemoveFrom)
{
collection.Remove(existingItem);
}
// Add to new collections that are now applicable
foreach (var collection in collectionsToAddTo)
{
if (!collection.Contains(existingItem))
{
collection.Add(existingItem);
}
}
CalendarItemUpdated?.Invoke(this, existingItem);
return true;
}
public void Clear()
{
_internalAllDayEvents.Clear();
@@ -1,41 +0,0 @@
using System.Linq;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Calendar;
namespace Wino.Core.Domain.Collections;
public class DayRangeCollection : ObservableRangeCollection<DayRangeRenderModel>
{
/// <summary>
/// Gets the range of dates that are currently displayed in the collection.
/// </summary>
public DateRange DisplayRange
{
get
{
if (Count == 0) return null;
var minimumLoadedDate = this[0].CalendarRenderOptions.DateRange.StartDate;
var maximumLoadedDate = this[Count - 1].CalendarRenderOptions.DateRange.EndDate;
return new DateRange(minimumLoadedDate, maximumLoadedDate);
}
}
public void RemoveCalendarItem(ICalendarItem calendarItem)
{
foreach (var dayRange in this)
{
}
}
public void AddCalendarItem(ICalendarItem calendarItem)
{
foreach (var dayRange in this)
{
var calendarDayModel = dayRange.CalendarDays.FirstOrDefault(x => x.Period.HasInside(calendarItem.Period.Start));
calendarDayModel?.EventsCollection.AddCalendarItem(calendarItem);
}
}
}
+13 -2
View File
@@ -1,4 +1,4 @@
namespace Wino.Core.Domain;
namespace Wino.Core.Domain;
public static class Constants
{
@@ -12,7 +12,17 @@ public static class Constants
public const string ToastMailUniqueIdKey = nameof(ToastMailUniqueIdKey);
public const string ToastActionKey = nameof(ToastActionKey);
public const string ToastMailAccountIdKey = nameof(ToastMailAccountIdKey);
public const string ToastCalendarItemIdKey = nameof(ToastCalendarItemIdKey);
public const string ToastCalendarActionKey = nameof(ToastCalendarActionKey);
public const string ToastCalendarNavigateAction = nameof(ToastCalendarNavigateAction);
public const string ToastCalendarSnoozeAction = nameof(ToastCalendarSnoozeAction);
public const string ToastCalendarSnoozeDurationInputId = nameof(ToastCalendarSnoozeDurationInputId);
public const string ToastModeKey = nameof(ToastModeKey);
public const string ToastModeMail = nameof(ToastModeMail);
public const string ToastModeCalendar = nameof(ToastModeCalendar);
public const string ToastStoreUpdateActionKey = nameof(ToastStoreUpdateActionKey);
public const string ToastStoreUpdateActionInstall = nameof(ToastStoreUpdateActionInstall);
public const string ClientLogFile = "Client_.log";
public const string ServerLogFile = "Server_.log";
public const string LogArchiveFileName = "WinoLogs.zip";
@@ -20,3 +30,4 @@ public static class Constants
public const string WinoMailIdentiifer = nameof(WinoMailIdentiifer);
public const string WinoCalendarIdentifier = nameof(WinoCalendarIdentifier);
}
@@ -1,9 +1,12 @@
using System;
using SQLite;
using Wino.Core.Domain.Entities.Shared;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Interfaces;
namespace Wino.Core.Domain.Entities.Calendar;
[Preserve]
public class AccountCalendar : IAccountCalendar
{
[PrimaryKey]
@@ -13,7 +16,9 @@ public class AccountCalendar : IAccountCalendar
public string SynchronizationDeltaToken { get; set; }
public string Name { get; set; }
public bool IsPrimary { get; set; }
public bool IsSynchronizationEnabled { get; set; } = true;
public bool IsExtended { get; set; } = true;
public CalendarItemShowAs DefaultShowAs { get; set; } = CalendarItemShowAs.Busy;
/// <summary>
/// Unused for now.
@@ -21,4 +26,7 @@ public class AccountCalendar : IAccountCalendar
public string TextColorHex { get; set; }
public string BackgroundColorHex { get; set; }
public string TimeZone { get; set; }
[Ignore]
public MailAccount MailAccount { get; set; }
}
@@ -0,0 +1,55 @@
using System;
using SQLite;
using Wino.Core.Domain.Enums;
namespace Wino.Core.Domain.Entities.Calendar;
/// <summary>
/// Represents metadata for calendar event attachments.
/// Actual file content is downloaded on-demand.
/// </summary>
public class CalendarAttachment
{
[PrimaryKey]
public Guid Id { get; set; }
/// <summary>
/// The calendar item this attachment belongs to.
/// </summary>
public Guid CalendarItemId { get; set; }
/// <summary>
/// Remote identifier for the attachment from the provider (Outlook, Gmail, etc.).
/// </summary>
public string RemoteAttachmentId { get; set; }
/// <summary>
/// File name of the attachment.
/// </summary>
public string FileName { get; set; }
/// <summary>
/// Size of the attachment in bytes.
/// </summary>
public long Size { get; set; }
/// <summary>
/// MIME content type (e.g., "application/pdf", "image/png").
/// </summary>
public string ContentType { get; set; }
/// <summary>
/// Whether the attachment has been downloaded to local storage.
/// </summary>
public bool IsDownloaded { get; set; }
/// <summary>
/// Local file path where the attachment is stored (if downloaded).
/// </summary>
public string LocalFilePath { get; set; }
/// <summary>
/// When the attachment was last modified.
/// </summary>
public DateTimeOffset LastModified { get; set; }
}
@@ -1,10 +1,10 @@
using System;
using SQLite;
using Wino.Core.Domain.Entities.Shared;
using Wino.Core.Domain.Enums;
namespace Wino.Core.Domain.Entities.Calendar;
// TODO: Connect to Contact store with Wino People.
public class CalendarEventAttendee
{
[PrimaryKey]
@@ -16,4 +16,11 @@ public class CalendarEventAttendee
public bool IsOrganizer { get; set; }
public bool IsOptionalAttendee { get; set; }
public string Comment { get; set; }
/// <summary>
/// Resolved contact from the contact store. Populated at runtime via IContactService;
/// not persisted to the database.
/// </summary>
[Ignore]
public AccountContact ResolvedContact { get; set; }
}
@@ -3,7 +3,9 @@ using System.Diagnostics;
using Itenso.TimePeriod;
using SQLite;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Extensions;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Calendar;
namespace Wino.Core.Domain.Entities.Calendar;
@@ -17,6 +19,14 @@ public class CalendarItem : ICalendarItem
public string Description { get; set; }
public string Location { get; set; }
/// <summary>
/// Indicates whether this item is a local preview that hasn't been synced to the server yet.
/// When true, the item exists only in the local database without a RemoteEventId.
/// Used to prevent duplicates when the server returns the newly created event.
/// </summary>
[Ignore]
public bool IsLocalPreview => string.IsNullOrEmpty(RemoteEventId);
public DateTime StartDate { get; set; }
public DateTime EndDate
@@ -27,8 +37,17 @@ public class CalendarItem : ICalendarItem
}
}
public TimeSpan StartDateOffset { get; set; }
public TimeSpan EndDateOffset { get; set; }
/// <summary>
/// IANA timezone identifier for the start time (e.g., "America/New_York", "Europe/London").
/// If null or empty, UTC is assumed.
/// </summary>
public string StartTimeZone { get; set; }
/// <summary>
/// IANA timezone identifier for the end time (e.g., "America/New_York", "Europe/London").
/// If null or empty, UTC is assumed.
/// </summary>
public string EndTimeZone { get; set; }
private ITimePeriod _period;
public ITimePeriod Period
@@ -55,9 +74,7 @@ public class CalendarItem : ICalendarItem
}
/// <summary>
/// Events that are either an exceptional instance of a recurring event or occurrences.
/// IsOccurrence is used to display occurrence instances of parent recurring events.
/// IsOccurrence == false && IsRecurringChild == true => exceptional single instance.
/// Events that are child instances of a recurring event (occurrences or exceptions).
/// </summary>
public bool IsRecurringChild
{
@@ -68,7 +85,7 @@ public class CalendarItem : ICalendarItem
}
/// <summary>
/// Events that are either an exceptional instance of a recurring event or occurrences.
/// Events that are part of a recurring series (either as parent or child).
/// </summary>
public bool IsRecurringEvent => IsRecurringChild || IsRecurringParent;
@@ -121,8 +138,15 @@ public class CalendarItem : ICalendarItem
// TODO
public string CustomEventColorHex { get; set; }
public string HtmlLink { get; set; }
public DateTime? SnoozedUntil { get; set; }
public CalendarItemStatus Status { get; set; }
public CalendarItemVisibility Visibility { get; set; }
/// <summary>
/// Indicates how the event should be shown in the calendar (Free, Busy, Tentative, etc.).
/// </summary>
public CalendarItemShowAs ShowAs { get; set; } = CalendarItemShowAs.Busy;
public DateTimeOffset CreatedAt { get; set; }
public DateTimeOffset UpdatedAt { get; set; }
public Guid CalendarId { get; set; }
@@ -131,49 +155,37 @@ public class CalendarItem : ICalendarItem
public IAccountCalendar AssignedCalendar { get; set; }
/// <summary>
/// Whether this item does not really exist in the database or not.
/// These are used to display occurrence instances of parent recurring events.
/// Id to load information related to this event (attendees, reminders, etc.).
/// For child events, if they have their own data, use their own Id.
/// For events that share data with their parent, return parent's Id.
/// </summary>
[Ignore]
public bool IsOccurrence { get; set; }
public Guid EventTrackingId => Id;
/// <summary>
/// Id to load information related to this event.
/// Occurrences tracked by the parent recurring event if they are not exceptional instances.
/// Recurring children here are exceptional instances. They have their own info in the database including Id.
/// Gets the start date converted to user's local timezone for display.
/// StartDate is stored according to StartTimeZone.
/// </summary>
public Guid EventTrackingId => IsOccurrence ? RecurringCalendarItemId.Value : Id;
public CalendarItem CreateRecurrence(DateTime startDate, double durationInSeconds)
[Ignore]
public DateTime LocalStartDate
{
// Create a copy with the new start date and duration
return new CalendarItem
get
{
Id = Guid.NewGuid(),
Title = Title,
Description = Description,
Location = Location,
StartDate = startDate,
DurationInSeconds = durationInSeconds,
Recurrence = Recurrence,
OrganizerDisplayName = OrganizerDisplayName,
OrganizerEmail = OrganizerEmail,
RecurringCalendarItemId = Id,
AssignedCalendar = AssignedCalendar,
CalendarId = CalendarId,
CreatedAt = CreatedAt,
UpdatedAt = UpdatedAt,
Visibility = Visibility,
Status = Status,
CustomEventColorHex = CustomEventColorHex,
HtmlLink = HtmlLink,
StartDateOffset = StartDateOffset,
EndDateOffset = EndDateOffset,
RemoteEventId = RemoteEventId,
IsHidden = IsHidden,
IsLocked = IsLocked,
IsOccurrence = true
};
return this.GetLocalStartDate();
}
}
/// <summary>
/// Gets the end date converted to user's local timezone for display.
/// EndDate is calculated from StartDate and is in StartTimeZone.
/// </summary>
[Ignore]
public DateTime LocalEndDate
{
get
{
return this.GetLocalEndDate();
}
}
public string GetDisplayTitle(ITimePeriod displayingPeriod, CalendarSettings calendarSettings) => Period.ToString();
}
@@ -10,6 +10,10 @@ public class Reminder
public Guid Id { get; set; }
public Guid CalendarItemId { get; set; }
public DateTimeOffset ReminderTime { get; set; }
/// <summary>
/// Duration in seconds before the event start time when the reminder should trigger.
/// For example, 900 seconds = 15 minutes before event.
/// </summary>
public long DurationInSeconds { get; set; }
public CalendarItemReminderType ReminderType { get; set; }
}
@@ -0,0 +1,16 @@
using System;
using SQLite;
namespace Wino.Core.Domain.Entities.Mail;
public class EmailTemplate
{
[PrimaryKey]
public Guid Id { get; set; }
public string Name { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
public string HtmlContent { get; set; } = string.Empty;
}
@@ -1,4 +1,6 @@
using System;
using System.Collections.ObjectModel;
using System.Security.Cryptography.X509Certificates;
using SQLite;
namespace Wino.Core.Domain.Entities.Mail;
@@ -59,4 +61,13 @@ public class MailAccountAlias : RemoteAccountAlias
/// Root aliases can't be deleted.
/// </summary>
public bool CanDelete => !IsRootAlias;
public string SelectedSigningCertificateThumbprint { get; set; }
public bool IsSmimeEncryptionEnabled { get; set; }
[Ignore]
public X509Certificate2 SelectedSigningCertificate { get; set; }
[Ignore]
public ObservableCollection<X509Certificate2> Certificates { get; set; } = [];
}
+6 -2
View File
@@ -3,7 +3,6 @@ using System.Collections.Generic;
using SQLite;
using Wino.Core.Domain.Entities.Shared;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Models.MailItem;
namespace Wino.Core.Domain.Entities.Mail;
@@ -11,7 +10,7 @@ namespace Wino.Core.Domain.Entities.Mail;
/// Summary of the parsed MIME messages.
/// Wino will do non-network operations on this table and others from the original MIME.
/// </summary>
public class MailCopy : IMailItem
public class MailCopy
{
/// <summary>
/// Unique Id of the mail.
@@ -104,6 +103,11 @@ public class MailCopy : IMailItem
/// </summary>
public bool HasAttachments { get; set; }
/// <summary>
/// Type of mail item (regular mail, calendar invitation, calendar response, etc.).
/// </summary>
public MailItemType ItemType { get; set; } = MailItemType.Mail;
/// <summary>
/// Assigned draft id.
/// </summary>
@@ -0,0 +1,31 @@
using System;
using SQLite;
namespace Wino.Core.Domain.Entities.Mail;
/// <summary>
/// Maps a calendar invitation mail item to a persisted calendar event.
/// </summary>
public class MailInvitationCalendarMapping
{
[PrimaryKey]
public Guid Id { get; set; }
public Guid AccountId { get; set; }
/// <summary>
/// MailCopy.Id value of the invitation mail.
/// </summary>
public string MailCopyId { get; set; }
/// <summary>
/// iCalendar UID extracted from invitation MIME/ICS content.
/// </summary>
public string InvitationUid { get; set; }
public Guid CalendarId { get; set; }
public Guid CalendarItemId { get; set; }
public string CalendarRemoteEventId { get; set; }
public DateTime UpdatedAtUtc { get; set; } = DateTime.UtcNow;
}
@@ -29,6 +29,8 @@ public class MailItemFolder : IMailItemFolder
// For IMAP
public uint UidValidity { get; set; }
public long HighestModeSeq { get; set; }
public uint HighestKnownUid { get; set; }
public DateTime? LastUidReconcileUtc { get; set; }
/// <summary>
/// Outlook shares delta changes per-folder. Gmail is for per-account.
@@ -11,7 +11,7 @@ namespace Wino.Core.Domain.Entities.Shared;
// TODO: This can easily evolve to Contact store, just like People app in Windows 10/11.
// Do it.
public class AccountContact : IEquatable<AccountContact>
public class AccountContact : IEquatable<AccountContact>, IContactDisplayItem
{
/// <summary>
/// E-mail address of the contact.
@@ -25,9 +25,10 @@ public class AccountContact : IEquatable<AccountContact>
public string Name { get; set; }
/// <summary>
/// Base64 encoded profile image of the contact.
/// File ID for the contact picture stored on disk.
/// The actual file lives at {ApplicationDataFolderPath}/contacts/{ContactPictureFileId}.jpg.
/// </summary>
public string Base64ContactPicture { get; set; }
public Guid? ContactPictureFileId { get; set; }
/// <summary>
/// All registered accounts have their contacts registered as root.
@@ -36,6 +37,15 @@ public class AccountContact : IEquatable<AccountContact>
/// </summary>
public bool IsRootContact { get; set; }
/// <summary>
/// When true, indicates that the contact has been manually modified by the user.
/// Contacts with this flag set to true should not be updated during synchronization.
/// </summary>
public bool IsOverridden { get; set; } = false;
public string DisplayName => string.IsNullOrWhiteSpace(Name) ? Address : Name;
AccountContact IContactDisplayItem.PreviewContact => this;
public override bool Equals(object obj)
{
return Equals(obj as AccountContact);
@@ -0,0 +1,19 @@
using System;
using SQLite;
namespace Wino.Core.Domain.Entities.Shared;
/// <summary>
/// A named group of contacts that can be expanded to individual addresses during mail composition.
/// </summary>
public class ContactGroup
{
[PrimaryKey]
public Guid Id { get; set; }
/// <summary>Display name of the group (e.g., "Team Alpha", "Family").</summary>
public string Name { get; set; }
/// <summary>Optional description for the group.</summary>
public string Description { get; set; }
}
@@ -0,0 +1,21 @@
using System;
using SQLite;
namespace Wino.Core.Domain.Entities.Shared;
/// <summary>
/// Associates an e-mail address with a <see cref="ContactGroup"/>.
/// </summary>
public class ContactGroupMember
{
[PrimaryKey, AutoIncrement]
public int Id { get; set; }
/// <summary>Group this member belongs to.</summary>
[Indexed]
public Guid GroupId { get; set; }
/// <summary>E-mail address of the member (FK to AccountContact.Address).</summary>
[Indexed]
public string MemberAddress { get; set; }
}
@@ -30,6 +30,11 @@ public class CustomServerInformation
public string OutgoingServerUsername { get; set; }
public string OutgoingServerPassword { get; set; }
public string CalDavServiceUrl { get; set; }
public string CalDavUsername { get; set; }
public string CalDavPassword { get; set; }
public ImapCalendarSupportMode CalendarSupportMode { get; set; }
/// <summary>
/// useSSL True: SslOnConnect
/// useSSL False: StartTlsWhenAvailable
@@ -65,6 +70,8 @@ public class CustomServerInformation
{ "OutgoingServerPort", OutgoingServerPort },
{ "OutgoingServerSocketOption", OutgoingServerSocketOption.ToString() },
{ "OutgoingAuthenticationMethod", OutgoingAuthenticationMethod.ToString() },
{ "CalendarSupportMode", CalendarSupportMode.ToString() },
{ "CalDavServiceUrl", CalDavServiceUrl },
{ "ProxyServer", ProxyServer },
{ "ProxyServerPort", ProxyServerPort }
};
@@ -0,0 +1,8 @@
namespace Wino.Core.Domain.Entities.Shared;
public interface IContactDisplayItem
{
string DisplayName { get; }
string Address { get; }
AccountContact PreviewContact { get; }
}
@@ -0,0 +1,65 @@
using System;
using SQLite;
using Wino.Core.Domain.Enums;
namespace Wino.Core.Domain.Entities.Shared;
/// <summary>
/// Represents a user-defined keyboard shortcut for mail operations.
/// </summary>
public class KeyboardShortcut
{
[PrimaryKey]
public Guid Id { get; set; }
/// <summary>
/// The application mode this shortcut applies to.
/// </summary>
public WinoApplicationMode Mode { get; set; } = WinoApplicationMode.Mail;
/// <summary>
/// The key combination string (e.g., "D", "Delete", "F1").
/// </summary>
public string Key { get; set; }
/// <summary>
/// The modifier keys for this shortcut.
/// </summary>
public ModifierKeys ModifierKeys { get; set; }
/// <summary>
/// The shortcut action this shortcut triggers.
/// </summary>
public KeyboardShortcutAction Action { get; set; }
/// <summary>
/// Whether this shortcut is enabled.
/// </summary>
public bool IsEnabled { get; set; } = true;
/// <summary>
/// When this shortcut was created.
/// </summary>
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
/// <summary>
/// User-friendly display name for the shortcut.
/// </summary>
public string DisplayName
{
get
{
var modifierText = string.Empty;
if (ModifierKeys.HasFlag(ModifierKeys.Control))
modifierText += "Ctrl+";
if (ModifierKeys.HasFlag(ModifierKeys.Alt))
modifierText += "Alt+";
if (ModifierKeys.HasFlag(ModifierKeys.Shift))
modifierText += "Shift+";
if (ModifierKeys.HasFlag(ModifierKeys.Windows))
modifierText += "Win+";
return modifierText + Key;
}
}
}
@@ -78,6 +78,14 @@ public class MailAccount
/// </summary>
public SpecialImapProvider SpecialImapProvider { get; set; }
/// <summary>
/// Gets or sets whether calendar access is granted for this account.
/// When false, synchronizers will not process EventMessages or calendar invitations.
/// Default is false for existing accounts to prevent scope issues.
/// New accounts created after this feature will have this set to true.
/// </summary>
public bool IsCalendarAccessGranted { get; set; }
/// <summary>
/// Contains the merged inbox this account belongs to.
/// Ignored for all SQLite operations.
@@ -98,6 +106,12 @@ public class MailAccount
[Ignore]
public MailAccountPreferences Preferences { get; set; }
/// <summary>
/// Last time folder structure was synchronized.
/// Used for optimization - skip folder sync if synced recently.
/// </summary>
public DateTime? LastFolderStructureSyncDate { get; set; }
/// <summary>
/// Gets whether the account can perform ProfileInformation sync type.
/// </summary>
@@ -107,4 +121,6 @@ public class MailAccount
/// Gets whether the account can perform AliasInformation sync type.
/// </summary>
public bool IsAliasSyncSupported => ProviderType == MailProviderType.Gmail;
public override string ToString() => Name;
}
@@ -0,0 +1,30 @@
using System;
using SQLite;
namespace Wino.Core.Domain.Entities.Shared;
public class WinoAccount
{
[PrimaryKey]
public Guid Id { get; set; }
public string Email { get; set; } = string.Empty;
public string AccountStatus { get; set; } = string.Empty;
public bool HasPassword { get; set; }
public bool HasGoogleLogin { get; set; }
public bool HasFacebookLogin { get; set; }
public string AccessToken { get; set; } = string.Empty;
public DateTime AccessTokenExpiresAtUtc { get; set; }
public string RefreshToken { get; set; } = string.Empty;
public DateTime RefreshTokenExpiresAtUtc { get; set; }
public DateTime LastAuthenticatedUtc { get; set; }
}
@@ -5,6 +5,7 @@ public enum AccountCreationDialogState
Idle,
SigningIn,
PreparingFolders,
CalendarMetadataFetch,
Completed,
ManuelSetupWaiting,
TestingConnection,
@@ -0,0 +1,9 @@
namespace Wino.Core.Domain.Enums;
public enum AccountSetupStepStatus
{
Pending,
InProgress,
Succeeded,
Failed
}
+12
View File
@@ -0,0 +1,12 @@
using System;
namespace Wino.Core.Domain.Enums;
[Flags]
public enum AiActionType
{
None = 0,
Translate = 1,
Rewrite = 2,
Summarize = 4,
}
@@ -5,6 +5,5 @@ public enum CalendarDisplayType
Day,
Week,
WorkWeek,
Month,
Year
Month
}
@@ -1,10 +0,0 @@
namespace Wino.Core.Domain.Enums;
/// <summary>
/// Trigger to load more data.
/// </summary>
public enum CalendarInitInitiative
{
User,
App
}
@@ -0,0 +1,13 @@
namespace Wino.Core.Domain.Enums;
/// <summary>
/// Defines how a calendar item should be displayed in terms of availability.
/// </summary>
public enum CalendarItemShowAs
{
Free,
Tentative,
Busy,
OutOfOffice,
WorkingElsewhere
}
+1 -1
View File
@@ -3,7 +3,7 @@
public enum CalendarItemStatus
{
NotResponded,
Confirmed,
Accepted,
Tentative,
Cancelled,
}
@@ -0,0 +1,22 @@
namespace Wino.Core.Domain.Enums;
/// <summary>
/// Indicates the source of a calendar item update.
/// </summary>
public enum CalendarItemUpdateSource
{
/// <summary>
/// Update originated from client-side UI changes (ApplyUIChanges).
/// </summary>
ClientUpdated,
/// <summary>
/// Update originated from client-side UI revert (RevertUIChanges).
/// </summary>
ClientReverted,
/// <summary>
/// Update originated from server synchronization or database operations.
/// </summary>
Server
}
@@ -5,6 +5,7 @@ public enum CalendarSynchronizationType
ExecuteRequests, // Execute all requests in the queue.
CalendarMetadata, // Sync calendar metadata.
CalendarEvents, // Sync all events for all calendars.
Strict, // Run metadata and event synchronization in sequence.
SingleCalendar, // Sync events for only specified calendars.
UpdateProfile // Update profile information only.
}
@@ -0,0 +1,10 @@
namespace Wino.Core.Domain.Enums;
/// <summary>
/// Grouping options for emails
/// </summary>
public enum EmailGroupingType
{
ByFromName,
ByDate
}
@@ -0,0 +1,8 @@
namespace Wino.Core.Domain.Enums;
public enum ImapCalendarSupportMode
{
Disabled = 0,
CalDav = 1,
LocalOnly = 2
}
@@ -0,0 +1,16 @@
namespace Wino.Core.Domain.Enums;
public enum KeyboardShortcutAction
{
None,
NewMail,
ToggleReadUnread,
ToggleFlag,
ToggleArchive,
Delete,
Move,
Reply,
ReplyAll,
Send,
NewEvent
}
@@ -0,0 +1,57 @@
using System;
namespace Wino.Core.Domain.Enums;
[Flags]
public enum MailCopyChangeFlags
{
None = 0,
Id = 1 << 0,
FolderId = 1 << 1,
ThreadId = 1 << 2,
MessageId = 1 << 3,
References = 1 << 4,
InReplyTo = 1 << 5,
FromName = 1 << 6,
FromAddress = 1 << 7,
Subject = 1 << 8,
PreviewText = 1 << 9,
CreationDate = 1 << 10,
Importance = 1 << 11,
IsRead = 1 << 12,
IsFlagged = 1 << 13,
IsFocused = 1 << 14,
HasAttachments = 1 << 15,
ItemType = 1 << 16,
DraftId = 1 << 17,
IsDraft = 1 << 18,
FileId = 1 << 19,
AssignedFolder = 1 << 20,
AssignedAccount = 1 << 21,
SenderContact = 1 << 22,
UniqueId = 1 << 23,
All = Id |
FolderId |
ThreadId |
MessageId |
References |
InReplyTo |
FromName |
FromAddress |
Subject |
PreviewText |
CreationDate |
Importance |
IsRead |
IsFlagged |
IsFocused |
HasAttachments |
ItemType |
DraftId |
IsDraft |
FileId |
AssignedFolder |
AssignedAccount |
SenderContact |
UniqueId
}
+27
View File
@@ -0,0 +1,27 @@
namespace Wino.Core.Domain.Enums;
/// <summary>
/// Represents the type of mail item.
/// </summary>
public enum MailItemType
{
/// <summary>
/// Regular mail message.
/// </summary>
Mail = 0,
/// <summary>
/// Calendar invitation (meeting request).
/// </summary>
CalendarInvitation = 1,
/// <summary>
/// Calendar response (meeting accepted, tentatively accepted, or declined).
/// </summary>
CalendarResponse = 2,
/// <summary>
/// Calendar cancellation (meeting cancelled).
/// </summary>
CalendarCancellation = 3
}
+12
View File
@@ -19,6 +19,18 @@ public enum FolderSynchronizerOperation
RenameFolder,
EmptyFolder,
MarkFolderRead,
DeleteFolder,
CreateSubFolder,
}
public enum CalendarSynchronizerOperation
{
CreateEvent,
UpdateEvent,
DeleteEvent,
AcceptEvent,
DeclineEvent,
TentativeEvent,
}
// UI requests
@@ -0,0 +1,22 @@
namespace Wino.Core.Domain.Enums;
/// <summary>
/// Indicates the source of a mail update.
/// </summary>
public enum MailUpdateSource
{
/// <summary>
/// Update originated from client-side UI changes (ApplyUIChanges).
/// </summary>
ClientUpdated,
/// <summary>
/// Update originated from client-side UI revert (RevertUIChanges).
/// </summary>
ClientReverted,
/// <summary>
/// Update originated from server synchronization or database operations.
/// </summary>
Server
}
+16
View File
@@ -0,0 +1,16 @@
using System;
namespace Wino.Core.Domain.Enums;
/// <summary>
/// Defines keyboard modifier keys that can be used in keyboard shortcuts.
/// </summary>
[Flags]
public enum ModifierKeys
{
None = 0,
Control = 1,
Alt = 2,
Shift = 4,
Windows = 8
}
@@ -3,5 +3,6 @@
public enum NavigationReferenceFrame
{
ShellFrame,
InnerShellFrame,
RenderingFrame
}
@@ -0,0 +1,27 @@
namespace Wino.Core.Domain.Enums;
/// <summary>
/// Specifies the animation effect to use during a slide navigation transition.
/// </summary>
public enum NavigationTransitionEffect
{
/// <summary>
/// The navigation transition effect starts from the left edge of the frame.
/// </summary>
FromLeft,
/// <summary>
/// The navigation transition effect starts from the right edge of the frame.
/// </summary>
FromRight,
/// <summary>
/// The navigation transition effect starts from the top edge of the frame.
/// </summary>
FromTop,
/// <summary>
/// The navigation transition effect starts from the bottom edge of the frame.
/// </summary>
FromBottom
}
@@ -0,0 +1,7 @@
namespace Wino.Core.Domain.Enums;
public enum NewEventButtonBehavior
{
AskEachTime = 0,
AlwaysUseSpecificCalendar = 1
}
+22
View File
@@ -0,0 +1,22 @@
namespace Wino.Core.Domain.Enums;
/// <summary>
/// Print collation options.
/// </summary>
public enum PrintCollation
{
/// <summary>
/// Default collation.
/// </summary>
Default = 0,
/// <summary>
/// Collated printing.
/// </summary>
Collated = 1,
/// <summary>
/// Uncollated printing.
/// </summary>
Uncollated = 2
}
+22
View File
@@ -0,0 +1,22 @@
namespace Wino.Core.Domain.Enums;
/// <summary>
/// Print color mode options.
/// </summary>
public enum PrintColorMode
{
/// <summary>
/// Default color mode.
/// </summary>
Default = 0,
/// <summary>
/// Color printing.
/// </summary>
Color = 1,
/// <summary>
/// Grayscale printing.
/// </summary>
Grayscale = 2
}
+27
View File
@@ -0,0 +1,27 @@
namespace Wino.Core.Domain.Enums;
/// <summary>
/// Print duplex (double-sided) options.
/// </summary>
public enum PrintDuplex
{
/// <summary>
/// Default duplex mode.
/// </summary>
Default = 0,
/// <summary>
/// Single-sided printing.
/// </summary>
Simplex = 1,
/// <summary>
/// Double-sided printing with pages flipped horizontally.
/// </summary>
DuplexShortEdge = 2,
/// <summary>
/// Double-sided printing with pages flipped vertically.
/// </summary>
DuplexLongEdge = 3
}
+57
View File
@@ -0,0 +1,57 @@
namespace Wino.Core.Domain.Enums;
/// <summary>
/// Print media size options.
/// </summary>
public enum PrintMediaSize
{
/// <summary>
/// Default media size.
/// </summary>
Default = 0,
/// <summary>
/// Letter size (8.5 x 11 inches).
/// </summary>
NorthAmericaLetter = 1,
/// <summary>
/// Legal size (8.5 x 14 inches).
/// </summary>
NorthAmericaLegal = 2,
/// <summary>
/// A4 size (210 x 297 mm).
/// </summary>
IsoA4 = 3,
/// <summary>
/// A3 size (297 x 420 mm).
/// </summary>
IsoA3 = 4,
/// <summary>
/// A5 size (148 x 210 mm).
/// </summary>
IsoA5 = 5,
/// <summary>
/// Tabloid size (11 x 17 inches).
/// </summary>
NorthAmericaTabloid = 6,
/// <summary>
/// Executive size (7.25 x 10.5 inches).
/// </summary>
NorthAmericaExecutive = 7,
/// <summary>
/// B4 size (250 x 353 mm).
/// </summary>
JisB4 = 8,
/// <summary>
/// B5 size (176 x 250 mm).
/// </summary>
JisB5 = 9
}
@@ -0,0 +1,17 @@
namespace Wino.Core.Domain.Enums;
/// <summary>
/// Print orientation options.
/// </summary>
public enum PrintOrientation
{
/// <summary>
/// Portrait orientation (default).
/// </summary>
Portrait = 0,
/// <summary>
/// Landscape orientation.
/// </summary>
Landscape = 1
}
@@ -1,11 +0,0 @@
namespace Wino.Core.Domain.Enums;
/// <summary>
/// What should happen to server app when the client is terminated.
/// </summary>
public enum ServerBackgroundMode
{
MinimizedTray, // Still runs, tray icon is visible.
Invisible, // Still runs, tray icon is invisible.
Terminate // Server is terminated as Wino terminates.
}
@@ -4,5 +4,6 @@ public enum SynchronizationCompletedState
{
Success, // All succeeded.
Canceled, // Canceled by user or HTTP call.
Failed // Exception.
Failed, // Exception.
PartiallyCompleted // Some folders succeeded, some failed.
}
@@ -1,11 +0,0 @@
namespace Wino.Core.Domain.Enums;
/// <summary>
/// Enumeration for the source of synchronization.
/// Right now it can either be from the client or the server.
/// </summary>
public enum SynchronizationSource
{
Client,
Server
}
@@ -0,0 +1,47 @@
namespace Wino.Core.Domain.Enums;
/// <summary>
/// Categorizes synchronization errors by their root cause for targeted handling.
/// </summary>
public enum SynchronizerErrorCategory
{
/// <summary>
/// Network-related issues: connection timeouts, DNS failures, socket errors.
/// </summary>
Network,
/// <summary>
/// Authentication failures: invalid credentials, expired tokens, revoked access.
/// </summary>
Authentication,
/// <summary>
/// Rate limiting: too many requests (HTTP 429), quota exceeded.
/// </summary>
RateLimit,
/// <summary>
/// Resource not found: folder or message deleted externally (HTTP 404).
/// </summary>
ResourceNotFound,
/// <summary>
/// Server errors: internal server errors (HTTP 5xx), service unavailable.
/// </summary>
ServerError,
/// <summary>
/// Protocol errors: IMAP/SMTP command failures, malformed responses.
/// </summary>
ProtocolError,
/// <summary>
/// Validation errors: invalid data, constraint violations.
/// </summary>
Validation,
/// <summary>
/// Unknown or unclassified error.
/// </summary>
Unknown
}
@@ -0,0 +1,31 @@
namespace Wino.Core.Domain.Enums;
/// <summary>
/// Classifies the severity of synchronization errors to determine retry behavior.
/// </summary>
public enum SynchronizerErrorSeverity
{
/// <summary>
/// Transient error that should be retried with exponential backoff.
/// Examples: network timeout, temporary server unavailability, rate limiting.
/// </summary>
Transient,
/// <summary>
/// Error that can be recovered from by skipping the affected item/folder and continuing sync.
/// Examples: folder deleted externally, message not found, permission denied on single item.
/// </summary>
Recoverable,
/// <summary>
/// Fatal error that requires stopping synchronization and user intervention.
/// Examples: account disabled, server permanently unavailable, critical configuration error.
/// </summary>
Fatal,
/// <summary>
/// Authentication error that requires the user to re-authenticate.
/// Examples: token expired, password changed, OAuth refresh failed.
/// </summary>
AuthRequired
}
@@ -0,0 +1,11 @@
namespace Wino.Core.Domain.Enums;
public enum WindowBackdropType
{
None,
Mica,
MicaAlt,
DesktopAcrylic,
AcrylicBase,
AcrylicThin
}
@@ -0,0 +1,7 @@
namespace Wino.Core.Domain.Enums;
public enum WinoAddOnProductType
{
AI_PACK,
UNLIMITED_ACCOUNTS
}
@@ -0,0 +1,9 @@
namespace Wino.Core.Domain.Enums;
public enum WinoApplicationMode
{
Mail,
Calendar,
Contacts,
Settings
}
+19 -5
View File
@@ -9,8 +9,8 @@ public enum WinoPage
IdlePage,
ComposePage,
SettingsPage,
ContactsPage,
MailRenderingPage,
WelcomePage,
AccountDetailsPage,
MergedAccountDetailsPage,
ManageAccountsPage,
@@ -21,13 +21,27 @@ public enum WinoPage
MessageListPage,
MailListPage,
ReadComposePanePage,
LanguageTimePage,
AppPreferencesPage,
SettingOptionsPage,
AliasManagementPage,
EditAccountDetailsPage,
// Calendar
ImapCalDavSettingsPage,
KeyboardShortcutsPage,
CalendarPage,
CalendarSettingsPage,
EventDetailsPage
CalendarRenderingSettingsPage,
CalendarNotificationSettingsPage,
CalendarPreferenceSettingsPage,
CalendarAccountSettingsPage,
EventDetailsPage,
CalendarEventComposePage,
SignatureAndEncryptionPage,
EmailTemplatesPage,
CreateEmailTemplatePage,
StoragePage,
WinoAccountManagementPage,
WelcomePageV2,
WelcomeHostPage,
ProviderSelectionPage,
AccountSetupProgressPage,
SpecialImapCredentialsPage
}
@@ -1,10 +0,0 @@
namespace Wino.Core.Domain.Enums;
public enum WinoServerConnectionStatus
{
None,
Connecting,
Connected,
Disconnected,
Failed
}
@@ -1,8 +0,0 @@
using System;
namespace Wino.Core.Domain.Exceptions;
/// <summary>
/// An exception thrown when the background task registration is failed.
/// </summary>
public class BackgroundTaskRegistrationFailedException : Exception { }
@@ -0,0 +1,10 @@
using System;
namespace Wino.Core.Domain.Exceptions;
public sealed class CalendarEventComposeValidationException : Exception
{
public CalendarEventComposeValidationException(string message) : base(message)
{
}
}
@@ -9,22 +9,18 @@ public class ImapClientPoolException : Exception
{
}
public ImapClientPoolException(string message, CustomServerInformation customServerInformation, string protocolLog) : base(message)
public ImapClientPoolException(string message, CustomServerInformation customServerInformation) : base(message)
{
CustomServerInformation = customServerInformation;
ProtocolLog = protocolLog;
}
public ImapClientPoolException(string message, string protocolLog) : base(message)
public ImapClientPoolException(string message) : base(message)
{
ProtocolLog = protocolLog;
}
public ImapClientPoolException(Exception innerException, string protocolLog) : base(innerException.Message, innerException)
public ImapClientPoolException(Exception innerException) : base(innerException.Message, innerException)
{
ProtocolLog = protocolLog;
}
public CustomServerInformation CustomServerInformation { get; }
public string ProtocolLog { get; }
}
@@ -1,17 +0,0 @@
using Wino.Core.Domain.Models.AutoDiscovery;
namespace Wino.Core.Domain.Exceptions;
public class ImapConnectionFailedPackage
{
public ImapConnectionFailedPackage(string errorMessage, string protocolLog, AutoDiscoverySettings settings)
{
ErrorMessage = errorMessage;
ProtocolLog = protocolLog;
Settings = settings;
}
public AutoDiscoverySettings Settings { get; }
public string ErrorMessage { get; set; }
public string ProtocolLog { get; }
}
@@ -1,11 +0,0 @@
using System;
namespace Wino.Core.Domain.Exceptions;
/// <summary>
/// All server crash types. Wino Server ideally should not throw anything else than this Exception type.
/// </summary>
public class WinoServerException : Exception
{
public WinoServerException(string message) : base(message) { }
}
@@ -0,0 +1,67 @@
using System;
namespace Wino.Core.Domain.Extensions;
public static class CalendarRemoteEventIdExtensions
{
private const string ClientTrackingSeparator = "::";
private const string CalDavClientTrackingPrefix = "caldav-";
private const string LocalClientTrackingPrefix = "local-";
public static string GetProviderRemoteEventId(this string remoteEventId)
{
if (string.IsNullOrWhiteSpace(remoteEventId))
return string.Empty;
var separatorIndex = remoteEventId.IndexOf(ClientTrackingSeparator, StringComparison.Ordinal);
return separatorIndex >= 0 ? remoteEventId[..separatorIndex] : remoteEventId;
}
public static Guid? GetClientTrackingId(this string remoteEventId)
{
if (string.IsNullOrWhiteSpace(remoteEventId))
return null;
if (remoteEventId.Contains(ClientTrackingSeparator, StringComparison.Ordinal))
{
var trackedPart = remoteEventId[(remoteEventId.LastIndexOf(ClientTrackingSeparator, StringComparison.Ordinal) + ClientTrackingSeparator.Length)..];
if (TryParseGuid(trackedPart, out var trackedId))
return trackedId;
}
if (TryParseGuid(remoteEventId, out var directId))
return directId;
if (remoteEventId.StartsWith(CalDavClientTrackingPrefix, StringComparison.OrdinalIgnoreCase) &&
TryParseGuid(remoteEventId[CalDavClientTrackingPrefix.Length..], out var calDavId))
{
return calDavId;
}
if (remoteEventId.StartsWith(LocalClientTrackingPrefix, StringComparison.OrdinalIgnoreCase) &&
TryParseGuid(remoteEventId[LocalClientTrackingPrefix.Length..], out var localId))
{
return localId;
}
return null;
}
public static string WithClientTrackingId(this string providerRemoteEventId, Guid? clientTrackingId)
{
if (string.IsNullOrWhiteSpace(providerRemoteEventId) || !clientTrackingId.HasValue)
return providerRemoteEventId ?? string.Empty;
return $"{providerRemoteEventId}{ClientTrackingSeparator}{clientTrackingId.Value:N}";
}
private static bool TryParseGuid(string value, out Guid parsedGuid)
{
parsedGuid = Guid.Empty;
if (string.IsNullOrWhiteSpace(value))
return false;
return Guid.TryParseExact(value, "N", out parsedGuid) || Guid.TryParse(value, out parsedGuid);
}
}
@@ -1,4 +1,5 @@
using System;
using System;
using Wino.Core.Domain.Entities.Calendar;
using Wino.Core.Domain.Models.Calendar;
namespace Wino.Core.Domain.Extensions;
@@ -29,4 +30,60 @@ public static class DateTimeExtensions
// Start loading from this date instead of visible date.
return date.AddDays(-diff).Date;
}
/// <summary>
/// Converts a datetime from source timezone into local timezone.
/// If timezone lookup fails, returns original value.
/// </summary>
public static DateTime ToLocalTimeFromTimeZone(this DateTime dateTime, string sourceTimeZoneId)
{
if (string.IsNullOrWhiteSpace(sourceTimeZoneId))
return dateTime;
try
{
var sourceTimeZone = TimeZoneInfo.FindSystemTimeZoneById(sourceTimeZoneId);
var localTimeZone = TimeZoneInfo.Local;
var unspecifiedDateTime = DateTime.SpecifyKind(dateTime, DateTimeKind.Unspecified);
return TimeZoneInfo.ConvertTime(unspecifiedDateTime, sourceTimeZone, localTimeZone);
}
catch
{
return dateTime;
}
}
/// <summary>
/// Converts local datetime into target timezone.
/// If timezone lookup fails, returns original value.
/// </summary>
public static DateTime ToTimeZoneFromLocal(this DateTime localDateTime, string targetTimeZoneId)
{
if (string.IsNullOrWhiteSpace(targetTimeZoneId))
return localDateTime;
try
{
var sourceTimeZone = TimeZoneInfo.Local;
var targetTimeZone = TimeZoneInfo.FindSystemTimeZoneById(targetTimeZoneId);
var unspecifiedDateTime = DateTime.SpecifyKind(localDateTime, DateTimeKind.Unspecified);
return TimeZoneInfo.ConvertTime(unspecifiedDateTime, sourceTimeZone, targetTimeZone);
}
catch
{
return localDateTime;
}
}
public static DateTime GetLocalStartDate(this CalendarItem calendarItem)
=> calendarItem.IsAllDayEvent
? calendarItem.StartDate
: calendarItem.StartDate.ToLocalTimeFromTimeZone(calendarItem.StartTimeZone);
public static DateTime GetLocalEndDate(this CalendarItem calendarItem)
=> calendarItem.IsAllDayEvent
? calendarItem.EndDate
: calendarItem.EndDate.ToLocalTimeFromTimeZone(calendarItem.EndTimeZone);
}
@@ -0,0 +1,42 @@
using System;
using System.Linq;
namespace Wino.Core.Domain.Extensions;
public static class MailHeaderExtensions
{
/// <summary>
/// Strips angle brackets from a Message-ID or In-Reply-To value.
/// RFC 5322 Message-IDs are formatted as &lt;id@domain&gt;, but MimeKit
/// properties store them without brackets. This normalizes raw header
/// values to match MimeKit's convention.
/// </summary>
public static string StripAngleBrackets(string value)
{
if (string.IsNullOrEmpty(value)) return value;
value = value.Trim();
if (value.StartsWith("<") && value.EndsWith(">"))
return value.Substring(1, value.Length - 2);
return value;
}
/// <summary>
/// Normalizes a raw RFC References header value into semicolon-separated Message-IDs
/// without angle brackets. Raw References headers contain space-separated bracketed IDs
/// like "&lt;id1@domain&gt; &lt;id2@domain&gt;". This converts them to "id1@domain;id2@domain".
/// </summary>
public static string NormalizeReferences(string rawReferences)
{
if (string.IsNullOrEmpty(rawReferences)) return rawReferences;
var ids = rawReferences
.Split(new[] { ' ', '\t', '\r', '\n', ';', ',' }, StringSplitOptions.RemoveEmptyEntries)
.Select(StripAngleBrackets)
.Where(id => !string.IsNullOrEmpty(id));
return string.Join(";", ids);
}
}
@@ -1,4 +1,6 @@
using System;
using Wino.Core.Domain.Entities.Shared;
using Wino.Core.Domain.Enums;
namespace Wino.Core.Domain.Interfaces;
@@ -8,8 +10,11 @@ public interface IAccountCalendar
string TextColorHex { get; set; }
string BackgroundColorHex { get; set; }
bool IsPrimary { get; set; }
bool IsSynchronizationEnabled { get; set; }
Guid AccountId { get; set; }
string RemoteCalendarId { get; set; }
bool IsExtended { get; set; }
CalendarItemShowAs DefaultShowAs { get; set; }
Guid Id { get; set; }
MailAccount MailAccount { get; set; }
}
@@ -7,7 +7,27 @@ namespace Wino.Core.Domain.Interfaces;
public interface IAccountMenuItem : IMenuItem
{
bool IsEnabled { get; set; }
double SynchronizationProgress { get; set; }
/// <summary>
/// Calculated synchronization progress percentage (0-100). -1 for indeterminate.
/// </summary>
double SynchronizationProgress { get; }
/// <summary>
/// Total items to sync. 0 for indeterminate progress.
/// </summary>
int TotalItemsToSync { get; set; }
/// <summary>
/// Remaining items to sync.
/// </summary>
int RemainingItemsToSync { get; set; }
/// <summary>
/// Current synchronization status message.
/// </summary>
string SynchronizationStatus { get; set; }
int UnreadItemCount { get; set; }
IEnumerable<MailAccount> HoldingAccounts { get; }
void UpdateAccount(MailAccount account);
@@ -171,4 +171,19 @@ public interface IAccountService
/// <returns>Whether the notifications should be created after sync or not.</returns>
Task<bool> IsNotificationsEnabled(Guid accountId);
Task UpdateAccountCustomServerInformationAsync(CustomServerInformation customServerInformation);
/// <summary>
/// Updates the last folder structure sync date for the given account.
/// Used for optimization to skip folder sync if it was done recently.
/// </summary>
/// <param name="accountId">Account id.</param>
Task UpdateLastFolderStructureSyncDateAsync(Guid accountId);
/// <summary>
/// Checks if folder structure should be synced based on the configured interval.
/// Returns true if LastFolderStructureSyncDate is null or older than the interval.
/// </summary>
/// <param name="accountId">Account id.</param>
/// <param name="syncInterval">Minimum interval between folder syncs.</param>
Task<bool> ShouldSyncFolderStructureAsync(Guid accountId, TimeSpan syncInterval);
}
@@ -0,0 +1,10 @@
using System.Collections.Generic;
using Wino.Core.Domain.Models.Ai;
namespace Wino.Core.Domain.Interfaces;
public interface IAiActionOptionsService
{
IReadOnlyList<AiTranslateLanguageOption> GetTranslateLanguageOptions();
IReadOnlyList<AiRewriteModeOption> GetRewriteModeOptions();
}
@@ -1,17 +1,22 @@
using System.Threading.Tasks;
using System;
using System.Threading;
using System.Threading.Tasks;
using Wino.Core.Domain.Models.AutoDiscovery;
namespace Wino.Core.Domain.Interfaces;
/// <summary>
/// Searches for Auto Discovery settings for custom mail accounts.
/// Searches for auto-discovery settings for custom mail accounts.
/// </summary>
public interface IAutoDiscoveryService
{
/// <summary>
/// Tries to return the best mail server settings using different techniques.
/// </summary>
/// <param name="mailAddress">Address to search settings for.</param>
/// <returns>CustomServerInformation with only settings applied.</returns>
Task<AutoDiscoverySettings> GetAutoDiscoverySettings(AutoDiscoveryMinimalSettings autoDiscoveryMinimalSettings);
/// <summary>
/// Tries to resolve a CalDAV endpoint for the mailbox address.
/// </summary>
Task<Uri> DiscoverCalDavServiceUriAsync(string mailAddress, CancellationToken cancellationToken = default);
}
@@ -1,17 +0,0 @@
using System.Threading.Tasks;
namespace Wino.Core.Domain.Interfaces;
public interface IBackgroundTaskService
{
/// <summary>
/// Unregisters all background tasks once.
/// This is used to clean up the background tasks when the app is updated.
/// </summary>
void UnregisterAllBackgroundTask();
/// <summary>
/// Registers required background tasks.
/// </summary>
Task RegisterBackgroundTasksAsync();
}
@@ -1,4 +1,6 @@
using System.Threading.Tasks;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Wino.Core.Domain.Entities.Shared;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Models.Accounts;
@@ -23,6 +25,23 @@ public interface IBaseSynchronizer
/// <param name="request">Request to queue.</param>
void QueueRequest(IRequestBase request);
/// <summary>
/// Returns whether there is an in-progress (queued or currently executing) operation for the given mail unique id.
/// </summary>
/// <param name="mailUniqueId">Mail unique id to check.</param>
bool HasPendingOperation(Guid mailUniqueId);
/// <summary>
/// Returns mail unique ids that currently have queued or executing operations.
/// </summary>
IReadOnlyCollection<Guid> GetPendingOperationUniqueIds();
/// <summary>
/// Returns whether there is an in-progress (queued or currently executing) operation for the given calendar item id.
/// </summary>
/// <param name="calendarItemId">Calendar item id to check.</param>
bool HasPendingCalendarOperation(Guid calendarItemId);
/// <summary>
/// Synchronizes profile information with the server.
/// Sender name and Profile picture are updated.
@@ -0,0 +1,35 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Wino.Core.Domain.Models.Calendar;
namespace Wino.Core.Domain.Interfaces;
public interface ICalDavClient
{
Task<IReadOnlyList<CalDavCalendar>> DiscoverCalendarsAsync(
CalDavConnectionSettings connectionSettings,
CancellationToken cancellationToken = default);
Task<IReadOnlyList<CalDavCalendarEvent>> GetCalendarEventsAsync(
CalDavConnectionSettings connectionSettings,
CalDavCalendar calendar,
DateTimeOffset startUtc,
DateTimeOffset endUtc,
CancellationToken cancellationToken = default);
Task UpsertCalendarEventAsync(
CalDavConnectionSettings connectionSettings,
CalDavCalendar calendar,
string remoteEventId,
string icsContent,
CancellationToken cancellationToken = default);
Task DeleteCalendarEventAsync(
CalDavConnectionSettings connectionSettings,
CalDavCalendar calendar,
string remoteEventId,
CancellationToken cancellationToken = default);
}
@@ -1,5 +0,0 @@
namespace Wino.Core.Domain.Interfaces;
public interface ICalendarDialogService : IDialogServiceBase
{
}
@@ -0,0 +1,15 @@
using System;
using System.Threading.Tasks;
namespace Wino.Core.Domain.Interfaces;
/// <summary>
/// Persists CalDAV ICS payloads on disk for IMAP accounts.
/// </summary>
public interface ICalendarIcsFileService
{
Task SaveCalendarItemIcsAsync(Guid accountId, Guid calendarId, Guid calendarItemId, string remoteEventId, string remoteResourceHref, string eTag, string icsContent);
Task<string> GetCalendarItemIcsETagAsync(Guid accountId, Guid calendarId, Guid calendarItemId);
Task DeleteCalendarItemIcsAsync(Guid accountId, Guid calendarItemId);
Task DeleteCalendarIcsForCalendarAsync(Guid accountId, Guid calendarId);
}
@@ -1,5 +1,6 @@
using System;
using Itenso.TimePeriod;
using Wino.Core.Domain.Models.Calendar;
namespace Wino.Core.Domain.Interfaces;
@@ -19,4 +20,13 @@ public interface ICalendarItem
bool IsRecurringChild { get; }
bool IsRecurringParent { get; }
bool IsRecurringEvent { get; }
/// <summary>
/// Gets the display title for this calendar item when rendered in a specific day.
/// For multi-day events, includes start/end time indicators.
/// </summary>
/// <param name="displayingPeriod">The period of the day where this item is being rendered.</param>
/// <param name="calendarSettings">Calendar settings for time formatting.</param>
/// <returns>The formatted title string.</returns>
string GetDisplayTitle(ITimePeriod displayingPeriod, CalendarSettings calendarSettings);
}
@@ -1,4 +1,8 @@
namespace Wino.Core.Domain.Interfaces;
using Itenso.TimePeriod;
using Wino.Core.Domain.Entities.Calendar;
using Wino.Core.Domain.Models.Calendar;
namespace Wino.Core.Domain.Interfaces;
/// <summary>
/// Temporarily to enforce CalendarItemViewModel. Used in CalendarEventCollection.
@@ -6,4 +10,23 @@
public interface ICalendarItemViewModel
{
bool IsSelected { get; set; }
bool IsBusy { get; set; }
/// <summary>
/// The period of the day where this item is currently being displayed.
/// </summary>
ITimePeriod DisplayingPeriod { get; set; }
/// <summary>
/// Calendar settings for time formatting.
/// </summary>
CalendarSettings CalendarSettings { get; set; }
/// <summary>
/// Updates the view model's underlying CalendarItem from new data.
/// This allows in-place updates without removing and re-adding items.
/// </summary>
/// <param name="calendarItem">The updated calendar item data.</param>
void UpdateFrom(CalendarItem calendarItem);
}
@@ -1,6 +1,8 @@
using System;
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Itenso.TimePeriod;
using Wino.Core.Domain.Entities.Calendar;
using Wino.Core.Domain.Models.Calendar;
@@ -11,12 +13,22 @@ public interface ICalendarService
Task<List<AccountCalendar>> GetAccountCalendarsAsync(Guid accountId);
Task<AccountCalendar> GetAccountCalendarAsync(Guid accountCalendarId);
Task DeleteCalendarItemAsync(Guid calendarItemId);
Task DeleteCalendarItemAsync(string calendarRemoteEventId, Guid calendarId);
Task DeleteAccountCalendarAsync(AccountCalendar accountCalendar);
Task InsertAccountCalendarAsync(AccountCalendar accountCalendar);
Task UpdateAccountCalendarAsync(AccountCalendar accountCalendar);
Task SetPrimaryCalendarAsync(Guid accountId, Guid accountCalendarId);
Task CreateNewCalendarItemAsync(CalendarItem calendarItem, List<CalendarEventAttendee> attendees);
Task<List<CalendarItem>> GetCalendarEventsAsync(IAccountCalendar calendar, DayRangeRenderModel dayRangeRenderModel);
/// <summary>
/// Retrieves calendar events for a given calendar within the specified time period.
/// </summary>
/// <param name="calendar">The calendar to retrieve events from.</param>
/// <param name="period">The time period to query events for.</param>
/// <returns>List of calendar items that fall within the requested period.</returns>
Task<List<CalendarItem>> GetCalendarEventsAsync(IAccountCalendar calendar, ITimePeriod period);
Task<CalendarItem> GetCalendarItemAsync(Guid accountCalendarId, string remoteEventId);
Task UpdateCalendarDeltaSynchronizationToken(Guid calendarId, string deltaToken);
@@ -28,4 +40,43 @@ public interface ICalendarService
Task<CalendarItem> GetCalendarItemAsync(Guid id);
Task<List<CalendarEventAttendee>> GetAttendeesAsync(Guid calendarEventTrackingId);
Task<List<CalendarEventAttendee>> ManageEventAttendeesAsync(Guid calendarItemId, List<CalendarEventAttendee> allAttendees);
Task UpdateCalendarItemAsync(CalendarItem calendarItem, List<CalendarEventAttendee> attendees);
Task<List<CalendarItem>> SearchCalendarItemsAsync(string searchQuery, int limit, CancellationToken cancellationToken = default);
Task<List<Reminder>> GetRemindersAsync(Guid calendarItemId);
Task SaveRemindersAsync(Guid calendarItemId, List<Reminder> reminders);
Task SnoozeCalendarItemAsync(Guid calendarItemId, DateTime snoozedUntilLocal);
/// <summary>
/// Checks due reminder windows and returns reminder notifications that should trigger now.
/// </summary>
Task<List<CalendarReminderNotificationRequest>> CheckAndNotifyAsync(DateTime lastCheckLocal, DateTime nowLocal, ISet<string> sentReminderKeys, CancellationToken cancellationToken = default);
/// <summary>
/// Gets predefined reminder options in minutes (1 Hour, 30 Min, 15 Min, 5 Min, 1 Min).
/// </summary>
int[] GetPredefinedReminderMinutes();
#region Attachments
/// <summary>
/// Gets all attachments for a calendar event.
/// </summary>
Task<List<CalendarAttachment>> GetAttachmentsAsync(Guid calendarItemId);
/// <summary>
/// Inserts or updates calendar attachments.
/// </summary>
Task InsertOrReplaceAttachmentsAsync(List<CalendarAttachment> attachments);
/// <summary>
/// Marks an attachment as downloaded and updates its local file path.
/// </summary>
Task MarkAttachmentDownloadedAsync(Guid attachmentId, string localFilePath);
/// <summary>
/// Deletes all attachments for a calendar item.
/// </summary>
Task DeleteAttachmentsAsync(Guid calendarItemId);
#endregion
}
@@ -0,0 +1,26 @@
using System;
using System.Threading.Tasks;
namespace Wino.Core.Domain.Interfaces;
/// <summary>
/// Manages contact picture files stored on disk instead of as base64 in SQLite,
/// eliminating DB bloat and enabling native WIC hardware-accelerated image loading.
/// </summary>
public interface IContactPictureFileService
{
/// <summary>
/// Returns the full file path for the given file ID, or null if the file does not exist on disk.
/// </summary>
string GetContactPicturePath(Guid fileId);
/// <summary>
/// Saves raw image bytes to disk and returns the new file ID.
/// </summary>
Task<Guid> SaveContactPictureAsync(byte[] imageData);
/// <summary>
/// Deletes the picture file for the given file ID if it exists.
/// </summary>
Task DeleteContactPictureAsync(Guid fileId);
}
+28 -1
View File
@@ -1,7 +1,10 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using MimeKit;
using Wino.Core.Domain.Entities.Shared;
using Wino.Core.Domain.Models.Contacts;
namespace Wino.Core.Domain.Interfaces;
@@ -9,6 +12,30 @@ public interface IContactService
{
Task<List<AccountContact>> GetAddressInformationAsync(string queryText);
Task<AccountContact> GetAddressInformationByAddressAsync(string address);
Task<List<AccountContact>> GetContactsByAddressesAsync(IEnumerable<string> addresses);
Task SaveAddressInformationAsync(MimeMessage message);
Task SaveAddressInformationAsync(IEnumerable<AccountContact> contacts);
Task<AccountContact> CreateNewContactAsync(string address, string displayName);
// Paged contact queries for ContactsPage
Task<List<AccountContact>> GetAllContactsAsync();
Task<List<AccountContact>> SearchContactsAsync(string searchQuery);
Task<PagedContactsResult> GetContactsPageAsync(int offset, int pageSize, string searchQuery = null, bool excludeRootContacts = false);
Task<AccountContact> UpdateContactAsync(AccountContact contact);
Task DeleteContactAsync(string address);
Task DeleteContactsAsync(IEnumerable<string> addresses);
// Group / distribution list support
Task<List<ContactGroup>> GetGroupsAsync();
Task<ContactGroup> CreateGroupAsync(string name, string description = null);
Task DeleteGroupAsync(Guid groupId);
Task<List<AccountContact>> GetGroupMembersAsync(Guid groupId);
Task AddGroupMemberAsync(Guid groupId, string memberAddress);
Task RemoveGroupMemberAsync(Guid groupId, string memberAddress);
/// <summary>
/// Expands a contact group to the individual <see cref="AccountContact"/> entries of its members.
/// Returns an empty list if the group does not exist or has no members.
/// </summary>
Task<List<AccountContact>> ExpandGroupAsync(Guid groupId);
}
@@ -1,6 +1,6 @@
using System.Collections.Generic;
using Wino.Core.Domain.Entities.Mail;
using Wino.Core.Domain.Models.Folders;
using Wino.Core.Domain.Models.MailItem;
using Wino.Core.Domain.Models.Menus;
namespace Wino.Core.Domain.Interfaces;
@@ -8,6 +8,6 @@ namespace Wino.Core.Domain.Interfaces;
public interface IContextMenuItemService
{
IEnumerable<FolderOperationMenuItem> GetFolderContextMenuActions(IBaseFolderMenuItem folderInformation);
IEnumerable<MailOperationMenuItem> GetMailItemContextMenuActions(IEnumerable<IMailItem> selectedMailItems);
IEnumerable<MailOperationMenuItem> GetMailItemRenderMenuActions(IMailItem mailItem, bool isDarkEditor);
IEnumerable<MailOperationMenuItem> GetMailItemContextMenuActions(IEnumerable<MailCopy> selectedMailItems);
IEnumerable<MailOperationMenuItem> GetMailItemRenderMenuActions(MailCopy mailItem, bool isDarkEditor);
}
@@ -1,6 +1,6 @@
using System.Collections.Generic;
using Wino.Core.Domain.Entities.Mail;
using Wino.Core.Domain.Models.Folders;
using Wino.Core.Domain.Models.MailItem;
using Wino.Core.Domain.Models.Menus;
namespace Wino.Core.Domain.Interfaces;
@@ -18,12 +18,12 @@ public interface IContextMenuProvider
/// </summary>
/// <param name="folderInformation">Current folder that asks for the menu items.</param>
/// <param name="selectedMailItems">Selected menu items in the given folder.</param>
IEnumerable<MailOperationMenuItem> GetMailItemContextMenuActions(IMailItemFolder folderInformation, IEnumerable<IMailItem> selectedMailItems);
IEnumerable<MailOperationMenuItem> GetMailItemContextMenuActions(IMailItemFolder folderInformation, IEnumerable<MailCopy> selectedMailItems);
/// <summary>
/// Calculates and returns available mail operations for mail rendering CommandBar.
/// </summary>
/// <param name="mailItem">Rendered mail item.</param>
/// <param name="activeFolder">Folder that mail item belongs to.</param>
IEnumerable<MailOperationMenuItem> GetMailItemRenderMenuActions(IMailItem mailItem, IMailItemFolder activeFolder, bool isDarkEditor);
IEnumerable<MailOperationMenuItem> GetMailItemRenderMenuActions(MailCopy mailItem, IMailItemFolder activeFolder, bool isDarkEditor);
}
@@ -4,6 +4,7 @@ using System.Threading.Tasks;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Models.Accounts;
using Wino.Core.Domain.Models.Common;
using Wino.Core.Domain.Models.Printing;
namespace Wino.Core.Domain.Interfaces;
@@ -27,5 +28,7 @@ public interface IDialogServiceBase
Task<AccountCreationDialogResult> ShowAccountProviderSelectionDialogAsync(List<IProviderDetail> availableProviders);
IAccountCreationDialog GetAccountCreationDialog(AccountCreationDialogResult accountCreationDialogResult);
Task<List<SharedFile>> PickFilesAsync(params object[] typeFilters);
Task<List<PickedFileMetadata>> PickFilesMetadataAsync(params object[] typeFilters);
Task<string> PickFilePathAsync(string saveFileName);
Task<WebView2PrintSettingsModel> ShowPrintDialogAsync(WebView2PrintSettingsModel initialSettings = null);
}
@@ -0,0 +1,15 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Wino.Core.Domain.Entities.Mail;
namespace Wino.Core.Domain.Interfaces;
public interface IEmailTemplateService
{
Task<List<EmailTemplate>> GetEmailTemplatesAsync();
Task<EmailTemplate> GetEmailTemplateAsync(Guid templateId);
Task<EmailTemplate> CreateEmailTemplateAsync(EmailTemplate template);
Task<EmailTemplate> UpdateEmailTemplateAsync(EmailTemplate template);
Task<EmailTemplate> DeleteEmailTemplateAsync(EmailTemplate template);
}
@@ -1,4 +1,5 @@
using System.Collections.Generic;
using System.Collections.ObjectModel;
using Wino.Core.Domain.Entities.Shared;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Models.Folders;
@@ -20,7 +21,7 @@ public interface IBaseFolderMenuItem : IMenuItem
int UnreadItemCount { get; set; }
SpecialFolderType SpecialFolderType { get; }
IEnumerable<IMailItemFolder> HandlingFolders { get; }
IEnumerable<IMenuItem> SubMenuItems { get; }
ObservableCollection<IMenuItem> SubMenuItems { get; }
bool IsMoveTarget { get; }
bool IsSticky { get; }
bool IsSystemFolder { get; }
@@ -79,6 +79,13 @@ public interface IFolderService
/// <param name="folder">Folder to update.</param>
Task UpdateFolderAsync(MailItemFolder folder);
/// <summary>
/// Updates only IMAP HighestModeSeq for the given folder.
/// </summary>
/// <param name="folderId">Folder id to update.</param>
/// <param name="highestModeSeq">Latest known mod-seq value.</param>
Task UpdateFolderHighestModeSeqAsync(Guid folderId, long highestModeSeq);
/// <summary>
/// Returns the active folder menu items for the given account for UI.
/// </summary>
@@ -1,3 +0,0 @@
namespace Wino.Core.Domain.Interfaces;
public interface IGmailThreadingStrategy : IThreadingStrategy { }
@@ -1,24 +0,0 @@
using System.Threading.Tasks;
using Wino.Core.Domain.Entities.Shared;
namespace Wino.Core.Domain.Interfaces;
public interface IImapAccountCreationDialog : IAccountCreationDialog
{
/// <summary>
/// Returns the custom server information from the dialog..
/// </summary>
/// <returns>Null if canceled.</returns>
Task<CustomServerInformation> GetCustomServerInformationAsync();
/// <summary>
/// Displays preparing folders page.
/// </summary>
void ShowPreparingFolders();
/// <summary>
/// Updates account properties for the welcome imap setup dialog and starts the setup.
/// </summary>
/// <param name="account">Account properties.</param>
void StartImapConnectionSetup(MailAccount account);
}
@@ -1,11 +0,0 @@
using MailKit.Net.Imap;
namespace Wino.Core.Domain.Interfaces;
/// <summary>
/// Provides a synchronization strategy for synchronizing IMAP folders based on the server capabilities.
/// </summary>
public interface IImapSynchronizationStrategyProvider
{
IImapSynchronizerStrategy GetSynchronizationStrategy(IImapClient client);
}
@@ -1,40 +0,0 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using MailKit;
using MailKit.Net.Imap;
using Wino.Core.Domain.Entities.Mail;
namespace Wino.Core.Domain.Interfaces;
public interface IImapSynchronizerStrategy
{
/// <summary>
/// Synchronizes given folder with the ImapClient client from the client pool.
/// </summary>
/// <param name="client">Client to perform sync with. I love Mira and Jasminka</param>
/// <param name="folder">Folder to synchronize.</param>
/// <param name="synchronizer">Imap synchronizer that downloads messages.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>List of new downloaded message ids that don't exist locally.</returns>
Task<List<string>> HandleSynchronizationAsync(IImapClient client,
MailItemFolder folder,
IImapSynchronizer synchronizer,
CancellationToken cancellationToken = default);
/// <summary>
/// Downloads given set of messages from the folder.
/// Folder is expected to be opened and synchronizer is connected.
/// </summary>
/// <param name="synchronizer">Synchronizer that performs the action.</param>
/// <param name="remoteFolder">Remote folder to download messages from.</param>
/// <param name="localFolder">Local folder to assign mails to.</param>
/// <param name="uniqueIdSet">Set of message uniqueids.</param>
/// <param name="cancellationToken">Cancellation token.</param>
Task DownloadMessagesAsync(IImapSynchronizer synchronizer,
IMailFolder remoteFolder,
MailItemFolder localFolder,
UniqueIdSet uniqueIdSet,
CancellationToken cancellationToken = default);
}
@@ -1,3 +0,0 @@
namespace Wino.Core.Domain.Interfaces;
public interface IImapThreadingStrategy : IThreadingStrategy { }
@@ -0,0 +1,67 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Wino.Core.Domain.Entities.Shared;
using Wino.Core.Domain.Enums;
namespace Wino.Core.Domain.Interfaces;
/// <summary>
/// Service for managing keyboard shortcuts for mail operations.
/// </summary>
public interface IKeyboardShortcutService
{
/// <summary>
/// Gets all available keyboard shortcuts.
/// </summary>
/// <returns>Collection of keyboard shortcuts.</returns>
Task<IEnumerable<KeyboardShortcut>> GetKeyboardShortcutsAsync();
/// <summary>
/// Gets enabled keyboard shortcuts only.
/// </summary>
/// <returns>Collection of enabled keyboard shortcuts.</returns>
Task<IEnumerable<KeyboardShortcut>> GetEnabledKeyboardShortcutsAsync();
/// <summary>
/// Creates or updates a keyboard shortcut.
/// </summary>
/// <param name="shortcut">The keyboard shortcut to save.</param>
/// <returns>The saved keyboard shortcut.</returns>
Task<KeyboardShortcut> SaveKeyboardShortcutAsync(KeyboardShortcut shortcut);
/// <summary>
/// Deletes a keyboard shortcut.
/// </summary>
/// <param name="shortcutId">The ID of the shortcut to delete.</param>
Task DeleteKeyboardShortcutAsync(Guid shortcutId);
/// <summary>
/// Gets the keyboard shortcut for the given key combination in a specific mode.
/// </summary>
/// <param name="mode">The application mode to search within.</param>
/// <param name="key">The pressed key.</param>
/// <param name="modifierKeys">The modifier keys pressed.</param>
/// <returns>The matching shortcut if found, otherwise null.</returns>
Task<KeyboardShortcut> GetShortcutForKeyAsync(WinoApplicationMode mode, string key, ModifierKeys modifierKeys);
/// <summary>
/// Checks if a key combination is already assigned to another shortcut.
/// </summary>
/// <param name="mode">The application mode to check within.</param>
/// <param name="key">The key to check.</param>
/// <param name="modifierKeys">The modifier keys to check.</param>
/// <param name="excludeShortcutId">Optional ID to exclude from the check (for updates).</param>
/// <returns>True if the combination is already used, false otherwise.</returns>
Task<bool> IsKeyCombinationInUseAsync(WinoApplicationMode mode, string key, ModifierKeys modifierKeys, Guid? excludeShortcutId = null);
/// <summary>
/// Creates default keyboard shortcuts for common mail operations.
/// </summary>
Task CreateDefaultShortcutsAsync();
/// <summary>
/// Resets all shortcuts to defaults.
/// </summary>
Task ResetToDefaultShortcutsAsync();
}
@@ -1,4 +1,5 @@
using Wino.Core.Domain.Models.Launch;
using System;
using Wino.Core.Domain.Models.Launch;
namespace Wino.Core.Domain.Interfaces;
@@ -13,4 +14,5 @@ public interface ILaunchProtocolService
/// Used to handle mailto links.
/// </summary>
MailToUri MailToUri { get; set; }
}
@@ -1,10 +1,15 @@
using System;
#nullable enable
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Threading.Tasks;
using Wino.Core.Domain.Entities.Calendar;
using Wino.Core.Domain.Entities.Mail;
using Wino.Core.Domain.Entities.Shared;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Models;
using Wino.Core.Domain.Models.Accounts;
using Wino.Core.Domain.Models.Calendar;
using Wino.Core.Domain.Models.Folders;
namespace Wino.Core.Domain.Interfaces;
@@ -17,6 +22,7 @@ public interface IMailDialogService : IDialogServiceBase
// Custom dialogs
Task<IMailItemFolder> ShowMoveMailFolderDialogAsync(List<IMailItemFolder> availableFolders);
Task<MailAccount> ShowAccountPickerDialogAsync(List<MailAccount> availableAccounts);
Task<AccountCalendarPickingResult> ShowSingleCalendarPickerDialogAsync(List<CalendarPickerAccountGroup> availableCalendarGroups);
/// <summary>
/// Displays a dialog to the user for reordering accounts.
@@ -37,7 +43,7 @@ public interface IMailDialogService : IDialogServiceBase
/// Presents a dialog to the user for signature creation/modification.
/// </summary>
/// <returns>Signature information. Null if canceled.</returns>
Task<AccountSignature> ShowSignatureEditorDialog(AccountSignature signatureModel = null);
Task<AccountSignature> ShowSignatureEditorDialog(AccountSignature? signatureModel = null);
/// <summary>
/// Presents a dialog to the user for account alias creation/modification.
@@ -49,4 +55,26 @@ public interface IMailDialogService : IDialogServiceBase
/// Presents a dialog to the user to show email source.
/// </summary>
Task ShowMessageSourceDialogAsync(string messageSource);
/// <summary>
/// Presents a dialog to the user for keyboard shortcut creation/modification.
/// </summary>
/// <param name="existingShortcut">Existing shortcut to edit, or null for new shortcut.</param>
/// <returns>Dialog result with shortcut information.</returns>
#pragma warning disable CS8625
Task<KeyboardShortcutDialogResult> ShowKeyboardShortcutDialogAsync(KeyboardShortcut existingShortcut = null);
#pragma warning restore CS8625
/// <summary>
/// Presents a dialog to the user for contact creation/modification.
/// </summary>
/// <param name="contact">Existing contact to edit, or null for new contact.</param>
/// <returns>Contact information. Null if canceled.</returns>
Task<AccountContact?> ShowEditContactDialogAsync(AccountContact? contact = null);
Task<WinoAccount?> ShowWinoAccountRegistrationDialogAsync();
Task<WinoAccount?> ShowWinoAccountLoginDialogAsync();
Task<WinoAccountSyncExportResult?> ShowWinoAccountExportDialogAsync();
}
@@ -0,0 +1,9 @@
using System;
using System.Collections.Generic;
namespace Wino.Core.Domain.Interfaces;
public interface IMailHashContainer
{
IEnumerable<Guid> GetContainingIds();
}
@@ -0,0 +1,27 @@
using System;
using System.ComponentModel;
using Wino.Core.Domain.Entities.Shared;
namespace Wino.Core.Domain.Interfaces;
/// <summary>
/// Shared display contract for mail list item rendering.
/// Implemented by both single mail and thread mail view models.
/// </summary>
public interface IMailItemDisplayInformation : INotifyPropertyChanged
{
string Subject { get; }
string FromName { get; }
string FromAddress { get; }
string PreviewText { get; }
bool IsRead { get; }
bool IsDraft { get; }
bool HasAttachments { get; }
bool IsCalendarEvent { get; }
bool IsFlagged { get; }
DateTime CreationDate { get; }
Guid? ContactPictureFileId { get; }
bool ThumbnailUpdatedEvent { get; }
bool IsThreadExpanded { get; }
AccountContact SenderContact { get; }
}
@@ -0,0 +1,9 @@
using System;
namespace Wino.Core.Domain.Interfaces;
public interface IMailListItemSorting
{
DateTime SortingDate { get; }
string SortingName { get; }
}

Some files were not shown because too many files have changed in this diff Show More