2024-04-18 01:44:37 +02:00
using System ;
2025-10-31 01:47:33 +01:00
using System.Collections.Concurrent ;
2024-04-18 01:44:37 +02:00
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 ;
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 ;
private readonly ILogger _logger = Log . ForContext < MailService > ( ) ;
public MailService ( IDatabaseService databaseService ,
IFolderService folderService ,
IContactService contactService ,
IAccountService accountService ,
ISignatureService signatureService ,
IMimeFileService mimeFileService ,
IPreferencesService preferencesService ) : 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 ;
}
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 ) ;
var createdDraftMimeMessage = await CreateDraftMimeAsync ( composerAccount , draftCreationOptions ) ;
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 ,
FromAddress = primaryAlias ? . AliasAddress ? ? composerAccount . Address ,
FromName = composerAccount . SenderName ,
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 ,
FileId = Guid . NewGuid ( )
} ;
2026-02-06 20:13:44 +01:00
// If replying, add In-Reply-To, ThreadId and References per RFC 5322.
// References must include all previous References + the Message-ID of the message being replied to.
2025-02-16 11:54:23 +01:00
if ( draftCreationOptions . ReferencedMessage ! = null )
{
2026-02-06 20:13:44 +01:00
var refMime = draftCreationOptions . ReferencedMessage . MimeMessage ;
var refs = new List < string > ( ) ;
2024-04-18 01:44:37 +02:00
2026-02-06 20:13:44 +01:00
if ( refMime . References ! = null )
refs . AddRange ( refMime . References ) ;
if ( ! string . IsNullOrEmpty ( refMime . MessageId ) )
{
copy . InReplyTo = refMime . MessageId ;
refs . Add ( refMime . MessageId ) ;
}
if ( refs . Count > 0 )
copy . References = string . Join ( ";" , refs ) ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
if ( ! string . IsNullOrEmpty ( draftCreationOptions . ReferencedMessage . MailCopy ? . ThreadId ) )
copy . ThreadId = draftCreationOptions . ReferencedMessage . MailCopy . ThreadId ;
}
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 ;
}
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
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
{
2025-02-22 00:22:00 +01:00
List < MailCopy > mails = null ;
2024-04-18 01:44:37 +02:00
2025-02-22 00:22:00 +01:00
// If user performs an online search, mail copies are passed to options.
if ( options . PreFetchMailCopies ! = null )
{
mails = options . PreFetchMailCopies ;
}
else
{
// If not just do the query.
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
2025-10-31 01:47:33 +01:00
ConcurrentDictionary < Guid , MailItemFolder > folderCache = new ( ) ;
ConcurrentDictionary < Guid , MailAccount > accountCache = new ( ) ;
ConcurrentDictionary < string , AccountContact > contactCache = new ( ) ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
// Populate Folder Assignment for each single mail, to be able later group by "MailAccountId".
// This is needed to execute threading strategy by account type.
// Avoid DBs calls as possible, storing info in a dictionary.
foreach ( var mail in mails )
{
2025-02-23 22:17:40 +01:00
await LoadAssignedPropertiesWithCacheAsync ( mail , folderCache , accountCache , contactCache ) . 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
// Remove items that has no assigned account or folder.
mails . RemoveAll ( a = > a . AssignedAccount = = null | | a . AssignedFolder = = null ) ;
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
2025-10-26 23:35:09 +01:00
// If CreateThreads is false, just return the mails as-is
if ( ! options . CreateThreads )
{
return [ . . mails ] ;
}
2025-10-31 01:41:51 +01:00
// Include other mails in the same threads - batch process to reduce DB calls
2025-10-26 23:35:09 +01:00
var expandedMails = new List < MailCopy > ( mails ) ;
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
2025-10-31 01:41:51 +01:00
if ( uniqueThreadIds . Count > 0 )
2025-10-26 23:35:09 +01:00
{
2025-10-31 01:41:51 +01:00
// Get all thread mails in a single DB call
var existingMailIds = expandedMails . Select ( m = > m . Id ) . ToHashSet ( ) ;
var allThreadMails = await GetMailsByThreadIdsAsync ( uniqueThreadIds , existingMailIds ) . ConfigureAwait ( false ) ;
2025-10-26 23:35:09 +01:00
2025-10-31 01:41:51 +01:00
if ( allThreadMails ? . Count > 0 )
{
// Process thread mails in parallel to improve performance
var tasks = allThreadMails . Select ( async threadMail = >
2025-10-26 23:35:09 +01:00
{
2025-10-31 01:41:51 +01:00
await LoadAssignedPropertiesWithCacheAsync ( threadMail , folderCache , accountCache , contactCache ) . ConfigureAwait ( false ) ;
return threadMail ;
} ) ;
var processedThreadMails = await Task . WhenAll ( tasks ) . ConfigureAwait ( false ) ;
2025-12-15 21:06:13 +01:00
2025-10-31 01:41:51 +01:00
// Filter out items with no assigned account or folder
var validThreadMails = processedThreadMails . Where ( m = > m . AssignedAccount ! = null & & m . AssignedFolder ! = null ) ;
2025-12-15 21:06:13 +01:00
2025-10-31 01:41:51 +01:00
expandedMails . AddRange ( validThreadMails ) ;
2025-10-26 23:35:09 +01:00
}
cancellationToken . ThrowIfCancellationRequested ( ) ;
}
return [ . . expandedMails ] ;
}
private async Task < List < MailCopy > > GetMailsByThreadIdAsync ( string threadId , HashSet < string > excludeMailIds )
{
if ( string . IsNullOrEmpty ( threadId ) )
return [ ] ;
2025-11-15 13:29:02 +01:00
var placeholders = string . Join ( "," , excludeMailIds . Select ( _ = > "?" ) ) ;
var sql = $"SELECT MailCopy.* FROM MailCopy WHERE ThreadId = ? AND Id NOT IN ({placeholders})" ;
var parameters = new List < object > { threadId } ;
parameters . AddRange ( excludeMailIds . Cast < object > ( ) ) ;
2025-10-26 23:35:09 +01:00
2025-11-15 13:29:02 +01:00
return await Connection . QueryAsync < MailCopy > ( sql , parameters . ToArray ( ) ) ;
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 excludePlaceholders = string . Join ( "," , excludeMailIds . Select ( _ = > "?" ) ) ;
var sql = $"SELECT MailCopy.* FROM MailCopy WHERE ThreadId IN ({threadPlaceholders}) AND Id NOT IN ({excludePlaceholders})" ;
var parameters = new List < object > ( ) ;
parameters . AddRange ( threadIds . Cast < object > ( ) ) ;
parameters . AddRange ( excludeMailIds . Cast < object > ( ) ) ;
2025-10-31 01:41:51 +01:00
2025-11-15 13:29:02 +01:00
return await Connection . QueryAsync < MailCopy > ( sql , parameters . ToArray ( ) ) . ConfigureAwait ( false ) ;
2025-10-31 01:41:51 +01:00
}
2025-02-23 22:17:40 +01:00
/// <summary>
/// This method should used for operations with multiple mailItems. Don't use this for single mail items.
/// Called method should provide own instances for caches.
/// </summary>
2025-10-31 01:47:33 +01:00
private async Task LoadAssignedPropertiesWithCacheAsync ( MailCopy mail , ConcurrentDictionary < Guid , MailItemFolder > folderCache , ConcurrentDictionary < Guid , MailAccount > accountCache , ConcurrentDictionary < string , AccountContact > contactCache )
2025-02-23 22:17:40 +01:00
{
if ( mail is MailCopy mailCopy )
{
var isFolderCached = folderCache . TryGetValue ( mailCopy . FolderId , out MailItemFolder folderAssignment ) ;
MailAccount accountAssignment = null ;
if ( ! isFolderCached )
2025-02-16 11:43:30 +01:00
{
2025-02-23 22:17:40 +01:00
folderAssignment = await _folderService . GetFolderAsync ( mailCopy . FolderId ) . ConfigureAwait ( false ) ;
folderCache . TryAdd ( mailCopy . FolderId , folderAssignment ) ;
}
2025-02-16 11:54:23 +01:00
2025-02-23 22:17:40 +01:00
if ( folderAssignment ! = null )
{
var isAccountCached = accountCache . TryGetValue ( folderAssignment . MailAccountId , out accountAssignment ) ;
if ( ! isAccountCached )
2024-04-30 00:04:59 +02:00
{
2025-02-23 22:17:40 +01:00
accountAssignment = await _accountService . GetAccountAsync ( folderAssignment . MailAccountId ) . ConfigureAwait ( false ) ;
2024-04-18 01:44:37 +02:00
2025-02-23 22:17:40 +01:00
accountCache . TryAdd ( folderAssignment . MailAccountId , accountAssignment ) ;
2025-02-16 11:54:23 +01:00
}
2025-02-23 22:17:40 +01:00
}
2024-08-30 01:03:35 +02:00
2025-02-23 22:17:40 +01:00
AccountContact contactAssignment = null ;
2024-08-23 01:07:00 +02:00
2025-02-23 22:17:40 +01:00
bool isContactCached = ! string . IsNullOrEmpty ( mailCopy . FromAddress ) & &
contactCache . TryGetValue ( mailCopy . FromAddress , out contactAssignment ) ;
if ( ! isContactCached & & accountAssignment ! = null )
{
contactAssignment = await GetSenderContactForAccountAsync ( accountAssignment , mailCopy . FromAddress ) . ConfigureAwait ( false ) ;
2024-08-23 02:07:50 +02:00
2025-02-23 22:17:40 +01:00
if ( contactAssignment ! = null )
2025-02-16 11:54:23 +01:00
{
2025-02-23 22:17:40 +01:00
contactCache . TryAdd ( mailCopy . FromAddress , contactAssignment ) ;
2025-02-16 11:43:30 +01:00
}
2024-04-18 01:44:37 +02:00
}
2025-02-23 22:17:40 +01:00
mailCopy . AssignedFolder = folderAssignment ;
mailCopy . AssignedAccount = accountAssignment ;
mailCopy . SenderContact = contactAssignment ? ? CreateUnknownContact ( mailCopy . FromName , mailCopy . FromAddress ) ;
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-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.
if ( fromAddress = = account . Address )
{
return Task . FromResult ( new AccountContact ( ) { Address = account . Address , Name = account . SenderName , Base64ContactPicture = account . Base64ProfilePictureData } ) ;
}
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
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 ) ;
}
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
2025-02-16 11:54:23 +01:00
ReportUIChange ( new MailAddedMessage ( mailCopy ) ) ;
}
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-01-27 21:21:04 +01:00
ReportUIChange ( new MailUpdatedMessage ( mailCopy , MailUpdateSource . 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
2025-02-16 11:54:23 +01:00
ReportUIChange ( new MailRemovedMessage ( mailCopy ) ) ;
}
#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
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 ;
var mimeSaveTask = _mimeFileService . SaveMimeMessageAsync ( mailCopy . FileId , mimeMessage , account . Id ) ;
var contactSaveTask = _contactService . SaveAddressInformationAsync ( mimeMessage ) ;
var insertMailTask = InsertMailAsync ( mailCopy ) ;
await Task . WhenAll ( mimeSaveTask , contactSaveTask , insertMailTask ) . ConfigureAwait ( false ) ;
}
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
// Save contact information.
await _contactService . SaveAddressInformationAsync ( mimeMessage ) . ConfigureAwait ( false ) ;
}
2025-02-16 11:35:43 +01:00
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 ) ;
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 ) ;
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
}
private async Task < MimeMessage > CreateDraftMimeAsync ( MailAccount account , DraftCreationOptions draftCreationOptions )
{
// 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 ( ) } } ,
} ;
2024-08-10 14:33:02 +02:00
2025-02-16 11:54:23 +01:00
var primaryAlias = 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.
message . From . Add ( new MailboxAddress ( account . SenderName , primaryAlias . AliasAddress ) ) ;
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 ) ;
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 ) ,
_ = > CreateReferencedDraft ( builder , message , draftCreationOptions , account , signature ) ,
} ;
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
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
2025-02-16 11:54:23 +01:00
private MimeMessage CreateReferencedDraft ( BodyBuilder builder , MimeMessage message , DraftCreationOptions draftCreationOptions , MailAccount account , string signature )
{
var reason = draftCreationOptions . Reason ;
var referenceMessage = draftCreationOptions . ReferencedMessage . MimeMessage ;
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 )
{
// Reply to the sender of the message
if ( referenceMessage . ReplyTo . Count > 0 )
message . To . AddRange ( referenceMessage . ReplyTo ) ;
else if ( referenceMessage . From . Count > 0 )
message . To . AddRange ( referenceMessage . From ) ;
else if ( referenceMessage . Sender ! = null )
message . To . Add ( referenceMessage . Sender ) ;
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
message . To . AddRange ( referenceMessage . To . Where ( x = > x is MailboxAddress mailboxAddress & & ! mailboxAddress . Address . Equals ( account . Address , StringComparison . OrdinalIgnoreCase ) ) ) ;
message . Cc . AddRange ( referenceMessage . Cc . Where ( x = > x is MailboxAddress mailboxAddress & & ! mailboxAddress . Address . Equals ( account . Address , StringComparison . OrdinalIgnoreCase ) ) ) ;
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.
if ( message . To . Count > 1 )
2024-04-18 01:44:37 +02:00
{
2025-02-16 11:54:23 +01:00
var self = message . To . FirstOrDefault ( x = > x is MailboxAddress mailboxAddress & & mailboxAddress . Address . Equals ( account . Address , StringComparison . OrdinalIgnoreCase ) ) ;
if ( self ! = null )
message . To . Remove ( self ) ;
2024-07-18 20:04:11 +02:00
}
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
// Manage "ThreadId-ConversationId"
2025-11-01 21:46:23 +01:00
// CRITICAL: In-Reply-To and References headers are essential for threading
// They must reference the original message's Message-ID from the MIME headers
2025-02-16 11:54:23 +01:00
if ( ! string . IsNullOrEmpty ( referenceMessage . MessageId ) )
2024-04-18 01:44:37 +02:00
{
2025-02-16 11:54:23 +01:00
message . InReplyTo = referenceMessage . MessageId ;
2025-12-15 21:06:13 +01:00
2025-11-01 21:46:23 +01:00
// Add all previous References first
if ( referenceMessage . References ! = null & & referenceMessage . References . Count > 0 )
{
message . References . AddRange ( referenceMessage . References ) ;
}
2025-12-15 21:06:13 +01:00
2025-11-01 21:46:23 +01:00
// Then add the message we're replying to
2025-02-16 11:54:23 +01:00
message . References . Add ( referenceMessage . MessageId ) ;
2024-04-18 01:44:37 +02:00
}
2025-11-01 21:46:23 +01:00
else
{
// WARNING: Reference message has no Message-ID!
// This will break threading. Try to use the MessageId from MailCopy if available.
var referenceMailCopy = draftCreationOptions . ReferencedMessage . MailCopy ;
if ( referenceMailCopy ! = null & & ! string . IsNullOrEmpty ( referenceMailCopy . MessageId ) )
{
message . InReplyTo = referenceMailCopy . MessageId ;
2025-12-15 21:06:13 +01:00
2025-11-01 21:46:23 +01:00
if ( ! string . IsNullOrEmpty ( referenceMailCopy . References ) )
{
2026-02-06 20:13:44 +01:00
// Parse the References string (supports both ";" and "," separators for backward compatibility)
var references = referenceMailCopy . References . Split ( new [ ] { ';' , ',' } , StringSplitOptions . RemoveEmptyEntries ) ;
2025-11-01 21:46:23 +01:00
foreach ( var reference in references )
{
message . References . Add ( reference . Trim ( ) ) ;
}
}
2025-12-15 21:06:13 +01:00
2025-11-01 21:46:23 +01:00
message . References . Add ( referenceMailCopy . MessageId ) ;
}
}
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
message . Headers . Add ( "Thread-Topic" , referenceMessage . Subject ) ;
}
// Manage Subject
if ( reason = = DraftCreationReason . Forward & & ! referenceMessage . Subject . StartsWith ( "FW: " , StringComparison . OrdinalIgnoreCase ) )
message . Subject = $"FW: {referenceMessage.Subject}" ;
else if ( ( reason = = DraftCreationReason . Reply | | reason = = DraftCreationReason . ReplyAll ) & & ! referenceMessage . Subject . StartsWith ( "RE: " , StringComparison . OrdinalIgnoreCase ) )
message . Subject = $"RE: {referenceMessage.Subject}" ;
else if ( referenceMessage ! = null )
message . Subject = referenceMessage . Subject ;
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 )
{
var localArchiveMails = await Connection . Table < MailCopy > ( )
. Where ( a = > a . FolderId = = archiveFolderId )
. ToListAsync ( ) . ConfigureAwait ( false ) ;
var removedMails = localArchiveMails . Where ( a = > ! onlineArchiveMailIds . Contains ( a . Id ) ) . Select ( a = > a . Id ) . Distinct ( ) . ToArray ( ) ;
var addedMails = onlineArchiveMailIds . Where ( a = > ! localArchiveMails . Select ( b = > b . Id ) . Contains ( a ) ) . Distinct ( ) . ToArray ( ) ;
return new GmailArchiveComparisonResult ( addedMails , removedMails ) ;
}
2025-02-23 22:17:40 +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 [ ] ;
2025-10-31 01:47:33 +01:00
ConcurrentDictionary < Guid , MailItemFolder > folderCache = new ( ) ;
ConcurrentDictionary < Guid , MailAccount > accountCache = new ( ) ;
ConcurrentDictionary < string , AccountContact > contactCache = new ( ) ;
2025-02-23 22:17:40 +01:00
foreach ( var mail in mailCopies )
{
await LoadAssignedPropertiesWithCacheAsync ( mail , folderCache , accountCache , contactCache ) . ConfigureAwait ( false ) ;
}
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
}
2024-04-18 01:44:37 +02:00
}