2024-04-18 01:44:37 +02:00
using System ;
using System.Collections.Generic ;
using System.Linq ;
2025-11-15 13:29:02 +01:00
using System.Text ;
2024-08-25 02:01:08 +02:00
using System.Threading ;
2024-04-18 01:44:37 +02:00
using System.Threading.Tasks ;
2025-07-26 12:51:53 +02:00
using CommunityToolkit.Mvvm.Messaging ;
2024-04-18 01:44:37 +02:00
using MimeKit ;
using Serilog ;
using Wino.Core.Domain ;
2024-11-10 23:28:25 +01:00
using Wino.Core.Domain.Entities.Mail ;
using Wino.Core.Domain.Entities.Shared ;
2024-04-18 01:44:37 +02:00
using Wino.Core.Domain.Enums ;
2024-08-17 22:55:58 +02:00
using Wino.Core.Domain.Exceptions ;
2024-08-05 00:36:26 +02:00
using Wino.Core.Domain.Extensions ;
2024-04-18 01:44:37 +02:00
using Wino.Core.Domain.Interfaces ;
2026-04-07 01:17:52 +02:00
using Wino.Core.Domain.Misc ;
2024-04-18 01:44:37 +02:00
using Wino.Core.Domain.Models.MailItem ;
2024-08-05 00:36:26 +02:00
using Wino.Messaging.UI ;
2024-11-30 23:05:07 +01:00
using Wino.Services.Extensions ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
namespace Wino.Services ;
public class MailService : BaseDatabaseService , IMailService
2024-04-18 01:44:37 +02:00
{
2025-02-16 11:54:23 +01:00
private const int ItemLoadCount = 100 ;
private readonly IFolderService _folderService ;
private readonly IContactService _contactService ;
private readonly IAccountService _accountService ;
private readonly ISignatureService _signatureService ;
private readonly IMimeFileService _mimeFileService ;
private readonly IPreferencesService _preferencesService ;
2026-04-11 21:02:51 +02:00
private readonly ISentMailReceiptService _sentMailReceiptService ;
2025-02-16 11:54:23 +01:00
private readonly ILogger _logger = Log . ForContext < MailService > ( ) ;
public MailService ( IDatabaseService databaseService ,
IFolderService folderService ,
IContactService contactService ,
IAccountService accountService ,
ISignatureService signatureService ,
IMimeFileService mimeFileService ,
2026-04-11 21:02:51 +02:00
IPreferencesService preferencesService ,
ISentMailReceiptService sentMailReceiptService ) : base ( databaseService )
2025-02-16 11:35:43 +01:00
{
2025-02-16 11:54:23 +01:00
_folderService = folderService ;
_contactService = contactService ;
_accountService = accountService ;
_signatureService = signatureService ;
_mimeFileService = mimeFileService ;
_preferencesService = preferencesService ;
2026-04-11 21:02:51 +02:00
_sentMailReceiptService = sentMailReceiptService ;
2025-02-16 11:54:23 +01:00
}
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
public async Task < ( MailCopy draftMailCopy , string draftBase64MimeMessage ) > CreateDraftAsync ( Guid accountId , DraftCreationOptions draftCreationOptions )
{
var composerAccount = await _accountService . GetAccountAsync ( accountId ) . ConfigureAwait ( false ) ;
2026-04-13 01:09:40 +02:00
var selectedAlias = await ResolveDraftAliasAsync ( composerAccount , draftCreationOptions ) . ConfigureAwait ( false ) ;
var createdDraftMimeMessage = await CreateDraftMimeAsync ( composerAccount , draftCreationOptions , selectedAlias ) . ConfigureAwait ( false ) ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
var draftFolder = await _folderService . GetSpecialFolderByAccountIdAsync ( composerAccount . Id , SpecialFolderType . Draft ) ;
2024-04-18 01:44:37 +02:00
2025-02-26 23:10:30 +01:00
if ( draftFolder = = null )
throw new UnavailableSpecialFolderException ( SpecialFolderType . Draft , accountId ) ;
2025-02-16 11:54:23 +01:00
// Get locally created unique id from the mime headers.
// This header will be used to map the local draft copy with the remote draft copy.
var mimeUniqueId = createdDraftMimeMessage . Headers [ Constants . WinoLocalDraftHeader ] ;
2025-02-16 11:43:30 +01:00
2025-02-16 11:54:23 +01:00
var primaryAlias = await _accountService . GetPrimaryAccountAliasAsync ( accountId ) . ConfigureAwait ( false ) ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
var copy = new MailCopy
{
UniqueId = Guid . Parse ( mimeUniqueId ) ,
Id = Guid . NewGuid ( ) . ToString ( ) , // This will be replaced after network call with the remote draft id.
CreationDate = DateTime . UtcNow ,
2026-04-13 01:09:40 +02:00
FromAddress = selectedAlias ? . AliasAddress ? ? primaryAlias ? . AliasAddress ? ? composerAccount . Address ,
FromName = selectedAlias ? . AliasSenderName ? ? composerAccount . SenderName ,
2025-02-16 11:54:23 +01:00
HasAttachments = false ,
Importance = MailImportance . Normal ,
Subject = createdDraftMimeMessage . Subject ,
PreviewText = createdDraftMimeMessage . TextBody ,
IsRead = true ,
IsDraft = true ,
FolderId = draftFolder . Id ,
DraftId = $"{Constants.LocalDraftStartPrefix}{Guid.NewGuid()}" ,
AssignedFolder = draftFolder ,
AssignedAccount = composerAccount ,
2026-04-07 01:17:52 +02:00
FileId = Guid . NewGuid ( ) ,
MessageId = GetNormalizedMimeMessageId ( createdDraftMimeMessage ) ,
InReplyTo = GetNormalizedMimeInReplyTo ( createdDraftMimeMessage ) ,
References = GetNormalizedMimeReferences ( createdDraftMimeMessage )
2025-02-16 11:54:23 +01:00
} ;
if ( draftCreationOptions . ReferencedMessage ! = null )
{
if ( ! string . IsNullOrEmpty ( draftCreationOptions . ReferencedMessage . MailCopy ? . ThreadId ) )
copy . ThreadId = draftCreationOptions . ReferencedMessage . MailCopy . ThreadId ;
2026-02-23 01:51:44 +01:00
// Fallback local threading when provider/native thread id is unavailable.
if ( string . IsNullOrWhiteSpace ( copy . ThreadId ) )
2026-04-07 01:17:52 +02:00
copy . ThreadId = MailHeaderExtensions . SplitMessageIds ( copy . References ) . FirstOrDefault ( ) ? ? copy . InReplyTo ;
2025-02-16 11:54:23 +01:00
}
2024-04-18 01:44:37 +02:00
2025-11-14 14:28:10 +01:00
await Connection . InsertAsync ( copy , typeof ( MailCopy ) ) ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
await _mimeFileService . SaveMimeMessageAsync ( copy . FileId , createdDraftMimeMessage , composerAccount . Id ) ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
ReportUIChange ( new DraftCreated ( copy , composerAccount ) ) ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
return ( copy , createdDraftMimeMessage . GetBase64MimeMessage ( ) ) ;
}
2024-07-09 01:05:16 +02:00
2025-02-16 11:54:23 +01:00
public async Task < List < MailCopy > > GetMailsByFolderIdAsync ( Guid folderId )
{
var mails = await Connection . QueryAsync < MailCopy > ( "SELECT * FROM MailCopy WHERE FolderId = ?" , folderId ) ;
2024-07-09 01:05:16 +02:00
2025-02-16 11:54:23 +01:00
foreach ( var mail in mails )
{
await LoadAssignedPropertiesAsync ( mail ) . ConfigureAwait ( false ) ;
2024-07-09 01:05:16 +02:00
}
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
return mails ;
}
2024-07-09 01:05:16 +02:00
2025-02-22 23:09:53 +01:00
public async Task < bool > HasAccountAnyDraftAsync ( Guid accountId )
{
// Get the draft folder.
var draftFolder = await _folderService . GetSpecialFolderByAccountIdAsync ( accountId , SpecialFolderType . Draft ) ;
if ( draftFolder = = null ) return false ;
var draftCount = await Connection . Table < MailCopy > ( ) . Where ( a = > a . FolderId = = draftFolder . Id ) . CountAsync ( ) ;
return draftCount > 0 ;
}
2025-02-16 11:54:23 +01:00
public async Task < List < MailCopy > > GetUnreadMailsByFolderIdAsync ( Guid folderId )
{
var unreadMails = await Connection . QueryAsync < MailCopy > ( "SELECT * FROM MailCopy WHERE FolderId = ? AND IsRead = 0" , folderId ) ;
2024-07-09 01:05:16 +02:00
2025-02-16 11:54:23 +01:00
foreach ( var mail in unreadMails )
{
await LoadAssignedPropertiesAsync ( mail ) . ConfigureAwait ( false ) ;
2025-02-16 11:43:30 +01:00
}
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
return unreadMails ;
}
2026-02-23 01:51:44 +01:00
public async Task < MailCopy > GetMailCopyByMessageIdAsync ( Guid accountId , string messageId )
{
var normalizedMessageId = MailHeaderExtensions . StripAngleBrackets ( messageId ) ? . Trim ( ) ;
if ( string . IsNullOrWhiteSpace ( normalizedMessageId ) )
return null ;
var mailCopy = await Connection . FindWithQueryAsync < MailCopy > (
"SELECT MailCopy.* FROM MailCopy " +
"INNER JOIN MailItemFolder ON MailCopy.FolderId = MailItemFolder.Id " +
"WHERE MailItemFolder.MailAccountId = ? AND MailCopy.MessageId = ? " +
"ORDER BY MailCopy.IsDraft ASC, MailCopy.CreationDate DESC LIMIT 1" ,
accountId ,
normalizedMessageId ) . ConfigureAwait ( false ) ;
if ( mailCopy ! = null )
await LoadAssignedPropertiesAsync ( mailCopy ) . ConfigureAwait ( false ) ;
return mailCopy ;
}
2025-11-15 13:29:02 +01:00
private static ( string Query , object [ ] Parameters ) BuildMailFetchQuery ( MailListInitializationOptions options )
2025-02-16 11:54:23 +01:00
{
2025-11-15 13:29:02 +01:00
var sql = new StringBuilder ( ) ;
sql . Append ( "SELECT MailCopy.* FROM MailCopy INNER JOIN MailItemFolder ON MailCopy.FolderId = MailItemFolder.Id" ) ;
2025-12-15 21:06:13 +01:00
2025-11-15 13:29:02 +01:00
var whereClauses = new List < string > ( ) ;
var parameters = new List < object > ( ) ;
2025-12-15 21:06:13 +01:00
2025-11-15 13:29:02 +01:00
// Folder filter
var folderPlaceholders = string . Join ( "," , options . Folders . Select ( _ = > "?" ) ) ;
whereClauses . Add ( $"MailCopy.FolderId IN ({folderPlaceholders})" ) ;
parameters . AddRange ( options . Folders . Select ( f = > ( object ) f . Id ) ) ;
2025-12-15 21:06:13 +01:00
2025-11-15 13:29:02 +01:00
// Filter type
2025-02-16 11:54:23 +01:00
switch ( options . FilterType )
2024-04-18 01:44:37 +02:00
{
2025-02-16 11:54:23 +01:00
case FilterOptionType . Unread :
2025-11-15 13:29:02 +01:00
whereClauses . Add ( "MailCopy.IsRead = 0" ) ;
2025-02-16 11:54:23 +01:00
break ;
case FilterOptionType . Flagged :
2025-11-15 13:29:02 +01:00
whereClauses . Add ( "MailCopy.IsFlagged = 1" ) ;
2025-02-16 11:54:23 +01:00
break ;
case FilterOptionType . Files :
2025-11-15 13:29:02 +01:00
whereClauses . Add ( "MailCopy.HasAttachments = 1" ) ;
2025-02-16 11:54:23 +01:00
break ;
}
2025-12-15 21:06:13 +01:00
2025-11-15 13:29:02 +01:00
// Focused filter
2025-02-16 11:54:23 +01:00
if ( options . IsFocusedOnly ! = null )
2025-11-15 13:29:02 +01:00
{
whereClauses . Add ( $"MailCopy.IsFocused = {(options.IsFocusedOnly.Value ? " 1 " : " 0 ")}" ) ;
}
2025-12-15 21:06:13 +01:00
2025-11-15 13:29:02 +01:00
// Search query
2025-02-16 11:54:23 +01:00
if ( ! string . IsNullOrEmpty ( options . SearchQuery ) )
2025-11-15 13:29:02 +01:00
{
whereClauses . Add ( "(MailCopy.PreviewText LIKE ? OR MailCopy.Subject LIKE ? OR MailCopy.FromName LIKE ? OR MailCopy.FromAddress LIKE ?)" ) ;
var searchPattern = $"%{options.SearchQuery}%" ;
parameters . Add ( searchPattern ) ;
parameters . Add ( searchPattern ) ;
parameters . Add ( searchPattern ) ;
parameters . Add ( searchPattern ) ;
}
2025-12-15 21:06:13 +01:00
2025-11-15 13:29:02 +01:00
// Exclude existing items
2025-02-16 11:54:23 +01:00
if ( options . ExistingUniqueIds ? . Any ( ) ? ? false )
2025-02-16 11:43:30 +01:00
{
2025-11-15 13:29:02 +01:00
var excludePlaceholders = string . Join ( "," , options . ExistingUniqueIds . Select ( _ = > "?" ) ) ;
whereClauses . Add ( $"MailCopy.UniqueId NOT IN ({excludePlaceholders})" ) ;
2025-12-15 21:06:13 +01:00
parameters . AddRange ( options . ExistingUniqueIds . Keys . Select ( id = > ( object ) id ) ) ;
2025-02-16 11:54:23 +01:00
}
2025-12-15 21:06:13 +01:00
2025-11-15 13:29:02 +01:00
if ( whereClauses . Any ( ) )
2025-10-18 11:45:10 +02:00
{
2025-11-15 13:29:02 +01:00
sql . Append ( " WHERE " ) ;
sql . Append ( string . Join ( " AND " , whereClauses ) ) ;
2025-10-18 11:45:10 +02:00
}
2025-12-15 21:06:13 +01:00
2025-11-15 13:29:02 +01:00
// Sorting
if ( options . SortingOptionType = = SortingOptionType . ReceiveDate )
sql . Append ( " ORDER BY CreationDate DESC" ) ;
else if ( options . SortingOptionType = = SortingOptionType . Sender )
sql . Append ( " ORDER BY FromName ASC" ) ;
2025-12-15 21:06:13 +01:00
2025-11-15 13:29:02 +01:00
// Pagination
var limit = options . Take > 0 ? options . Take : ItemLoadCount ;
sql . Append ( $" LIMIT {limit}" ) ;
2025-12-15 21:06:13 +01:00
2025-11-15 13:29:02 +01:00
if ( options . Skip > 0 )
2025-10-18 11:45:10 +02:00
{
2025-11-15 13:29:02 +01:00
sql . Append ( $" OFFSET {options.Skip}" ) ;
2025-10-18 11:45:10 +02:00
}
2025-12-15 21:06:13 +01:00
2025-11-15 13:29:02 +01:00
return ( sql . ToString ( ) , parameters . ToArray ( ) ) ;
2025-02-16 11:54:23 +01:00
}
2024-04-18 01:44:37 +02:00
2026-02-12 18:57:55 +01:00
private static List < MailCopy > ApplyOptionsToPreFetchedMails ( MailListInitializationOptions options )
{
var allowedFolderIds = options . Folders . Select ( f = > f . Id ) . ToHashSet ( ) ;
2026-04-12 15:56:27 +02:00
var accountIdsByFolderId = options . Folders
. Where ( folder = > folder ! = null )
. GroupBy ( folder = > folder . Id )
. ToDictionary ( group = > group . Key , group = > group . First ( ) . MailAccountId ) ;
2026-02-12 18:57:55 +01:00
IEnumerable < MailCopy > query = options . PreFetchMailCopies
2026-04-12 15:56:27 +02:00
. Where ( m = > m ! = null & & allowedFolderIds . Contains ( m . FolderId ) ) ;
2026-02-12 18:57:55 +01:00
switch ( options . FilterType )
{
case FilterOptionType . Unread :
query = query . Where ( m = > ! m . IsRead ) ;
break ;
case FilterOptionType . Flagged :
query = query . Where ( m = > m . IsFlagged ) ;
break ;
case FilterOptionType . Files :
query = query . Where ( m = > m . HasAttachments ) ;
break ;
}
if ( options . IsFocusedOnly is bool isFocused )
{
query = query . Where ( m = > m . IsFocused = = isFocused ) ;
}
if ( ! string . IsNullOrWhiteSpace ( options . SearchQuery ) )
{
var search = options . SearchQuery . Trim ( ) ;
query = query . Where ( m = >
( ! string . IsNullOrEmpty ( m . PreviewText ) & & m . PreviewText . Contains ( search , StringComparison . OrdinalIgnoreCase ) )
| | ( ! string . IsNullOrEmpty ( m . Subject ) & & m . Subject . Contains ( search , StringComparison . OrdinalIgnoreCase ) )
| | ( ! string . IsNullOrEmpty ( m . FromName ) & & m . FromName . Contains ( search , StringComparison . OrdinalIgnoreCase ) )
| | ( ! string . IsNullOrEmpty ( m . FromAddress ) & & m . FromAddress . Contains ( search , StringComparison . OrdinalIgnoreCase ) ) ) ;
}
if ( options . ExistingUniqueIds ? . Any ( ) ? ? false )
{
query = query . Where ( m = > ! options . ExistingUniqueIds . ContainsKey ( m . UniqueId ) ) ;
}
2026-04-12 15:56:27 +02:00
query = options . DeduplicateByServerId
? query
. GroupBy ( m = > ( ResolveMailAccountId ( m , accountIdsByFolderId ) , ResolveServerMailId ( m ) ) )
. Select ( group = > group
. OrderByDescending ( m = > allowedFolderIds . Contains ( m . FolderId ) )
. ThenByDescending ( m = > m . CreationDate )
. ThenBy ( m = > m . FolderId )
. ThenBy ( m = > m . UniqueId )
. First ( ) )
: query
. GroupBy ( m = > m . UniqueId )
. Select ( group = > group . First ( ) ) ;
2026-02-12 18:57:55 +01:00
query = options . SortingOptionType switch
{
SortingOptionType . Sender = > query . OrderBy ( m = > m . FromName ) . ThenByDescending ( m = > m . CreationDate ) ,
_ = > query . OrderByDescending ( m = > m . CreationDate )
} ;
if ( options . Skip > 0 )
{
query = query . Skip ( options . Skip ) ;
}
if ( options . Take > 0 )
{
query = query . Take ( options . Take ) ;
}
return query . ToList ( ) ;
}
2026-04-12 15:56:27 +02:00
private static Guid ResolveMailAccountId ( MailCopy mail , IReadOnlyDictionary < Guid , Guid > accountIdsByFolderId )
{
if ( mail ? . AssignedAccount ! = null )
return mail . AssignedAccount . Id ;
if ( mail ! = null & & accountIdsByFolderId . TryGetValue ( mail . FolderId , out var accountId ) )
return accountId ;
return Guid . Empty ;
}
private static string ResolveServerMailId ( MailCopy mail )
= > string . IsNullOrWhiteSpace ( mail ? . Id ) ? mail ? . UniqueId . ToString ( "N" ) ? ? string . Empty : mail . Id ;
2025-10-03 15:46:38 +02:00
public async Task < List < MailCopy > > FetchMailsAsync ( MailListInitializationOptions options , CancellationToken cancellationToken = default )
2025-02-16 11:54:23 +01:00
{
2026-03-01 09:14:02 +01:00
List < MailCopy > mails ;
2024-04-18 01:44:37 +02:00
2025-02-22 00:22:00 +01:00
if ( options . PreFetchMailCopies ! = null )
{
2026-02-12 18:57:55 +01:00
mails = ApplyOptionsToPreFetchedMails ( options ) ;
2025-02-22 00:22:00 +01:00
}
else
{
2025-11-15 13:29:02 +01:00
var ( query , parameters ) = BuildMailFetchQuery ( options ) ;
mails = await Connection . QueryAsync < MailCopy > ( query , parameters ) ;
2025-02-22 00:22:00 +01:00
}
2024-04-18 01:44:37 +02:00
2026-03-01 09:14:02 +01:00
if ( mails . Count = = 0 )
return mails ;
2024-04-18 01:44:37 +02:00
2026-03-01 09:14:02 +01:00
cancellationToken . ThrowIfCancellationRequested ( ) ;
// Pre-load all data needed for property assignment in as few DB round-trips as possible.
// 1. Seed the folder cache directly from the options folders - these cover the vast majority
// of mails in a normal folder view and require zero extra DB calls.
var folderCache = options . Folders
. OfType < MailItemFolder > ( )
. ToDictionary ( f = > f . Id ) ;
// 2. Load all accounts in one call (typically 1-5 accounts) instead of N per-mail lookups.
var allAccounts = await _accountService . GetAccountsAsync ( ) . ConfigureAwait ( false ) ;
var accountCache = allAccounts . ToDictionary ( a = > a . Id ) ;
// 3. Fetch any folders not already in the cache (rare for normal views, common for merged inboxes
// that include Sent/Draft copies belonging to different folder objects).
var uncachedFolderIds = mails
. Select ( m = > m . FolderId )
. Distinct ( )
. Where ( id = > ! folderCache . ContainsKey ( id ) )
. ToList ( ) ;
if ( uncachedFolderIds . Count > 0 )
2025-02-16 11:54:23 +01:00
{
2026-03-01 09:14:02 +01:00
var folders = await Task . WhenAll (
uncachedFolderIds . Select ( id = > _folderService . GetFolderAsync ( id ) ) ) . ConfigureAwait ( false ) ;
foreach ( var f in folders . Where ( f = > f ! = null ) )
folderCache [ f . Id ] = f ;
2025-02-16 11:54:23 +01:00
}
2024-04-18 01:44:37 +02:00
2026-03-01 09:14:02 +01:00
// 4. Batch-fetch all sender contacts in a single SQL IN(...) query instead of one query per mail.
var uniqueAddresses = mails
. Where ( m = > ! string . IsNullOrEmpty ( m . FromAddress ) )
. Select ( m = > m . FromAddress )
. Distinct ( )
. ToList ( ) ;
var contactList = await _contactService . GetContactsByAddressesAsync ( uniqueAddresses ) . ConfigureAwait ( false ) ;
var contactCache = contactList . ToDictionary ( c = > c . Address ) ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
cancellationToken . ThrowIfCancellationRequested ( ) ;
2024-04-18 01:44:37 +02:00
2026-03-01 09:14:02 +01:00
// 5. Assign all properties synchronously from the pre-loaded in-memory caches - no DB calls here.
AssignPropertiesFromCaches ( mails , folderCache , accountCache , contactCache ) ;
mails . RemoveAll ( m = > m . AssignedAccount = = null | | m . AssignedFolder = = null ) ;
2026-04-11 21:02:51 +02:00
await _sentMailReceiptService . PopulateReceiptStatesAsync ( mails ) . ConfigureAwait ( false ) ;
2026-03-01 09:14:02 +01:00
if ( ! options . CreateThreads | | mails . Count = = 0 )
2025-10-26 23:35:09 +01:00
return [ . . mails ] ;
2026-03-01 09:14:02 +01:00
// 6. Expand threads: one batch query for all sibling mails across all threads.
2025-10-31 01:41:51 +01:00
var uniqueThreadIds = mails
. Where ( m = > ! string . IsNullOrEmpty ( m . ThreadId ) )
. Select ( m = > m . ThreadId )
. Distinct ( )
. ToList ( ) ;
2025-10-26 23:35:09 +01:00
2026-03-01 09:14:02 +01:00
if ( uniqueThreadIds . Count = = 0 )
return [ . . mails ] ;
var existingMailIds = mails . Select ( m = > m . Id ) . ToHashSet ( ) ;
var threadMails = await GetMailsByThreadIdsAsync ( uniqueThreadIds , existingMailIds ) . ConfigureAwait ( false ) ;
if ( threadMails ? . Count > 0 )
2025-10-26 23:35:09 +01:00
{
2026-03-01 09:14:02 +01:00
cancellationToken . ThrowIfCancellationRequested ( ) ;
2025-10-26 23:35:09 +01:00
2026-03-01 09:14:02 +01:00
// Load any folders that thread mails belong to but are not yet cached.
var newFolderIds = threadMails
. Select ( m = > m . FolderId )
. Distinct ( )
. Where ( id = > ! folderCache . ContainsKey ( id ) )
. ToList ( ) ;
if ( newFolderIds . Count > 0 )
2025-10-31 01:41:51 +01:00
{
2026-03-01 09:14:02 +01:00
var newFolders = await Task . WhenAll (
newFolderIds . Select ( id = > _folderService . GetFolderAsync ( id ) ) ) . ConfigureAwait ( false ) ;
2025-10-31 01:41:51 +01:00
2026-03-01 09:14:02 +01:00
foreach ( var f in newFolders . Where ( f = > f ! = null ) )
folderCache [ f . Id ] = f ;
}
2025-12-15 21:06:13 +01:00
2026-03-01 09:14:02 +01:00
// Batch-fetch contacts for any new senders in thread mails.
var newAddresses = threadMails
. Where ( m = > ! string . IsNullOrEmpty ( m . FromAddress ) & & ! contactCache . ContainsKey ( m . FromAddress ) )
. Select ( m = > m . FromAddress )
. Distinct ( )
. ToList ( ) ;
2025-12-15 21:06:13 +01:00
2026-03-01 09:14:02 +01:00
if ( newAddresses . Count > 0 )
{
var newContacts = await _contactService . GetContactsByAddressesAsync ( newAddresses ) . ConfigureAwait ( false ) ;
foreach ( var c in newContacts . Where ( c = > c ! = null ) )
contactCache [ c . Address ] = c ;
2025-10-26 23:35:09 +01:00
}
2026-03-01 09:14:02 +01:00
AssignPropertiesFromCaches ( threadMails , folderCache , accountCache , contactCache ) ;
mails . AddRange ( threadMails . Where ( m = > m . AssignedAccount ! = null & & m . AssignedFolder ! = null ) ) ;
2025-10-26 23:35:09 +01:00
}
2026-04-11 21:02:51 +02:00
await _sentMailReceiptService . PopulateReceiptStatesAsync ( mails ) . ConfigureAwait ( false ) ;
2026-03-01 09:14:02 +01:00
cancellationToken . ThrowIfCancellationRequested ( ) ;
return [ . . mails ] ;
2025-10-26 23:35:09 +01:00
}
2026-03-01 09:14:02 +01:00
/// <summary>
/// Assigns AssignedFolder, AssignedAccount, and SenderContact to each mail from pre-loaded
/// in-memory dictionaries. No DB calls are made here.
/// </summary>
private void AssignPropertiesFromCaches (
List < MailCopy > mails ,
Dictionary < Guid , MailItemFolder > folderCache ,
Dictionary < Guid , MailAccount > accountCache ,
Dictionary < string , AccountContact > contactCache )
2025-10-26 23:35:09 +01:00
{
2026-03-01 09:14:02 +01:00
foreach ( var mail in mails )
{
if ( ! folderCache . TryGetValue ( mail . FolderId , out var folder ) )
continue ;
2025-10-26 23:35:09 +01:00
2026-03-01 09:14:02 +01:00
if ( ! accountCache . TryGetValue ( folder . MailAccountId , out var account ) )
continue ;
2025-10-26 23:35:09 +01:00
2026-03-01 09:14:02 +01:00
mail . AssignedFolder = folder ;
mail . AssignedAccount = account ;
// Self-sent mails (e.g. Sent folder): construct contact from account meta
// to get the up-to-date profile picture without a DB roundtrip.
2026-03-07 11:43:56 +01:00
if ( ! string . IsNullOrEmpty ( mail . FromAddress ) & &
string . Equals ( mail . FromAddress , account . Address , StringComparison . OrdinalIgnoreCase ) )
2026-03-01 09:14:02 +01:00
{
2026-03-07 11:43:56 +01:00
if ( contactCache . TryGetValue ( mail . FromAddress , out var ownContact ) )
2026-03-01 09:14:02 +01:00
{
2026-03-07 11:43:56 +01:00
mail . SenderContact = ownContact ;
}
else
{
mail . SenderContact = new AccountContact
{
Address = account . Address ,
Name = account . SenderName
} ;
}
2026-03-01 09:14:02 +01:00
}
else
{
contactCache . TryGetValue ( mail . FromAddress ? ? string . Empty , out var contact ) ;
mail . SenderContact = contact ? ? CreateUnknownContact ( mail . FromName , mail . FromAddress ) ;
}
}
2025-02-23 22:17:40 +01:00
}
2024-04-18 01:44:37 +02:00
2025-10-31 01:41:51 +01:00
private async Task < List < MailCopy > > GetMailsByThreadIdsAsync ( List < string > threadIds , HashSet < string > excludeMailIds )
{
if ( threadIds ? . Count = = 0 )
return [ ] ;
2025-11-15 13:29:02 +01:00
var threadPlaceholders = string . Join ( "," , threadIds . Select ( _ = > "?" ) ) ;
var parameters = new List < object > ( ) ;
parameters . AddRange ( threadIds . Cast < object > ( ) ) ;
2025-10-31 01:41:51 +01:00
2026-03-01 09:14:02 +01:00
string sql ;
if ( excludeMailIds . Count > 0 )
2025-02-23 22:17:40 +01:00
{
2026-03-01 09:14:02 +01:00
var excludePlaceholders = string . Join ( "," , excludeMailIds . Select ( _ = > "?" ) ) ;
sql = $"SELECT MailCopy.* FROM MailCopy WHERE ThreadId IN ({threadPlaceholders}) AND Id NOT IN ({excludePlaceholders})" ;
parameters . AddRange ( excludeMailIds . Cast < object > ( ) ) ;
2024-04-18 01:44:37 +02:00
}
2026-03-01 09:14:02 +01:00
else
{
sql = $"SELECT MailCopy.* FROM MailCopy WHERE ThreadId IN ({threadPlaceholders})" ;
}
return await Connection . QueryAsync < MailCopy > ( sql , parameters . ToArray ( ) ) . ConfigureAwait ( false ) ;
2025-02-16 11:54:23 +01:00
}
2024-04-18 01:44:37 +02:00
2025-02-23 22:17:40 +01:00
private static AccountContact CreateUnknownContact ( string fromName , string fromAddress )
2025-02-16 11:54:23 +01:00
{
if ( string . IsNullOrEmpty ( fromName ) & & string . IsNullOrEmpty ( fromAddress ) )
2024-08-30 01:03:35 +02:00
{
2025-02-16 11:54:23 +01:00
return new AccountContact ( )
2024-08-30 01:03:35 +02:00
{
2025-02-16 11:54:23 +01:00
Name = Translator . UnknownSender ,
Address = Translator . UnknownAddress
} ;
2025-02-16 11:43:30 +01:00
}
2025-02-16 11:54:23 +01:00
else
2024-08-23 01:07:00 +02:00
{
2025-02-16 11:54:23 +01:00
if ( string . IsNullOrEmpty ( fromName ) ) fromName = fromAddress ;
2024-08-23 01:07:00 +02:00
2025-02-16 11:54:23 +01:00
return new AccountContact ( )
2025-02-16 11:43:30 +01:00
{
2025-02-16 11:54:23 +01:00
Name = fromName ,
Address = fromAddress
} ;
2025-02-16 11:35:43 +01:00
}
2025-02-16 11:54:23 +01:00
}
2025-02-16 11:43:30 +01:00
2025-02-16 11:54:23 +01:00
private async Task < List < MailCopy > > GetMailItemsAsync ( string mailCopyId )
{
var mailCopies = await Connection . Table < MailCopy > ( ) . Where ( a = > a . Id = = mailCopyId ) . ToListAsync ( ) ;
foreach ( var mailCopy in mailCopies )
2024-04-18 01:44:37 +02:00
{
2025-02-16 11:54:23 +01:00
await LoadAssignedPropertiesAsync ( mailCopy ) . ConfigureAwait ( false ) ;
2025-02-16 11:35:43 +01:00
}
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
return mailCopies ;
}
private Task < AccountContact > GetSenderContactForAccountAsync ( MailAccount account , string fromAddress )
{
// Make sure to return the latest up to date contact information for the original account.
2026-03-07 11:43:56 +01:00
if ( string . Equals ( fromAddress , account . Address , StringComparison . OrdinalIgnoreCase ) )
2025-02-16 11:54:23 +01:00
{
2026-03-07 11:43:56 +01:00
return GetOwnSenderContactAsync ( account ) ;
2025-02-16 11:54:23 +01:00
}
else
2025-02-16 11:43:30 +01:00
{
2025-02-16 11:54:23 +01:00
return _contactService . GetAddressInformationByAddressAsync ( fromAddress ) ;
}
}
2024-04-18 01:44:37 +02:00
2026-03-07 11:43:56 +01:00
private async Task < AccountContact > GetOwnSenderContactAsync ( MailAccount account )
{
var contact = await _contactService . GetAddressInformationByAddressAsync ( account . Address ) . ConfigureAwait ( false ) ;
return contact ? ? new AccountContact
{
Address = account . Address ,
Name = account . SenderName
} ;
}
2025-02-16 11:54:23 +01:00
private async Task LoadAssignedPropertiesAsync ( MailCopy mailCopy )
{
if ( mailCopy = = null ) return ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
// Load AssignedAccount, AssignedFolder and SenderContact.
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
var folder = await _folderService . GetFolderAsync ( mailCopy . FolderId ) ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
if ( folder = = null ) return ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
var account = await _accountService . GetAccountAsync ( folder . MailAccountId ) ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
if ( account = = null ) return ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
mailCopy . AssignedAccount = account ;
mailCopy . AssignedFolder = folder ;
mailCopy . SenderContact = await GetSenderContactForAccountAsync ( account , mailCopy . FromAddress ) . ConfigureAwait ( false ) ;
2026-04-11 21:02:51 +02:00
await _sentMailReceiptService . PopulateReceiptStateAsync ( mailCopy ) . ConfigureAwait ( false ) ;
2025-02-16 11:54:23 +01:00
}
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
public async Task < MailCopy > GetSingleMailItemWithoutFolderAssignmentAsync ( string mailCopyId )
{
var mailCopy = await Connection . Table < MailCopy > ( ) . FirstOrDefaultAsync ( a = > a . Id = = mailCopyId ) ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
if ( mailCopy = = null ) return null ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
await LoadAssignedPropertiesAsync ( mailCopy ) . ConfigureAwait ( false ) ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
return mailCopy ;
}
2024-04-18 01:44:37 +02:00
2025-02-22 00:22:00 +01:00
/// <summary>
/// Using this override is dangerous.
/// Gmail stores multiple copies of same mail in different folders.
/// This one will always return the first one. Use with caution.
/// </summary>
/// <param name="mailCopyId">Mail copy id.</param>
public async Task < MailCopy > GetSingleMailItemAsync ( string mailCopyId )
{
2025-11-15 13:29:02 +01:00
var mailCopy = await Connection . FindWithQueryAsync < MailCopy > (
"SELECT MailCopy.* FROM MailCopy WHERE MailCopy.Id = ?" ,
mailCopyId ) ;
2025-02-22 00:22:00 +01:00
if ( mailCopy = = null ) return null ;
await LoadAssignedPropertiesAsync ( mailCopy ) . ConfigureAwait ( false ) ;
return mailCopy ;
}
2025-02-16 11:54:23 +01:00
public async Task < MailCopy > GetSingleMailItemAsync ( string mailCopyId , string remoteFolderId )
{
2025-11-15 13:29:02 +01:00
var mailItem = await Connection . FindWithQueryAsync < MailCopy > (
"SELECT MailCopy.* FROM MailCopy INNER JOIN MailItemFolder ON MailCopy.FolderId = MailItemFolder.Id WHERE MailCopy.Id = ? AND MailItemFolder.RemoteFolderId = ?" ,
mailCopyId , remoteFolderId ) ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
if ( mailItem = = null ) return null ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
await LoadAssignedPropertiesAsync ( mailItem ) . ConfigureAwait ( false ) ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
return mailItem ;
}
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
public async Task < MailCopy > GetSingleMailItemAsync ( Guid uniqueMailId )
{
var mailItem = await Connection . FindAsync < MailCopy > ( uniqueMailId ) ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
if ( mailItem = = null ) return null ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
await LoadAssignedPropertiesAsync ( mailItem ) . ConfigureAwait ( false ) ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
return mailItem ;
}
2024-08-29 23:58:39 +02:00
2025-02-16 11:54:23 +01:00
// v2
2025-02-16 11:35:43 +01:00
2025-02-16 11:54:23 +01:00
public async Task DeleteMailAsync ( Guid accountId , string mailCopyId )
{
var allMails = await GetMailItemsAsync ( mailCopyId ) . ConfigureAwait ( false ) ;
2025-02-16 11:43:30 +01:00
2025-02-16 11:54:23 +01:00
foreach ( var mailItem in allMails )
{
// Delete mime file as well.
// Even though Gmail might have multiple copies for the same mail, we only have one MIME file for all.
// Their FileId is inserted same.
await DeleteMailInternalAsync ( mailItem , preserveMimeFile : false ) . ConfigureAwait ( false ) ;
2024-04-18 01:44:37 +02:00
}
2025-02-16 11:54:23 +01:00
}
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
#region Repository Calls
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
private async Task InsertMailAsync ( MailCopy mailCopy )
{
if ( mailCopy = = null )
2024-04-18 01:44:37 +02:00
{
2025-02-16 11:54:23 +01:00
_logger . Warning ( "Null mail passed to InsertMailAsync call." ) ;
return ;
}
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
if ( mailCopy . FolderId = = Guid . Empty )
{
_logger . Warning ( "Invalid FolderId for MailCopyId {Id} for InsertMailAsync" , mailCopy . Id ) ;
return ;
}
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
_logger . Debug ( "Inserting mail {MailCopyId} to {FolderName}" , mailCopy . Id , mailCopy . AssignedFolder . FolderName ) ;
2024-04-18 01:44:37 +02:00
2025-11-14 14:28:10 +01:00
await Connection . InsertAsync ( mailCopy , typeof ( MailCopy ) ) . ConfigureAwait ( false ) ;
2024-04-18 01:44:37 +02:00
2026-04-07 16:48:46 +02:00
ReportUIChange ( new MailAddedMessage ( mailCopy , EntityUpdateSource . Server ) ) ;
2025-02-16 11:54:23 +01:00
}
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
public async Task UpdateMailAsync ( MailCopy mailCopy )
{
if ( mailCopy = = null )
2024-04-18 01:44:37 +02:00
{
2025-02-16 11:54:23 +01:00
_logger . Warning ( "Null mail passed to UpdateMailAsync call." ) ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
return ;
}
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
_logger . Debug ( "Updating mail {MailCopyId} with Folder {FolderId}" , mailCopy . Id , mailCopy . FolderId ) ;
2024-04-18 01:44:37 +02:00
2025-11-14 14:28:10 +01:00
await Connection . UpdateAsync ( mailCopy , typeof ( MailCopy ) ) . ConfigureAwait ( false ) ;
2024-04-18 01:44:37 +02:00
2026-04-07 16:48:46 +02:00
ReportUIChange ( new MailUpdatedMessage ( mailCopy , EntityUpdateSource . Server ) ) ;
2025-02-16 11:54:23 +01:00
}
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
private async Task DeleteMailInternalAsync ( MailCopy mailCopy , bool preserveMimeFile )
{
if ( mailCopy = = null )
2024-04-18 01:44:37 +02:00
{
2025-02-16 11:54:23 +01:00
_logger . Warning ( "Null mail passed to DeleteMailAsync call." ) ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
return ;
}
2024-06-11 22:48:18 +02:00
2025-02-16 11:54:23 +01:00
_logger . Debug ( "Deleting mail {Id} from folder {FolderName}" , mailCopy . Id , mailCopy . AssignedFolder . FolderName ) ;
2024-06-11 22:48:18 +02:00
2025-12-25 17:21:23 +01:00
await Connection . DeleteAsync < MailCopy > ( mailCopy . UniqueId ) . ConfigureAwait ( false ) ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
// If there are no more copies exists of the same mail, delete the MIME file as well.
var isMailExists = await IsMailExistsAsync ( mailCopy . Id ) . ConfigureAwait ( false ) ;
2025-02-16 11:35:43 +01:00
2025-02-16 11:54:23 +01:00
if ( ! isMailExists & & ! preserveMimeFile )
{
await _mimeFileService . DeleteMimeMessageAsync ( mailCopy . AssignedAccount . Id , mailCopy . FileId ) . ConfigureAwait ( false ) ;
2025-02-16 11:43:30 +01:00
}
2025-02-16 11:35:43 +01:00
2026-04-07 16:48:46 +02:00
ReportUIChange ( new MailRemovedMessage ( mailCopy , EntityUpdateSource . Server ) ) ;
2025-02-16 11:54:23 +01:00
}
#endregion
private async Task UpdateAllMailCopiesAsync ( string mailCopyId , Func < MailCopy , bool > action )
{
var mailCopies = await GetMailItemsAsync ( mailCopyId ) ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
if ( mailCopies = = null | | ! mailCopies . Any ( ) )
2024-04-18 01:44:37 +02:00
{
2025-02-16 11:54:23 +01:00
_logger . Warning ( "Updating mail copies failed because there are no copies available with Id {MailCopyId}" , mailCopyId ) ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
return ;
}
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
_logger . Debug ( "Updating {MailCopyCount} mail copies with Id {MailCopyId}" , mailCopies . Count , mailCopyId ) ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
foreach ( var mailCopy in mailCopies )
{
bool shouldUpdateItem = action ( mailCopy ) ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
if ( shouldUpdateItem )
2024-04-18 01:44:37 +02:00
{
2025-02-16 11:54:23 +01:00
await UpdateMailAsync ( mailCopy ) . ConfigureAwait ( false ) ;
2024-04-18 01:44:37 +02:00
}
2025-02-16 11:54:23 +01:00
else
_logger . Debug ( "Skipped updating mail because it is already in the desired state." ) ;
2024-04-18 01:44:37 +02:00
}
2025-02-16 11:54:23 +01:00
}
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
public Task ChangeReadStatusAsync ( string mailCopyId , bool isRead )
= > UpdateAllMailCopiesAsync ( mailCopyId , ( item ) = >
{
if ( item . IsRead = = isRead ) return false ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
item . IsRead = isRead ;
2025-07-26 12:51:53 +02:00
if ( isRead & & item . UniqueId ! = Guid . Empty )
{
WeakReferenceMessenger . Default . Send ( new MailReadStatusChanged ( item . UniqueId ) ) ;
}
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
return true ;
} ) ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
public Task ChangeFlagStatusAsync ( string mailCopyId , bool isFlagged )
= > UpdateAllMailCopiesAsync ( mailCopyId , ( item ) = >
2025-02-16 11:35:43 +01:00
{
2025-02-16 11:54:23 +01:00
if ( item . IsFlagged = = isFlagged ) return false ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
item . IsFlagged = isFlagged ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
return true ;
} ) ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
public async Task CreateAssignmentAsync ( Guid accountId , string mailCopyId , string remoteFolderId )
{
// Note: Folder might not be available at the moment due to user not syncing folders before the delta processing.
// This is a problem, because assignments won't be created.
// Therefore we sync folders every time before the delta processing.
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
var localFolder = await _folderService . GetFolderAsync ( accountId , remoteFolderId ) ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
if ( localFolder = = null )
{
_logger . Warning ( "Local folder not found for remote folder {RemoteFolderId}" , remoteFolderId ) ;
_logger . Warning ( "Skipping assignment creation for the the message {MailCopyId}" , mailCopyId ) ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
return ;
}
2024-04-18 01:44:37 +02:00
2026-04-12 15:56:27 +02:00
if ( await IsMailExistsAsync ( mailCopyId , localFolder . Id ) . ConfigureAwait ( false ) )
{
_logger . Debug ( "Skipping assignment creation for {MailCopyId} because folder {FolderId} already has a local copy." ,
mailCopyId , localFolder . Id ) ;
return ;
}
2025-02-16 11:54:23 +01:00
var mailCopy = await GetSingleMailItemWithoutFolderAssignmentAsync ( mailCopyId ) ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
if ( mailCopy = = null )
{
_logger . Warning ( "Can't create assignment for mail {MailCopyId} because it does not exist." , mailCopyId ) ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
return ;
2024-04-18 01:44:37 +02:00
}
2025-02-16 11:54:23 +01:00
if ( mailCopy . AssignedFolder . SpecialFolderType = = SpecialFolderType . Sent & &
localFolder . SpecialFolderType = = SpecialFolderType . Deleted )
2024-04-18 01:44:37 +02:00
{
2025-02-16 11:54:23 +01:00
// Sent item is deleted.
// Gmail does not delete the sent items, but moves them to the deleted folder.
// API doesn't allow removing Sent label.
// Here we intercept this behavior, removing the Sent copy of the mail and adding the Deleted copy.
// This way item will only be visible in Trash folder as in Gmail Web UI.
// Don't delete MIME file since if exists.
await DeleteMailInternalAsync ( mailCopy , preserveMimeFile : true ) . ConfigureAwait ( false ) ;
}
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
// Copy one of the mail copy and assign it to the new folder.
// We don't need to create a new MIME pack.
// Therefore FileId is not changed for the new MailCopy.
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
mailCopy . UniqueId = Guid . NewGuid ( ) ;
mailCopy . FolderId = localFolder . Id ;
mailCopy . AssignedFolder = localFolder ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
await InsertMailAsync ( mailCopy ) . ConfigureAwait ( false ) ;
}
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
public async Task DeleteAssignmentAsync ( Guid accountId , string mailCopyId , string remoteFolderId )
{
var mailItem = await GetSingleMailItemAsync ( mailCopyId , remoteFolderId ) . ConfigureAwait ( false ) ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
if ( mailItem = = null )
{
_logger . Warning ( "Mail not found with id {MailCopyId} with remote folder {RemoteFolderId}" , mailCopyId , remoteFolderId ) ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
return ;
2025-02-16 11:35:43 +01:00
}
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
var localFolder = await _folderService . GetFolderAsync ( accountId , remoteFolderId ) ;
if ( localFolder = = null )
2025-02-16 11:35:43 +01:00
{
2025-02-16 11:54:23 +01:00
_logger . Warning ( "Local folder not found for remote folder {RemoteFolderId}" , remoteFolderId ) ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
return ;
}
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
await DeleteMailInternalAsync ( mailItem , preserveMimeFile : false ) . ConfigureAwait ( false ) ;
}
2025-02-16 11:35:43 +01:00
2025-02-22 00:22:00 +01:00
public async Task CreateMailRawAsync ( MailAccount account , MailItemFolder mailItemFolder , NewMailItemPackage package )
{
var mailCopy = package . Copy ;
var mimeMessage = package . Mime ;
mailCopy . UniqueId = Guid . NewGuid ( ) ;
mailCopy . AssignedAccount = account ;
mailCopy . AssignedFolder = mailItemFolder ;
mailCopy . SenderContact = await GetSenderContactForAccountAsync ( account , mailCopy . FromAddress ) . ConfigureAwait ( false ) ;
mailCopy . FolderId = mailItemFolder . Id ;
2026-02-09 22:39:30 +01:00
await SaveContactsForPackageAsync ( package ) . ConfigureAwait ( false ) ;
2025-02-22 00:22:00 +01:00
var mimeSaveTask = _mimeFileService . SaveMimeMessageAsync ( mailCopy . FileId , mimeMessage , account . Id ) ;
var insertMailTask = InsertMailAsync ( mailCopy ) ;
2026-02-09 22:39:30 +01:00
await Task . WhenAll ( mimeSaveTask , insertMailTask ) . ConfigureAwait ( false ) ;
2026-04-11 21:02:51 +02:00
await _sentMailReceiptService . TrackSentMailAsync ( mailCopy , mimeMessage ) . ConfigureAwait ( false ) ;
await _sentMailReceiptService . ProcessIncomingReceiptAsync ( mailCopy , mimeMessage ) . ConfigureAwait ( false ) ;
2025-02-22 00:22:00 +01:00
}
2025-10-30 17:15:05 +01:00
public async Task CreateMailAsyncEx ( Guid accountId , NewMailItemPackage package )
{
}
2025-02-16 11:54:23 +01:00
public async Task < bool > CreateMailAsync ( Guid accountId , NewMailItemPackage package )
{
var account = await _accountService . GetAccountAsync ( accountId ) . ConfigureAwait ( false ) ;
2025-02-16 11:35:43 +01:00
2025-02-16 11:54:23 +01:00
if ( account = = null ) return false ;
2025-02-16 11:35:43 +01:00
2025-02-16 11:54:23 +01:00
if ( string . IsNullOrEmpty ( package . AssignedRemoteFolderId ) )
{
_logger . Warning ( "Remote folder id is not set for {MailCopyId}." , package . Copy . Id ) ;
_logger . Warning ( "Ignoring creation of mail." ) ;
2025-02-16 11:43:30 +01:00
2025-02-16 11:54:23 +01:00
return false ;
}
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
var assignedFolder = await _folderService . GetFolderAsync ( accountId , package . AssignedRemoteFolderId ) . ConfigureAwait ( false ) ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
if ( assignedFolder = = null )
{
_logger . Warning ( "Assigned folder not found for {MailCopyId}." , package . Copy . Id ) ;
_logger . Warning ( "Ignoring creation of mail." ) ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
return false ;
}
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
var mailCopy = package . Copy ;
var mimeMessage = package . Mime ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
mailCopy . UniqueId = Guid . NewGuid ( ) ;
mailCopy . AssignedAccount = account ;
mailCopy . AssignedFolder = assignedFolder ;
mailCopy . SenderContact = await GetSenderContactForAccountAsync ( account , mailCopy . FromAddress ) . ConfigureAwait ( false ) ;
mailCopy . FolderId = assignedFolder . Id ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
// Only save MIME files if they don't exists.
// This is because 1 mail may have multiple copies in different folders.
// but only single MIME to represent all.
2025-10-12 16:23:33 +02:00
// Save mime file to disk if provided.
2025-02-16 11:54:23 +01:00
2025-10-12 16:23:33 +02:00
if ( mimeMessage ! = null )
2025-02-16 11:54:23 +01:00
{
2025-10-12 16:23:33 +02:00
var isMimeExists = await _mimeFileService . IsMimeExistAsync ( accountId , mailCopy . FileId ) . ConfigureAwait ( false ) ;
2025-02-16 11:54:23 +01:00
2025-10-12 16:23:33 +02:00
if ( ! isMimeExists )
2025-02-16 11:54:23 +01:00
{
2025-10-12 16:23:33 +02:00
bool isMimeSaved = await _mimeFileService . SaveMimeMessageAsync ( mailCopy . FileId , mimeMessage , accountId ) . ConfigureAwait ( false ) ;
if ( ! isMimeSaved )
{
_logger . Warning ( "Failed to save mime file for {MailCopyId}." , mailCopy . Id ) ;
}
2025-02-16 11:43:30 +01:00
}
2024-04-18 01:44:37 +02:00
2025-10-12 16:23:33 +02:00
}
2025-02-16 11:35:43 +01:00
2026-02-09 22:39:30 +01:00
// Save contact information extracted from provider API or MIME before insert/update.
await SaveContactsForPackageAsync ( package ) . ConfigureAwait ( false ) ;
2025-02-16 11:54:23 +01:00
// Create mail copy in the database.
// Update if exists.
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
var existingCopyItem = await Connection . Table < MailCopy > ( )
. FirstOrDefaultAsync ( a = > a . Id = = mailCopy . Id & & a . FolderId = = assignedFolder . Id ) ;
2024-08-17 22:55:58 +02:00
2025-02-16 11:54:23 +01:00
if ( existingCopyItem ! = null )
{
mailCopy . UniqueId = existingCopyItem . UniqueId ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
await UpdateMailAsync ( mailCopy ) . ConfigureAwait ( false ) ;
2026-04-11 21:02:51 +02:00
await _sentMailReceiptService . TrackSentMailAsync ( mailCopy , mimeMessage ) . ConfigureAwait ( false ) ;
await _sentMailReceiptService . ProcessIncomingReceiptAsync ( mailCopy , mimeMessage ) . ConfigureAwait ( false ) ;
2024-08-17 22:55:58 +02:00
2025-02-16 11:54:23 +01:00
return false ;
}
else
{
2025-04-04 23:55:50 +02:00
if ( account . ProviderType ! = MailProviderType . Gmail )
{
// Make sure there is only 1 instance left of this mail copy id.
var allMails = await GetMailItemsAsync ( mailCopy . Id ) . ConfigureAwait ( false ) ;
await DeleteMailAsync ( accountId , mailCopy . Id ) . ConfigureAwait ( false ) ;
}
2025-02-16 11:54:23 +01:00
await InsertMailAsync ( mailCopy ) . ConfigureAwait ( false ) ;
2026-04-11 21:02:51 +02:00
await _sentMailReceiptService . TrackSentMailAsync ( mailCopy , mimeMessage ) . ConfigureAwait ( false ) ;
await _sentMailReceiptService . ProcessIncomingReceiptAsync ( mailCopy , mimeMessage ) . ConfigureAwait ( false ) ;
2024-08-17 22:55:58 +02:00
2025-02-16 11:54:23 +01:00
return true ;
2025-02-16 11:43:30 +01:00
}
2025-02-16 11:54:23 +01:00
}
2026-02-09 22:39:30 +01:00
private async Task SaveContactsForPackageAsync ( NewMailItemPackage package )
{
if ( package = = null ) return ;
if ( package . Mime ! = null )
{
await _contactService . SaveAddressInformationAsync ( package . Mime ) . ConfigureAwait ( false ) ;
return ;
}
var contacts = package . ExtractedContacts ?
. Where ( c = > c ! = null & & ! string . IsNullOrWhiteSpace ( c . Address ) )
. ToList ( ) ? ? new List < AccountContact > ( ) ;
var senderAddress = package . Copy ? . FromAddress ;
if ( ! string . IsNullOrWhiteSpace ( senderAddress ) )
{
contacts . Add ( new AccountContact
{
Address = senderAddress ,
Name = string . IsNullOrWhiteSpace ( package . Copy ? . FromName ) ? senderAddress : package . Copy . FromName
} ) ;
}
if ( contacts . Count = = 0 ) return ;
await _contactService . SaveAddressInformationAsync ( contacts ) . ConfigureAwait ( false ) ;
}
2026-04-13 01:09:40 +02:00
private async Task < MimeMessage > CreateDraftMimeAsync ( MailAccount account , DraftCreationOptions draftCreationOptions , MailAccountAlias selectedAlias )
2025-02-16 11:54:23 +01:00
{
// This unique id is stored in mime headers for Wino to identify remote message with local copy.
// Same unique id will be used for the local copy as well.
// Synchronizer will map this unique id to the local draft copy after synchronization.
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
var message = new MimeMessage ( )
2025-02-16 11:35:43 +01:00
{
2025-02-16 11:54:23 +01:00
Headers = { { Constants . WinoLocalDraftHeader , Guid . NewGuid ( ) . ToString ( ) } } ,
} ;
2026-04-07 01:17:52 +02:00
EnsureOutgoingMessageId ( message ) ;
2024-08-10 14:33:02 +02:00
2026-04-13 01:09:40 +02:00
selectedAlias ? ? = await _accountService . GetPrimaryAccountAliasAsync ( account . Id ) ? ? throw new MissingAliasException ( ) ;
2024-08-10 14:33:02 +02:00
2025-02-16 11:54:23 +01:00
// Set FromName and FromAddress by alias.
2026-04-13 01:09:40 +02:00
message . From . Add ( new MailboxAddress ( selectedAlias . AliasSenderName ? ? account . SenderName , selectedAlias . AliasAddress ) ) ;
if ( ! string . IsNullOrWhiteSpace ( selectedAlias . ReplyToAddress ) )
{
message . ReplyTo . Add ( new MailboxAddress ( selectedAlias . ReplyToAddress , selectedAlias . ReplyToAddress ) ) ;
}
2024-08-10 14:33:02 +02:00
2025-02-16 11:54:23 +01:00
var builder = new BodyBuilder ( ) ;
2024-08-10 14:33:02 +02:00
2025-02-16 11:54:23 +01:00
var signature = await GetSignature ( account , draftCreationOptions . Reason ) ;
2026-02-23 01:51:44 +01:00
var ownAddresses = await GetOwnAddressesAsync ( account ) . ConfigureAwait ( false ) ;
2024-08-10 14:33:02 +02:00
2025-02-16 11:54:23 +01:00
_ = draftCreationOptions . Reason switch
{
DraftCreationReason . Empty = > CreateEmptyDraft ( builder , message , draftCreationOptions , signature ) ,
2026-02-23 01:51:44 +01:00
_ = > CreateReferencedDraft ( builder , message , draftCreationOptions , signature , ownAddresses ) ,
2025-02-16 11:54:23 +01:00
} ;
2024-08-10 14:33:02 +02:00
2025-02-16 11:54:23 +01:00
// TODO: Migration
// builder.SetHtmlBody(builder.HtmlBody);
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
message . Body = builder . ToMessageBody ( ) ;
2024-08-10 14:33:02 +02:00
2025-02-16 11:54:23 +01:00
return message ;
}
2025-02-16 11:35:43 +01:00
2026-04-13 01:09:40 +02:00
private async Task < MailAccountAlias > ResolveDraftAliasAsync ( MailAccount account , DraftCreationOptions draftCreationOptions )
{
var aliases = await _accountService . GetAccountAliasesAsync ( account . Id ) . ConfigureAwait ( false ) ;
var primaryAlias = aliases . FirstOrDefault ( a = > a . IsPrimary ) ? ? aliases . FirstOrDefault ( ) ;
if ( draftCreationOptions ? . ReferencedMessage ? . MimeMessage = = null )
return primaryAlias ;
var referencedMessage = draftCreationOptions . ReferencedMessage . MimeMessage ;
MailAccountAlias FindAlias ( string address )
{
if ( string . IsNullOrWhiteSpace ( address ) )
return null ;
return aliases . FirstOrDefault ( a = > a . AliasAddress . Equals ( address . Trim ( ) , StringComparison . OrdinalIgnoreCase ) ) ;
}
var deliveredToAlias = FindAlias ( ExtractAddressFromHeader ( referencedMessage . Headers [ "Delivered-To" ] ) )
? ? FindAlias ( ExtractAddressFromHeader ( referencedMessage . Headers [ "X-Original-To" ] ) ) ;
if ( deliveredToAlias ! = null )
return deliveredToAlias ;
foreach ( var mailbox in referencedMessage . To . Mailboxes )
{
var matchedAlias = FindAlias ( mailbox . Address ) ;
if ( matchedAlias ! = null )
return matchedAlias ;
}
foreach ( var mailbox in referencedMessage . Cc . Mailboxes )
{
var matchedAlias = FindAlias ( mailbox . Address ) ;
if ( matchedAlias ! = null )
return matchedAlias ;
}
return primaryAlias ;
}
private static string ExtractAddressFromHeader ( string headerValue )
{
if ( string . IsNullOrWhiteSpace ( headerValue ) )
return string . Empty ;
var trimmed = headerValue . Trim ( ) ;
var leftBracketIndex = trimmed . LastIndexOf ( '<' ) ;
var rightBracketIndex = trimmed . LastIndexOf ( '>' ) ;
if ( leftBracketIndex > = 0 & & rightBracketIndex > leftBracketIndex )
return trimmed [ ( leftBracketIndex + 1 ) . . rightBracketIndex ] . Trim ( ) ;
return trimmed . Trim ( ) . Trim ( '<' , '>' , '"' , '\'' ) ;
}
2025-02-16 11:54:23 +01:00
private string CreateHtmlGap ( )
{
var template = $"""<div style=" font - family : ' { _preferencesService . ComposerFont } ' , Arial , sans - serif ; font - size : { _preferencesService . ComposerFontSize } px "><br></div>" "" ;
return string . Concat ( Enumerable . Repeat ( template , 2 ) ) ;
}
2025-02-16 11:43:30 +01:00
2025-02-16 11:54:23 +01:00
private async Task < string > GetSignature ( MailAccount account , DraftCreationReason reason )
{
if ( account . Preferences . IsSignatureEnabled )
2024-08-10 14:33:02 +02:00
{
2025-02-16 11:54:23 +01:00
var signatureId = reason = = DraftCreationReason . Empty ?
account . Preferences . SignatureIdForNewMessages :
account . Preferences . SignatureIdForFollowingMessages ;
2025-02-16 11:35:43 +01:00
2025-02-16 11:54:23 +01:00
if ( signatureId ! = null )
2024-04-18 01:44:37 +02:00
{
2025-02-16 11:54:23 +01:00
var signature = await _signatureService . GetSignatureAsync ( signatureId . Value ) ;
2025-02-16 11:35:43 +01:00
2025-02-16 11:54:23 +01:00
return signature . HtmlBody ;
2025-02-16 11:43:30 +01:00
}
2025-02-16 11:35:43 +01:00
}
2024-08-10 14:33:02 +02:00
2025-02-16 11:54:23 +01:00
return null ;
}
private MimeMessage CreateEmptyDraft ( BodyBuilder builder , MimeMessage message , DraftCreationOptions draftCreationOptions , string signature )
{
builder . HtmlBody = CreateHtmlGap ( ) ;
if ( draftCreationOptions . MailToUri ! = null )
2025-02-16 11:43:30 +01:00
{
2025-02-16 11:54:23 +01:00
if ( draftCreationOptions . MailToUri . Subject ! = null )
message . Subject = draftCreationOptions . MailToUri . Subject ;
if ( draftCreationOptions . MailToUri . Body ! = null )
2025-02-16 11:43:30 +01:00
{
2025-02-16 11:54:23 +01:00
// TODO: In .NET 6+ replace with string "ReplaceLineEndings" method.
var escapedBody = draftCreationOptions . MailToUri . Body . Replace ( "\r\n" , "<br>" ) . Replace ( "\n" , "<br>" ) . Replace ( "\r" , "<br>" ) ;
builder . HtmlBody = $"""<div style=" font - family : ' { _preferencesService . ComposerFont } ' , Arial , sans - serif ; font - size : { _preferencesService . ComposerFontSize } px ">{escapedBody}</div>" "" + builder . HtmlBody ;
}
2024-08-10 14:33:02 +02:00
2025-02-16 11:54:23 +01:00
if ( draftCreationOptions . MailToUri . To . Any ( ) )
message . To . AddRange ( draftCreationOptions . MailToUri . To . Select ( x = > new MailboxAddress ( x , x ) ) ) ;
if ( draftCreationOptions . MailToUri . Cc . Any ( ) )
message . Cc . AddRange ( draftCreationOptions . MailToUri . Cc . Select ( x = > new MailboxAddress ( x , x ) ) ) ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
if ( draftCreationOptions . MailToUri . Bcc . Any ( ) )
message . Bcc . AddRange ( draftCreationOptions . MailToUri . Bcc . Select ( x = > new MailboxAddress ( x , x ) ) ) ;
}
2024-08-10 14:33:02 +02:00
2025-02-16 11:54:23 +01:00
if ( signature ! = null )
builder . HtmlBody + = signature ;
2024-08-10 14:33:02 +02:00
2025-02-16 11:54:23 +01:00
return message ;
}
2025-02-16 11:43:30 +01:00
2026-02-23 01:51:44 +01:00
private MimeMessage CreateReferencedDraft ( BodyBuilder builder ,
MimeMessage message ,
DraftCreationOptions draftCreationOptions ,
string signature ,
ISet < string > ownAddresses )
2025-02-16 11:54:23 +01:00
{
var reason = draftCreationOptions . Reason ;
var referenceMessage = draftCreationOptions . ReferencedMessage . MimeMessage ;
2026-04-07 01:17:52 +02:00
var referenceMailCopy = draftCreationOptions . ReferencedMessage . MailCopy ;
2026-02-23 01:51:44 +01:00
ownAddresses ? ? = new HashSet < string > ( StringComparer . OrdinalIgnoreCase ) ;
2025-02-16 11:43:30 +01:00
2025-02-16 11:54:23 +01:00
var gap = CreateHtmlGap ( ) ;
builder . HtmlBody = gap + CreateHtmlForReferencingMessage ( referenceMessage ) ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
if ( signature ! = null )
2025-02-16 11:35:43 +01:00
{
2025-02-16 11:54:23 +01:00
builder . HtmlBody = gap + signature + builder . HtmlBody ;
}
2025-02-16 11:43:30 +01:00
2025-02-16 11:54:23 +01:00
// Manage "To"
if ( reason = = DraftCreationReason . Reply | | reason = = DraftCreationReason . ReplyAll )
{
2026-02-23 01:51:44 +01:00
var toRecipients = new HashSet < string > ( StringComparer . OrdinalIgnoreCase ) ;
var ccRecipients = new HashSet < string > ( StringComparer . OrdinalIgnoreCase ) ;
void AddToRecipient ( MailboxAddress mailbox , bool allowSelf )
{
var address = mailbox ? . Address ? . Trim ( ) ;
if ( string . IsNullOrWhiteSpace ( address ) )
return ;
if ( ! allowSelf & & ownAddresses . Contains ( address ) )
return ;
if ( ! toRecipients . Add ( address ) )
return ;
message . To . Add ( new MailboxAddress ( mailbox . Name , address ) ) ;
}
void AddCcRecipient ( MailboxAddress mailbox , bool allowSelf )
{
var address = mailbox ? . Address ? . Trim ( ) ;
if ( string . IsNullOrWhiteSpace ( address ) )
return ;
if ( ! allowSelf & & ownAddresses . Contains ( address ) )
return ;
if ( toRecipients . Contains ( address ) | | ! ccRecipients . Add ( address ) )
return ;
message . Cc . Add ( new MailboxAddress ( mailbox . Name , address ) ) ;
}
// Reply target follows Reply-To first, then From, then Sender.
if ( referenceMessage . ReplyTo . Mailboxes . Any ( ) )
{
foreach ( var mailbox in referenceMessage . ReplyTo . Mailboxes )
AddToRecipient ( mailbox , allowSelf : true ) ;
}
else if ( referenceMessage . From . Mailboxes . Any ( ) )
{
foreach ( var mailbox in referenceMessage . From . Mailboxes )
AddToRecipient ( mailbox , allowSelf : true ) ;
}
else if ( referenceMessage . Sender is MailboxAddress senderMailbox )
{
AddToRecipient ( senderMailbox , allowSelf : true ) ;
}
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
if ( reason = = DraftCreationReason . ReplyAll )
2024-08-10 14:33:02 +02:00
{
2025-02-16 11:54:23 +01:00
// Include all of the other original recipients
2026-02-23 01:51:44 +01:00
foreach ( var mailbox in referenceMessage . To . Mailboxes )
AddToRecipient ( mailbox , allowSelf : false ) ;
foreach ( var mailbox in referenceMessage . Cc . Mailboxes )
AddCcRecipient ( mailbox , allowSelf : false ) ;
2024-08-10 14:33:02 +02:00
}
2024-07-18 20:04:11 +02:00
2025-02-16 11:54:23 +01:00
// Self email can be present at this step, when replying to own message. It should be removed only in case there no other recipients.
2026-02-23 01:51:44 +01:00
if ( message . To . Mailboxes . Count ( ) > 1 )
2024-04-18 01:44:37 +02:00
{
2026-02-23 01:51:44 +01:00
var selfRecipients = message . To . Mailboxes
. Where ( m = > ownAddresses . Contains ( m . Address ? ? string . Empty ) )
. ToList ( ) ;
foreach ( var self in selfRecipients )
{
2025-02-16 11:54:23 +01:00
message . To . Remove ( self ) ;
2026-02-23 01:51:44 +01:00
}
2024-07-18 20:04:11 +02:00
}
2024-04-18 01:44:37 +02:00
2026-04-07 01:17:52 +02:00
var referenceMessageId = MailHeaderExtensions . NormalizeMessageId ( referenceMessage . Headers [ HeaderId . MessageId ] ) ;
if ( string . IsNullOrEmpty ( referenceMessageId ) )
referenceMessageId = MailHeaderExtensions . NormalizeMessageId ( referenceMailCopy ? . MessageId ) ;
if ( ! string . IsNullOrEmpty ( referenceMessageId ) )
2024-04-18 01:44:37 +02:00
{
2026-04-07 01:17:52 +02:00
message . InReplyTo = referenceMessageId ;
2025-12-15 21:06:13 +01:00
2026-04-07 01:17:52 +02:00
var existingReferences = referenceMessage . References ? . Select ( MailHeaderExtensions . NormalizeMessageId ) . ToList ( ) ? ? [ ] ;
if ( existingReferences . Count = = 0 )
existingReferences = MailHeaderExtensions . SplitMessageIds ( referenceMailCopy ? . References ) . ToList ( ) ;
var refs = MailHeaderExtensions . BuildReferencesChain ( existingReferences , referenceMessageId ) ;
2025-12-15 21:06:13 +01:00
2026-02-23 01:51:44 +01:00
foreach ( var referenceId in refs )
message . References . Add ( referenceId ) ;
2024-04-18 01:44:37 +02:00
}
2026-02-23 01:51:44 +01:00
if ( ! string . IsNullOrEmpty ( referenceMessage . Subject ) )
message . Headers . Add ( "Thread-Topic" , referenceMessage . Subject ) ;
2025-02-16 11:54:23 +01:00
}
// Manage Subject
2026-02-23 01:51:44 +01:00
var referenceSubject = referenceMessage ? . Subject ? ? string . Empty ;
if ( reason = = DraftCreationReason . Forward & & ! referenceSubject . StartsWith ( "FW: " , StringComparison . OrdinalIgnoreCase ) )
message . Subject = $"FW: {referenceSubject}" ;
2026-04-07 01:17:52 +02:00
else if ( ( reason = = DraftCreationReason . Reply | | reason = = DraftCreationReason . ReplyAll ) & & ! referenceSubject . StartsWith ( "Re:" , StringComparison . OrdinalIgnoreCase ) )
message . Subject = $"Re: {referenceSubject}" ;
2026-02-23 01:51:44 +01:00
else
message . Subject = referenceSubject ;
2024-07-03 23:54:19 +02:00
2025-02-16 11:54:23 +01:00
// Only include attachments if forwarding.
if ( reason = = DraftCreationReason . Forward & & ( referenceMessage ? . Attachments ? . Any ( ) ? ? false ) )
{
foreach ( var attachment in referenceMessage . Attachments )
2024-07-03 23:54:19 +02:00
{
2025-02-16 11:54:23 +01:00
builder . Attachments . Add ( attachment ) ;
2024-07-03 23:54:19 +02:00
}
2025-02-16 11:35:43 +01:00
}
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
return message ;
// Generates html representation of To/Cc/From/Time and so on from referenced message.
string CreateHtmlForReferencingMessage ( MimeMessage referenceMessage )
2025-02-16 11:43:30 +01:00
{
2025-02-16 11:54:23 +01:00
var htmlMimeInfo = string . Empty ;
// Separation Line
htmlMimeInfo + = "<hr style='display:inline-block;width:100%' tabindex='-1'>" ;
var visitor = _mimeFileService . CreateHTMLPreviewVisitor ( referenceMessage , string . Empty ) ;
visitor . Visit ( referenceMessage ) ;
htmlMimeInfo + = $"""
<div id=" divRplyFwdMsg " dir=" ltr ">
< font face = "Calibri, sans-serif" style = "font-size: 11pt;" color = "#000000" >
< b > From : < / b > { ParticipantsToHtml ( referenceMessage . From ) } < br >
< b > Sent : < / b > { referenceMessage . Date . ToLocalTime ( ) } < br >
< b > To : < / b > { ParticipantsToHtml ( referenceMessage . To ) } < br >
{ ( referenceMessage . Cc . Count > 0 ? $"<b>Cc:</b> {ParticipantsToHtml(referenceMessage.Cc)}<br>" : string . Empty ) }
< b > Subject : < / b > { referenceMessage . Subject }
< / font >
< div > & nbsp ; < / div >
{ visitor . HtmlBody }
< / div >
"" ";
return htmlMimeInfo ;
}
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
static string ParticipantsToHtml ( InternetAddressList internetAddresses ) = >
string . Join ( "; " , internetAddresses . Mailboxes
. Select ( x = > $"{x.Name ?? Translator.UnknownSender} <<a href=\" mailto : { x . Address ? ? Translator . UnknownAddress } \ ">{x.Address ?? Translator.UnknownAddress}</a>>" ) ) ;
}
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
public async Task < bool > MapLocalDraftAsync ( Guid accountId , Guid localDraftCopyUniqueId , string newMailCopyId , string newDraftId , string newThreadId )
{
2025-11-15 13:29:02 +01:00
var localDraftCopy = await Connection . FindWithQueryAsync < MailCopy > (
"SELECT MailCopy.* FROM MailCopy INNER JOIN MailItemFolder ON MailCopy.FolderId = MailItemFolder.Id WHERE MailCopy.UniqueId = ? AND MailItemFolder.MailAccountId = ?" ,
localDraftCopyUniqueId , accountId ) ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
if ( localDraftCopy = = null )
{
_logger . Warning ( "Draft mapping failed because local draft copy with unique id {LocalDraftCopyUniqueId} does not exist." , localDraftCopyUniqueId ) ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
return false ;
}
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
var oldLocalDraftId = localDraftCopy . Id ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
await LoadAssignedPropertiesAsync ( localDraftCopy ) . ConfigureAwait ( false ) ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
bool isIdChanging = localDraftCopy . Id ! = newMailCopyId ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
localDraftCopy . Id = newMailCopyId ;
2026-02-07 13:10:57 +01:00
if ( ! string . IsNullOrEmpty ( newDraftId ) )
localDraftCopy . DraftId = newDraftId ;
if ( ! string . IsNullOrEmpty ( newThreadId ) )
localDraftCopy . ThreadId = newThreadId ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
await UpdateMailAsync ( localDraftCopy ) . ConfigureAwait ( false ) ;
2024-04-18 01:44:37 +02:00
2026-02-07 13:10:57 +01:00
ReportUIChange ( new DraftMapped ( oldLocalDraftId , localDraftCopy . DraftId ) ) ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
return true ;
}
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
public Task MapLocalDraftAsync ( string mailCopyId , string newDraftId , string newThreadId )
{
return UpdateAllMailCopiesAsync ( mailCopyId , ( item ) = >
{
2026-02-07 13:10:57 +01:00
var shouldUpdateThreadId = ! string . IsNullOrEmpty ( newThreadId ) ;
var shouldUpdateDraftId = ! string . IsNullOrEmpty ( newDraftId ) ;
if ( ( shouldUpdateThreadId & & item . ThreadId ! = newThreadId ) | |
( shouldUpdateDraftId & & item . DraftId ! = newDraftId ) )
2025-02-16 11:54:23 +01:00
{
var oldDraftId = item . DraftId ;
2024-04-18 01:44:37 +02:00
2026-02-07 13:10:57 +01:00
if ( shouldUpdateDraftId )
item . DraftId = newDraftId ;
if ( shouldUpdateThreadId )
item . ThreadId = newThreadId ;
2024-04-18 01:44:37 +02:00
2026-02-07 13:10:57 +01:00
ReportUIChange ( new DraftMapped ( oldDraftId , item . DraftId ) ) ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
return true ;
}
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
return false ;
} ) ;
}
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
public Task < List < MailCopy > > GetDownloadedUnreadMailsAsync ( Guid accountId , IEnumerable < string > downloadedMailCopyIds )
{
2025-11-15 13:29:02 +01:00
var placeholders = string . Join ( "," , downloadedMailCopyIds . Select ( _ = > "?" ) ) ;
var sql = $"SELECT MailCopy.* FROM MailCopy INNER JOIN MailItemFolder ON MailCopy.FolderId = MailItemFolder.Id WHERE MailCopy.Id IN ({placeholders}) AND MailCopy.IsRead = ? AND MailItemFolder.MailAccountId = ? AND MailItemFolder.SpecialFolderType = ?" ;
var parameters = new List < object > ( ) ;
parameters . AddRange ( downloadedMailCopyIds . Cast < object > ( ) ) ;
parameters . Add ( false ) ;
parameters . Add ( accountId ) ;
parameters . Add ( ( int ) SpecialFolderType . Inbox ) ;
return Connection . QueryAsync < MailCopy > ( sql , parameters . ToArray ( ) ) ;
2025-02-16 11:54:23 +01:00
}
2024-08-05 00:36:26 +02:00
2025-02-16 11:54:23 +01:00
public Task < MailAccount > GetMailAccountByUniqueIdAsync ( Guid uniqueMailId )
{
2025-11-15 13:29:02 +01:00
return Connection . FindWithQueryAsync < MailAccount > (
"SELECT MailAccount.* FROM MailCopy INNER JOIN MailItemFolder ON MailCopy.FolderId = MailItemFolder.Id INNER JOIN MailAccount ON MailItemFolder.MailAccountId = MailAccount.Id WHERE MailCopy.UniqueId = ?" ,
uniqueMailId ) ;
2025-02-16 11:54:23 +01:00
}
2025-02-15 12:53:32 +01:00
2025-02-16 11:54:23 +01:00
public Task < bool > IsMailExistsAsync ( string mailCopyId )
= > Connection . ExecuteScalarAsync < bool > ( "SELECT EXISTS(SELECT 1 FROM MailCopy WHERE Id = ?)" , mailCopyId ) ;
2025-02-15 12:53:32 +01:00
2025-02-16 11:54:23 +01:00
public async Task < List < MailCopy > > GetExistingMailsAsync ( Guid folderId , IEnumerable < MailKit . UniqueId > uniqueIds )
{
2025-10-18 11:45:10 +02:00
var localMailIds = uniqueIds . Select ( a = > MailkitClientExtensions . CreateUid ( folderId , a . Id ) ) . ToArray ( ) ;
2025-02-16 11:35:43 +01:00
2025-11-15 13:29:02 +01:00
var placeholders = string . Join ( "," , localMailIds . Select ( _ = > "?" ) ) ;
var sql = $"SELECT * FROM MailCopy WHERE Id IN ({placeholders})" ;
2025-02-16 11:43:30 +01:00
2025-11-15 13:29:02 +01:00
return await Connection . QueryAsync < MailCopy > ( sql , localMailIds . Cast < object > ( ) . ToArray ( ) ) ;
2025-02-16 11:43:30 +01:00
}
2025-02-16 11:54:23 +01:00
public Task < bool > IsMailExistsAsync ( string mailCopyId , Guid folderId )
= > Connection . ExecuteScalarAsync < bool > ( "SELECT EXISTS(SELECT 1 FROM MailCopy WHERE Id = ? AND FolderId = ?)" , mailCopyId , folderId ) ;
2025-02-23 17:05:46 +01:00
public async Task < GmailArchiveComparisonResult > GetGmailArchiveComparisonResultAsync ( Guid archiveFolderId , List < string > onlineArchiveMailIds )
{
2026-02-12 18:57:55 +01:00
onlineArchiveMailIds ? ? = [ ] ;
2025-02-23 17:05:46 +01:00
var localArchiveMails = await Connection . Table < MailCopy > ( )
. Where ( a = > a . FolderId = = archiveFolderId )
. ToListAsync ( ) . ConfigureAwait ( false ) ;
2026-02-12 18:57:55 +01:00
var onlineArchiveIdSet = onlineArchiveMailIds
. Where ( a = > ! string . IsNullOrWhiteSpace ( a ) )
. ToHashSet ( StringComparer . Ordinal ) ;
var localArchiveIdSet = localArchiveMails
. Select ( a = > a . Id )
. Where ( a = > ! string . IsNullOrWhiteSpace ( a ) )
. ToHashSet ( StringComparer . Ordinal ) ;
var removedMails = localArchiveIdSet . Except ( onlineArchiveIdSet ) . ToArray ( ) ;
var addedMails = onlineArchiveIdSet . Except ( localArchiveIdSet ) . ToArray ( ) ;
2025-02-23 17:05:46 +01:00
return new GmailArchiveComparisonResult ( addedMails , removedMails ) ;
}
2025-02-23 22:17:40 +01:00
2026-02-23 01:51:44 +01:00
private async Task < HashSet < string > > GetOwnAddressesAsync ( MailAccount account )
{
var ownAddresses = new HashSet < string > ( StringComparer . OrdinalIgnoreCase ) ;
if ( ! string . IsNullOrWhiteSpace ( account ? . Address ) )
ownAddresses . Add ( account . Address . Trim ( ) ) ;
var aliases = await _accountService . GetAccountAliasesAsync ( account . Id ) . ConfigureAwait ( false ) ;
if ( aliases ! = null )
{
foreach ( var alias in aliases )
{
if ( ! string . IsNullOrWhiteSpace ( alias ? . AliasAddress ) )
ownAddresses . Add ( alias . AliasAddress . Trim ( ) ) ;
}
}
return ownAddresses ;
}
2026-04-07 01:17:52 +02:00
private static void EnsureOutgoingMessageId ( MimeMessage message )
2026-02-23 01:51:44 +01:00
{
2026-04-07 01:17:52 +02:00
if ( message = = null )
return ;
var messageId = MailHeaderExtensions . NormalizeMessageId ( MessageIdGenerator . Generate ( ) ) ;
if ( string . IsNullOrEmpty ( messageId ) )
return ;
var headerValue = MailHeaderExtensions . ToHeaderMessageId ( messageId ) ;
2026-02-23 01:51:44 +01:00
2026-04-07 01:17:52 +02:00
if ( message . Headers . Contains ( HeaderId . MessageId ) )
message . Headers . Remove ( HeaderId . MessageId ) ;
message . Headers . Add ( HeaderId . MessageId , headerValue ) ;
message . MessageId = messageId ;
2026-02-23 01:51:44 +01:00
}
2026-04-07 01:17:52 +02:00
private static string GetNormalizedMimeMessageId ( MimeMessage message )
= > MailHeaderExtensions . NormalizeMessageId ( message ? . Headers [ HeaderId . MessageId ] ) ;
2026-02-23 01:51:44 +01:00
2026-04-07 01:17:52 +02:00
private static string GetNormalizedMimeInReplyTo ( MimeMessage message )
{
if ( message = = null )
return string . Empty ;
2026-02-23 01:51:44 +01:00
2026-04-07 01:17:52 +02:00
var inReplyTo = string . IsNullOrWhiteSpace ( message . InReplyTo )
? message . Headers [ HeaderId . InReplyTo ]
: message . InReplyTo ;
2026-02-23 01:51:44 +01:00
2026-04-07 01:17:52 +02:00
return MailHeaderExtensions . NormalizeMessageId ( inReplyTo ) ;
}
2026-02-23 01:51:44 +01:00
2026-04-07 01:17:52 +02:00
private static string GetNormalizedMimeReferences ( MimeMessage message )
{
if ( message = = null )
return string . Empty ;
2026-02-23 01:51:44 +01:00
2026-04-07 01:17:52 +02:00
if ( message . References ? . Count > 0 )
return MailHeaderExtensions . JoinStoredReferences ( message . References ) ;
2026-02-23 01:51:44 +01:00
2026-04-07 01:17:52 +02:00
return MailHeaderExtensions . NormalizeReferences ( message . Headers [ HeaderId . References ] ) ;
2026-02-23 01:51:44 +01:00
}
2026-02-06 01:18:12 +01:00
public async Task < IEnumerable < string > > GetRecentMailIdsForFolderAsync ( Guid folderId , int count )
{
var recentMails = await Connection . Table < MailCopy > ( )
. Where ( a = > a . FolderId = = folderId )
. OrderByDescending ( a = > a . CreationDate )
. Take ( count )
. ToListAsync ( )
. ConfigureAwait ( false ) ;
return recentMails . Select ( m = > m . Id ) ;
}
2025-02-23 22:17:40 +01:00
public async Task < List < MailCopy > > GetMailItemsAsync ( IEnumerable < string > mailCopyIds )
{
if ( ! mailCopyIds . Any ( ) ) return [ ] ;
2025-11-15 13:29:02 +01:00
var placeholders = string . Join ( "," , mailCopyIds . Select ( _ = > "?" ) ) ;
var sql = $"SELECT MailCopy.* FROM MailCopy WHERE MailCopy.Id IN ({placeholders})" ;
2025-02-23 22:17:40 +01:00
2025-11-15 13:29:02 +01:00
var mailCopies = await Connection . QueryAsync < MailCopy > ( sql , mailCopyIds . Cast < object > ( ) . ToArray ( ) ) ;
2025-02-23 22:17:40 +01:00
if ( mailCopies ? . Count = = 0 ) return [ ] ;
2026-03-01 09:14:02 +01:00
var folderIds = mailCopies . Select ( m = > m . FolderId ) . Distinct ( ) . ToList ( ) ;
var folderTasks = folderIds . Select ( id = > _folderService . GetFolderAsync ( id ) ) ;
var folders = await Task . WhenAll ( folderTasks ) . ConfigureAwait ( false ) ;
var folderCache = folders . Where ( f = > f ! = null ) . ToDictionary ( f = > f . Id ) ;
2025-02-23 22:17:40 +01:00
2026-03-01 09:14:02 +01:00
var allAccounts = await _accountService . GetAccountsAsync ( ) . ConfigureAwait ( false ) ;
var accountCache = allAccounts . ToDictionary ( a = > a . Id ) ;
var addresses = mailCopies . Where ( m = > ! string . IsNullOrEmpty ( m . FromAddress ) ) . Select ( m = > m . FromAddress ) . Distinct ( ) . ToList ( ) ;
var contactList = await _contactService . GetContactsByAddressesAsync ( addresses ) . ConfigureAwait ( false ) ;
var contactCache = contactList . ToDictionary ( c = > c . Address ) ;
AssignPropertiesFromCaches ( mailCopies , folderCache , accountCache , contactCache ) ;
2025-02-23 22:17:40 +01:00
return mailCopies ;
}
public async Task < List < string > > AreMailsExistsAsync ( IEnumerable < string > mailCopyIds )
{
2025-11-15 13:29:02 +01:00
var placeholders = string . Join ( "," , mailCopyIds . Select ( _ = > "?" ) ) ;
var sql = $"SELECT Id FROM MailCopy WHERE Id IN ({placeholders})" ;
2025-02-23 22:17:40 +01:00
2025-11-15 13:29:02 +01:00
return await Connection . QueryScalarsAsync < string > ( sql , mailCopyIds . Cast < object > ( ) . ToArray ( ) ) ;
2025-02-23 22:17:40 +01:00
}
2026-02-07 19:47:21 +01:00
public Task < List < MailCopy > > GetMailCopiesBeforeDateAsync ( Guid accountId , DateTime cutoffDateUtc )
{
const string query = "" "
SELECT MailCopy . *
FROM MailCopy
INNER JOIN MailItemFolder ON MailCopy . FolderId = MailItemFolder . Id
WHERE MailItemFolder . MailAccountId = ?
AND MailCopy . CreationDate < ?
"" ";
return Connection . QueryAsync < MailCopy > ( query , accountId , cutoffDateUtc ) ;
}
2024-04-18 01:44:37 +02:00
}