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 ;
2026-02-23 01:51:44 +01:00
var referenceMailCopy = draftCreationOptions . ReferencedMessage . MailCopy ;
2024-04-18 01:44:37 +02:00
2026-02-23 01:51:44 +01:00
string referenceMessageId = refMime ? . MessageId ;
string referenceInReplyTo = refMime ? . InReplyTo ;
IEnumerable < string > referenceChain = refMime ? . References ? ? [ ] ;
2026-02-06 20:13:44 +01:00
2026-02-23 01:51:44 +01:00
// Fallback to MailCopy metadata if MIME lacks threading headers.
if ( string . IsNullOrWhiteSpace ( referenceMessageId ) & & referenceMailCopy ! = null )
2026-02-06 20:13:44 +01:00
{
2026-02-23 01:51:44 +01:00
referenceMessageId = referenceMailCopy . MessageId ;
referenceInReplyTo = referenceMailCopy . InReplyTo ;
referenceChain = SplitStoredReferences ( referenceMailCopy . References ) ;
2026-02-06 20:13:44 +01:00
}
2026-02-23 01:51:44 +01:00
if ( ! string . IsNullOrWhiteSpace ( referenceMessageId ) )
copy . InReplyTo = MailHeaderExtensions . StripAngleBrackets ( referenceMessageId ) ;
var refs = BuildReferencesChain ( referenceChain , referenceInReplyTo , referenceMessageId ) ;
2026-02-06 20:13:44 +01:00
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 ;
2026-02-23 01:51:44 +01:00
// Fallback local threading when provider/native thread id is unavailable.
if ( string . IsNullOrWhiteSpace ( copy . ThreadId ) )
copy . ThreadId = refs . 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 ( ) ;
IEnumerable < MailCopy > query = options . PreFetchMailCopies
. Where ( m = > m ! = null & & allowedFolderIds . Contains ( m . FolderId ) )
. GroupBy ( m = > m . UniqueId )
. Select ( g = > g . First ( ) ) ;
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 ) ) ;
}
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 ( ) ;
}
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 )
{
2026-02-12 18:57:55 +01:00
mails = ApplyOptionsToPreFetchedMails ( options ) ;
2025-02-22 00:22:00 +01:00
}
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 ;
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 ) ;
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 ) ;
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
}
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 ) ;
}
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 ) ;
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
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-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
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
{
2026-02-23 01:51:44 +01:00
message . InReplyTo = MailHeaderExtensions . StripAngleBrackets ( referenceMessage . MessageId ) ;
2025-12-15 21:06:13 +01:00
2026-02-23 01:51:44 +01:00
var refs = BuildReferencesChain (
referenceMessage . References ,
referenceMessage . InReplyTo ,
referenceMessage . MessageId ) ;
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
}
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 ) )
{
2026-02-23 01:51:44 +01:00
message . InReplyTo = MailHeaderExtensions . StripAngleBrackets ( referenceMailCopy . MessageId ) ;
var refs = BuildReferencesChain (
SplitStoredReferences ( referenceMailCopy . References ) ,
referenceMailCopy . InReplyTo ,
referenceMailCopy . MessageId ) ;
foreach ( var referenceId in refs )
message . References . Add ( referenceId ) ;
2025-11-01 21:46:23 +01:00
}
}
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}" ;
else if ( ( reason = = DraftCreationReason . Reply | | reason = = DraftCreationReason . ReplyAll ) & & ! referenceSubject . StartsWith ( "RE: " , StringComparison . OrdinalIgnoreCase ) )
message . Subject = $"RE: {referenceSubject}" ;
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 ;
}
private static IEnumerable < string > SplitStoredReferences ( string references )
{
if ( string . IsNullOrWhiteSpace ( references ) )
return [ ] ;
return references
. Split ( new [ ] { ';' , ',' , ' ' , '\t' , '\r' , '\n' } , StringSplitOptions . RemoveEmptyEntries )
. Select ( r = > r . Trim ( ) ) ;
}
private static List < string > BuildReferencesChain ( IEnumerable < string > existingReferences , string parentInReplyTo , string parentMessageId )
{
var results = new List < string > ( ) ;
var seen = new HashSet < string > ( StringComparer . OrdinalIgnoreCase ) ;
void AddReference ( string value )
{
var normalized = MailHeaderExtensions . StripAngleBrackets ( value ) ? . Trim ( ) ;
if ( string . IsNullOrWhiteSpace ( normalized ) )
return ;
if ( ! seen . Add ( normalized ) )
return ;
results . Add ( normalized ) ;
}
if ( existingReferences ! = null )
{
foreach ( var reference in existingReferences )
AddReference ( reference ) ;
}
// RFC 5322 fallback: if References is absent, include parent In-Reply-To first when available.
if ( results . Count = = 0 )
AddReference ( parentInReplyTo ) ;
AddReference ( parentMessageId ) ;
return results ;
}
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
}
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
}