Improved mailboxes to include sent mails and drafts (#1038)

This commit is contained in:
Thorsten Sommer authored and GitHub committed 2026-10-10 10:48:54 +02:00
1 parent 67d44588d4
commit 5c319c45a0
31 files changed
+792 -103

No files matched your search

@@ -20,4 +20,10 @@ internal enum DataSourceEmbeddingRefreshMode
/// changes, e.g. a mailbox on a server.
/// </summary>
INTERVAL_CHECK,
/// <summary>
/// A tool is about to read what may have changed since the last run, e.g. the drafts of a
/// mailbox. Unlike a retry of the user, it never signs in despite a refused sign-in.
/// </summary>
TOOL_REQUEST,
}
@@ -408,6 +408,32 @@ public sealed partial class DataSourceEmbeddingService(SettingsManager settingsM
: Task.CompletedTask;
}
/// <summary>
/// Syncs a mailbox soon, because a tool is about to read what may have changed on its server since the last sync.
/// </summary>
/// <remarks>
/// Runs like a sync at the interval: only while the user lets the data sources refresh on their
/// own, and never despite a refused sign-in. A sync which is running already gets one follow-up,
/// since it may have looked at the folder in question before the change. One which is queued
/// already stays the only one.
/// </remarks>
/// <param name="mailboxId">The id of the mailbox.</param>
/// <returns>True when a sync is on its way; false when the user switched the automatic refresh off, the mailbox is not configured, or it waits for the user to sign in again.</returns>
public async Task<bool> RequestMailboxSyncAsync(string mailboxId)
{
if (!settingsManager.ConfigurationData.App.DataSourceIndexing.AutomaticRefresh)
return false;
if (!this.TryGetConfiguredIndexedSource(mailboxId, out var dataSource) || dataSource is not DataSourceMailbox || !this.IsSupportedIndexedSource(dataSource))
return false;
if (this.statuses.TryGetValue(mailboxId, out var status) && IsWaitingForSignIn(status))
return false;
await this.QueueDataSourceAsync(dataSource, true, DataSourceEmbeddingRefreshMode.TOOL_REQUEST);
return true;
}
private async Task QueueDataSourceAsync(IDataSourceBase dataSource, bool queueAfterCurrentRun, DataSourceEmbeddingRefreshMode refreshMode)
{
if (!this.IsSupportedIndexedSource(dataSource))
@@ -219,7 +219,7 @@ internal sealed partial class MailboxIndexer(SettingsManager settingsManager, Ru
try
{
var selection = MailFolderSelection.Select(await connector.GetFoldersAsync(token), mailbox.RootFolder);
var selection = MailFolderSelection.Select(await connector.GetFoldersAsync(token), mailbox.RootFolder, mailbox.IncludeSentAndDrafts);
if (!selection.RootFolderFound)
{
//
@@ -385,7 +385,7 @@ internal sealed partial class MailboxIndexer(SettingsManager settingsManager, Ru
var serverState = await connector.OpenFolderAsync(folder.FullName, token);
var storedFolder = storedFolders.GetValueOrDefault(folder.FullName);
var storedLocations = await context.IndexStore.GetMailLocationsAsync(mailbox.Id, folder.FullName, token);
var indexedUids = await connector.SearchIndexedMailsAsync(receivedSince, token);
var indexedUids = await connector.SearchIndexedMailsAsync(MailFolderSelection.GetReceivedSince(folder, receivedSince), token);
var plan = MailFolderSyncPlan.Create(storedFolder, storedLocations, serverState, indexedUids);
//
@@ -498,7 +498,7 @@ internal sealed partial class MailboxIndexer(SettingsManager settingsManager, Ru
/// </remarks>
private static async Task<PlannedRemovals> CollectRemovalsAsync(IndexedRunContext context, DataSourceMailbox mailbox, IReadOnlyList<PlannedFolder> plannedFolders, IEnumerable<string> storedFolderPaths, CancellationToken token)
{
// Folders which the server no longer lists, or which no longer lie below the root folder:
// Folders which the server no longer lists, or which the sync no longer selects, e.g. since the mailbox no longer includes sent mails and drafts:
var plannedPaths = plannedFolders.Select(folder => folder.Folder.FullName).ToHashSet(StringComparer.Ordinal);
var goneFolderPaths = storedFolderPaths.Where(path => !plannedPaths.Contains(path)).ToList();
@@ -10,4 +10,5 @@ namespace AIStudio.Tools.Services;
/// <param name="Summary">What a list of mails shows about the mail, e.g., its subject and its folders.</param>
/// <param name="Mail">Its addresses, its parts as AI Studio read them, and its places on the server.</param>
/// <param name="InReplyToMailId">The id of the mail it replies to, or null when the index does not hold that one.</param>
public sealed record MailReading(DataSourceMailbox Mailbox, MailSummary Summary, MailRecord Mail, string? InReplyToMailId);
/// <param name="MailboxFolders">The folders of the mailbox, which tell what the folders of the mail are for, e.g., the sent mails.</param>
public sealed record MailReading(DataSourceMailbox Mailbox, MailSummary Summary, MailRecord Mail, string? InReplyToMailId, IReadOnlyList<MailFolderRecord> MailboxFolders);
@@ -5,7 +5,7 @@ namespace AIStudio.Tools.Services;
/// <summary>
/// How far the index of a mailbox reaches, so that an answer from it can say what it does not cover.
/// </summary>
/// <param name="ReceivedSinceUtc">Since when mails are indexed, or null when all of them are. Flagged mails are indexed regardless of their age.</param>
/// <param name="ReceivedSinceUtc">Since when mails are indexed, or null when all of them are. Flagged mails and drafts are indexed regardless of their age.</param>
/// <param name="LastCompleteSyncUtc">When the last complete sync ended, or null while the first one is still running.</param>
/// <param name="SignInRefusedAtUtc">When the server refused to let AI Studio sign in, or null when it did not. Until the user deals with that, no new mail arrives in the index.</param>
/// <param name="PendingRemovalCount">How many mails the index still holds although a sync would have removed them, waiting for the user to agree, or null when there are none.</param>
@@ -381,7 +381,8 @@ public sealed class MailboxRetrievalService(SettingsManager settingsManager, Dat
continue;
var inReplyToMailId = await indexStore.FindMailByMessageIdAsync(mailbox.Id, mail.InReplyTo, token);
return new(mailbox, summaries[0], mail, inReplyToMailId == mailId ? null : inReplyToMailId);
var folders = await indexStore.GetMailFoldersAsync(mailbox.Id, token);
return new(mailbox, summaries[0], mail, inReplyToMailId == mailId ? null : inReplyToMailId, folders);
}
return null;