Added mailboxes as local data sources (#1022)
Build and Release / Determine run mode (push) Waiting to run
Build and Release / Read metadata (push) Blocked by required conditions
Build and Release / Sync Flatpak repo (push) Blocked by required conditions
Build and Release / Collect Flatpak artifacts (push) Blocked by required conditions
Build and Release / Verify (push) Waiting to run
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Publish release (push) Blocked by required conditions

This commit is contained in:
Thorsten Sommer authored and GitHub committed 2026-10-04 12:26:26 +02:00
1 parent 93be539e50
commit c4400c0ff6
343 files changed
+26051 -3122

No files matched your search

@@ -42,7 +42,25 @@ public sealed class Data
/// A collection of data sources configured.
/// </summary>
public List<IDataSource> DataSources { get; set; } = [];
/// <summary>
/// A collection of mailboxes configured.
/// </summary>
/// <remarks>
/// A list of its own rather than a part of DataSources: classic RAG, Semantic Search and the
/// agents read DataSources, and none of them is meant to see a mailbox. It also keeps the
/// settings readable for older versions of AI Studio, so the settings version stays the same.
/// An older version skips a property it does not know, but would fail to read the whole file
/// over a data source type it does not know. It drops the list, though, the next time it stores
/// the settings. The numbers come from NextDataSourceNum, which the data sources use as well.
/// </remarks>
public List<DataSourceMailbox> Mailboxes { get; set; } = [];
/// <summary>
/// Settings which apply to all mailboxes.
/// </summary>
public DataMailboxes MailboxSettings { get; init; } = new(x => x.MailboxSettings);
/// <summary>
/// List of configured profiles.
/// </summary>
@@ -181,6 +181,15 @@ public sealed class DataApp(Expression<Func<Data, DataApp>>? configSelection = n
/// </summary>
public bool AllowUserToAddTranscriptionProvider { get; set; } = ManagedConfiguration.Register(configSelection, n => n.AllowUserToAddTranscriptionProvider, true);
/// <summary>
/// Should the user be allowed to add mailboxes?
/// </summary>
/// <remarks>
/// Independent of AllowUserToAddProvider, because a mailbox is a data source, not a provider.
/// Mailboxes the user added before stay, and the user may still edit or delete them.
/// </remarks>
public bool AllowUserToAddMailbox { get; set; } = ManagedConfiguration.Register(configSelection, n => n.AllowUserToAddMailbox, true);
/// <summary>
/// Should the user be allowed to import plugin archives from disk?
/// </summary>
@@ -0,0 +1,148 @@
using System.Diagnostics.CodeAnalysis;
using AIStudio.Tools.Mail;
using AIStudio.Tools.PluginSystem;
using Lua;
namespace AIStudio.Settings.DataModel;
/// <summary>
/// A mail server of the organization, which a configuration plugin offers for new mailboxes.
/// </summary>
/// <remarks>
/// Live plugin content rather than a stored configuration object: it is a choice in the mailbox
/// dialog, not something the user owns. Large organizations run more than one mail system, so a
/// plugin may define several, and the ones of all plugins are offered together. Username and
/// password stay with each user.
/// </remarks>
public sealed record DataMailboxProvider : ILivePluginContent
{
/// <summary>
/// The stable ID of the mail server.
/// </summary>
public string Id { get; private init; } = string.Empty;
/// <summary>
/// The ID of the enterprise configuration plugin that provides this mail server.
/// </summary>
public Guid EnterpriseConfigurationPluginId { get; private init; } = Guid.Empty;
/// <summary>
/// The name shown to the user, e.g., the name of the mail system.
/// </summary>
public string Name { get; private init; } = string.Empty;
/// <summary>
/// The host of the IMAP server, as the organization wrote it.
/// </summary>
public string Host { get; private init; } = string.Empty;
/// <summary>
/// The port of the IMAP server.
/// </summary>
public int Port { get; private init; } = MailboxTransportSecurityExtensions.SSL_ON_CONNECT_PORT;
/// <summary>
/// How the connection to the IMAP server is encrypted.
/// </summary>
public MailboxTransportSecurity TransportSecurity { get; private init; } = MailboxTransportSecurity.SSL_ON_CONNECT;
/// <summary>
/// What the user enters as the username, in the words of the organization. Empty when the organization gave no hint.
/// </summary>
public string UsernameHint { get; private init; } = string.Empty;
/// <summary>
/// The page of the organization which explains how to set up the mailbox. Empty when there is none.
/// </summary>
public string HelpUrl { get; private init; } = string.Empty;
/// <summary>
/// Reads a mail server from an entry of the table MAILBOX_PROVIDERS.
/// </summary>
/// <remarks>
/// An entry with an invalid field is dropped as a whole, also when only an optional field is
/// invalid. Offering a server with a part of its settings left out would let the user connect
/// differently than the organization meant.
/// </remarks>
/// <param name="index">The position of the entry, for the log.</param>
/// <param name="table">The entry.</param>
/// <param name="configPluginId">The ID of the configuration plugin which defines the entry.</param>
/// <param name="logger">The logger to report an invalid entry to.</param>
/// <param name="provider">The mail server, when the entry is valid.</param>
/// <returns>True when the entry is valid.</returns>
public static bool TryParseConfiguration(int index, LuaTable table, Guid configPluginId, ILogger logger, [NotNullWhen(true)] out DataMailboxProvider? provider)
{
provider = null;
if (!table.TryGetValue("Id", out var idValue) || !idValue.TryRead<string>(out var idText) || !Guid.TryParse(idText, out var id))
{
logger.LogWarning("The configured mailbox provider {ProviderIndex} does not contain a valid ID. The ID must be a valid GUID.", index);
return false;
}
if (!table.TryGetValue("Name", out var nameValue) || !nameValue.TryRead<string>(out var name) || string.IsNullOrWhiteSpace(name))
{
logger.LogWarning("The configured mailbox provider {ProviderIndex} does not contain a valid Name field.", index);
return false;
}
if (!table.TryGetValue("Host", out var hostValue) || !hostValue.TryRead<string>(out var host) || !MailServerHosts.TryGetIdnHost(host, out _))
{
logger.LogWarning("The configured mailbox provider {ProviderIndex} does not contain a valid Host field. The host must be a host name or an IP address, without a scheme or a port.", index);
return false;
}
var transportSecurity = MailboxTransportSecurity.SSL_ON_CONNECT;
if (table.TryGetValue("TransportSecurity", out var transportSecurityValue) && (!transportSecurityValue.TryRead<string>(out var transportSecurityText) || !EnumNames.TryParse(transportSecurityText, out transportSecurity) || transportSecurity is MailboxTransportSecurity.UNKNOWN))
{
logger.LogWarning("The configured mailbox provider {ProviderIndex} does not contain a valid TransportSecurity field. Allowed values are SSL_ON_CONNECT and STARTTLS.", index);
return false;
}
var port = transportSecurity.GetUsualPort() ?? MailboxTransportSecurityExtensions.SSL_ON_CONNECT_PORT;
if (table.TryGetValue("Port", out var portValue) && (!portValue.TryRead(out port) || port is < 1 or > 65535))
{
logger.LogWarning("The configured mailbox provider {ProviderIndex} does not contain a valid Port field. The port must be a number from 1 to 65535.", index);
return false;
}
var usernameHint = string.Empty;
if (table.TryGetValue("UsernameHint", out var usernameHintValue))
{
if (!usernameHintValue.TryRead<string>(out var usernameHintText))
{
logger.LogWarning("The configured mailbox provider {ProviderIndex} does not contain a valid UsernameHint field. The hint must be a text.", index);
return false;
}
usernameHint = usernameHintText.Trim();
}
var helpUrl = string.Empty;
if (table.TryGetValue("HelpUrl", out var helpUrlValue))
{
if (!helpUrlValue.TryRead<string>(out var helpUrlText) || !Uri.TryCreate(helpUrlText.Trim(), UriKind.Absolute, out var helpUri) || (helpUri.Scheme != Uri.UriSchemeHttps && helpUri.Scheme != Uri.UriSchemeHttp))
{
logger.LogWarning("The configured mailbox provider {ProviderIndex} does not contain a valid HelpUrl field. The URL must be an absolute http or https address.", index);
return false;
}
helpUrl = helpUri.OriginalString;
}
provider = new DataMailboxProvider
{
Id = id.ToString(),
EnterpriseConfigurationPluginId = configPluginId,
Name = name.Trim(),
Host = host.Trim(),
Port = port,
TransportSecurity = transportSecurity,
UsernameHint = usernameHint,
HelpUrl = helpUrl,
};
return true;
}
}
@@ -0,0 +1,36 @@
using System.Linq.Expressions;
namespace AIStudio.Settings.DataModel;
/// <summary>
/// Settings which apply to all mailboxes, as opposed to the settings of each one.
/// </summary>
public sealed class DataMailboxes(Expression<Func<Data, DataMailboxes>>? configSelection = null)
{
/// <summary>
/// The default constructor for the JSON deserializer.
/// </summary>
public DataMailboxes() : this(null)
{
}
/// <summary>
/// The least strict outbound data restriction a mailbox may have.
/// </summary>
/// <remarks>
/// Only an organization sets it; the default leaves every level to the user. A mailbox set to a
/// less strict level gets this one when its mails reach a chat, see
/// MailToolResults.GetRequirements, and its dialog no longer offers the less strict levels.
/// </remarks>
public OutboundDataRestriction MinimumOutboundDataRestriction { get; set; } = ManagedConfiguration.Register(configSelection, n => n.MinimumOutboundDataRestriction, OutboundDataRestriction.UNRESTRICTED);
/// <summary>
/// Whether mailboxes may only be on the mail servers which configuration plugins offer.
/// </summary>
/// <remarks>
/// Only an organization sets it. AI Studio then connects to no other server, and the mail tools
/// leave a mailbox on another server out, see MailServerPolicy. Its index stays, so the mailbox
/// comes back as it was once the organization allows its server again.
/// </remarks>
public bool AllowOnlyOrganizationMailServers { get; set; } = ManagedConfiguration.Register(configSelection, n => n.AllowOnlyOrganizationMailServers, false);
}
@@ -0,0 +1,132 @@
using System.Text.Json.Serialization;
using AIStudio.Provider;
using AIStudio.Tools.Services;
namespace AIStudio.Settings.DataModel;
/// <summary>
/// An e-mail mailbox on an IMAP server, which AI Studio embeds and indexes itself.
/// </summary>
/// <remarks>
/// Mailboxes are kept in Data.Mailboxes rather than in DataSources, which is why this is no
/// IDataSource: classic RAG, Semantic Search and the agents never see a mailbox. Only the mail
/// tools read from one.
/// </remarks>
public readonly record struct DataSourceMailbox : IIndexedDataSource, ISecretId
{
public DataSourceMailbox()
{
}
/// <inheritdoc />
public uint Num { get; init; }
/// <inheritdoc />
public string Id { get; init; } = Guid.Empty.ToString();
/// <inheritdoc />
public string Name { get; init; } = string.Empty;
/// <inheritdoc />
public DataSourceType Type { get; init; } = DataSourceType.MAILBOX;
/// <inheritdoc />
public bool IsEnterpriseConfiguration { get; init; }
/// <inheritdoc />
public Guid EnterpriseConfigurationPluginId { get; init; } = Guid.Empty;
/// <inheritdoc />
public string EmbeddingId { get; init; } = Guid.Empty.ToString();
/// <inheritdoc />
public int MaxChunkTokenLength { get; init; }
/// <inheritdoc />
public int ChunkOverlapTokenLength { get; init; } = DataSourceEmbeddingService.DEFAULT_CHUNK_OVERLAP_TOKEN_LENGTH;
/// <inheritdoc />
/// <remarks>
/// There is no default, the user has to choose one of the levels which IsAllowedMailboxConfidence
/// accepts. Until then, the level is NONE, and no provider may read the mailbox.
/// </remarks>
public ConfidenceLevel ConfidenceLevel { get; init; } = ConfidenceLevel.NONE;
/// <summary>
/// The host name of the IMAP server, e.g., imap.example.org.
/// </summary>
public string Host { get; init; } = string.Empty;
/// <summary>
/// The port of the IMAP server. The default is the one for IMAP with TLS from the first byte on.
/// </summary>
public int Port { get; init; } = MailboxTransportSecurityExtensions.SSL_ON_CONNECT_PORT;
/// <summary>
/// How the connection to the IMAP server is encrypted.
/// </summary>
public MailboxTransportSecurity TransportSecurity { get; init; } = MailboxTransportSecurity.SSL_ON_CONNECT;
/// <summary>
/// How AI Studio signs in to the IMAP server.
/// </summary>
public MailboxAuthMethod AuthMethod { get; init; } = MailboxAuthMethod.PASSWORD;
/// <summary>
/// The username to sign in with, often the e-mail address.
/// </summary>
public string Username { get; init; } = string.Empty;
/// <summary>
/// The folder to which the synchronization and all mail tools are restricted, together with its subfolders.
/// </summary>
/// <remarks>
/// The full path as the server names it, including the server's own hierarchy delimiter. Empty
/// means the whole mailbox, apart from the trash and the junk folder.
/// </remarks>
public string RootFolder { get; init; } = string.Empty;
/// <summary>
/// How far back the index reaches. Flagged mails are indexed regardless of their age.
/// </summary>
public MailboxMaxAge MaxAge { get; init; } = MailboxMaxAge.LAST_12_MONTHS;
/// <summary>
/// Whether the text of attached documents is indexed as well.
/// </summary>
public bool IndexAttachments { get; init; } = true;
/// <summary>
/// The size in megabytes up to which the text of an attachment is indexed. Of a larger one, only the name is.
/// </summary>
public int MaxAttachmentSizeMegabytes { get; init; } = 10;
/// <summary>
/// Where a chat may still send data, once it has read from this mailbox.
/// </summary>
/// <remarks>
/// An organization may demand a stricter one, see DataMailboxes.MinimumOutboundDataRestriction.
/// </remarks>
public OutboundDataRestriction OutboundDataRestriction { get; init; } = OutboundDataRestriction.ONLY_CONFIGURED_SERVICES;
/// <summary>
/// The maximum number of mails one search returns. Searched page by page, it is the size of a page.
/// </summary>
public ushort MaxMatches { get; init; } = 10;
#region Implementation of ISecretId
/// <remarks>
/// The OS keyring stores the password under this ID together with the name of the mailbox, so
/// that the user recognizes the entry there. Renaming a mailbox therefore stores the password
/// anew, and deletes the old entry.
/// </remarks>
[JsonIgnore]
string ISecretId.SecretId => this.IsEnterpriseConfiguration ? $"{ISecretId.ENTERPRISE_KEY_PREFIX}::{this.Id}" : this.Id;
[JsonIgnore]
string ISecretId.SecretName => this.Name;
#endregion
}
@@ -24,4 +24,9 @@ public enum DataSourceType
/// External data source accessed via an ERI server, cf. https://github.com/MindWorkAI/ERI.
/// </summary>
ERI_V1,
/// <summary>
/// An e-mail mailbox on an IMAP server, kept in a list of its own, cf. DataSourceMailbox.
/// </summary>
MAILBOX,
}
@@ -19,6 +19,7 @@ public static class DataSourceTypeExtension
DataSourceType.LOCAL_FILE => TB("Local File"),
DataSourceType.LOCAL_DIRECTORY => TB("Local Directory"),
DataSourceType.ERI_V1 => TB("External ERI Server (v1)"),
DataSourceType.MAILBOX => TB("Mailbox"),
_ => TB("None"),
};
@@ -13,6 +13,9 @@ public sealed class DataTools(Expression<Func<Data, DataTools>>? configSelection
/// </summary>
public Dictionary<string, Dictionary<string, string>> Settings { get; set; } = [];
/// <summary>
/// The tool collections preselected per component, see DisabledToolIds for the IDs.
/// </summary>
public Dictionary<string, HashSet<string>> DefaultToolIdsByComponent { get; set; } = [];
public HashSet<string> VisibleToolSelectionComponents { get; set; } = [];
@@ -22,11 +25,27 @@ public sealed class DataTools(Expression<Func<Data, DataTools>>? configSelection
x => x.EnableTools,
true);
/// <summary>
/// The tool collections an organization switched off.
/// </summary>
/// <remarks>
/// The IDs are those of tool collections. A tool outside of a declared collection forms one of
/// its own under its own ID, which is why the names of this and the other settings still speak
/// of tools. The ID of a tool inside a declared collection stands for its whole collection, see
/// ToolRegistry.GetCollectionId.
/// </remarks>
public HashSet<string> DisabledToolIds { get; set; } = ManagedConfiguration.Register(
configSelection,
x => x.DisabledToolIds,
[]);
/// <summary>
/// The minimum provider confidence per tool collection, see DisabledToolIds for the IDs.
/// </summary>
/// <remarks>
/// When the ID of a collection and those of its tools carry different levels, the highest one
/// applies, see SettingsManager.GetMinimumProviderConfidenceResolution.
/// </remarks>
public Dictionary<string, string> MinimumProviderConfidenceByToolId { get; set; } = ManagedConfiguration.Register(
configSelection,
x => x.MinimumProviderConfidenceByToolId,
@@ -0,0 +1,23 @@
namespace AIStudio.Settings.DataModel;
/// <summary>
/// How AI Studio signs in to an IMAP server.
/// </summary>
public enum MailboxAuthMethod
{
/// <summary>
/// A method this version of AI Studio does not know, e.g., one written by a newer version. AI Studio never signs in then.
/// </summary>
/// <remarks>
/// It is the member with the underlying value 0 on purpose: when the settings file holds a value
/// TolerantEnumConverter cannot read, it falls back to that member. Falling back to a password
/// instead would sign in with a secret meant for something else, and every such attempt counts
/// as a failed sign-in, which can lock the account.
/// </remarks>
UNKNOWN = 0,
/// <summary>
/// A username and a password, or an app password where the provider asks for one.
/// </summary>
PASSWORD,
}
@@ -0,0 +1,19 @@
namespace AIStudio.Settings.DataModel;
/// <summary>
/// How far back the index of a mailbox reaches, by the date the server received a mail. Flagged mails are indexed regardless of their age.
/// </summary>
public enum MailboxMaxAge
{
// The shortest period is deliberately the member with the underlying value 0: when the settings
// file holds a value TolerantEnumConverter cannot read, it falls back to that member. A shorter
// period than the user chose removes mails from the index, which the protection against mass
// removal asks about first. A longer one would embed mails nobody asked for, which takes hours
// for a large mailbox and costs money with a cloud embedding.
LAST_3_MONTHS = 0,
LAST_6_MONTHS,
LAST_12_MONTHS,
LAST_24_MONTHS,
ALL,
}
@@ -0,0 +1,36 @@
using AIStudio.Tools.PluginSystem;
namespace AIStudio.Settings.DataModel;
public static class MailboxMaxAgeExtensions
{
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(MailboxMaxAgeExtensions).Namespace, nameof(MailboxMaxAgeExtensions));
public static string GetName(this MailboxMaxAge maxAge) => maxAge switch
{
MailboxMaxAge.LAST_3_MONTHS => TB("The last 3 months"),
MailboxMaxAge.LAST_6_MONTHS => TB("The last 6 months"),
MailboxMaxAge.LAST_12_MONTHS => TB("The last 12 months"),
MailboxMaxAge.LAST_24_MONTHS => TB("The last 24 months"),
MailboxMaxAge.ALL => TB("All mails"),
_ => TB("Unknown period"),
};
/// <summary>
/// The first day of the period, as the sync asks the server for it.
/// </summary>
/// <param name="maxAge">The period.</param>
/// <param name="now">The current time.</param>
/// <returns>The first day, or null when the period has no end.</returns>
public static DateTimeOffset? GetReceivedSince(this MailboxMaxAge maxAge, DateTimeOffset now) => maxAge switch
{
MailboxMaxAge.LAST_6_MONTHS => now.AddMonths(-6),
MailboxMaxAge.LAST_12_MONTHS => now.AddMonths(-12),
MailboxMaxAge.LAST_24_MONTHS => now.AddMonths(-24),
MailboxMaxAge.ALL => null,
// The shortest period also stands in for a value this version does not know, as it does in the settings:
_ => now.AddMonths(-3),
};
}
@@ -0,0 +1,27 @@
namespace AIStudio.Settings.DataModel;
/// <summary>
/// How the connection to an IMAP server is encrypted. There is deliberately no way without encryption.
/// </summary>
public enum MailboxTransportSecurity
{
/// <summary>
/// A way this version of AI Studio does not know, e.g., one written by a newer version. AI Studio does not connect then.
/// </summary>
/// <remarks>
/// It is the member with the underlying value 0 on purpose: when the settings file holds a value
/// TolerantEnumConverter cannot read, it falls back to that member. Falling back to a way of
/// connecting instead would mean guessing how the password travels.
/// </remarks>
UNKNOWN = 0,
/// <summary>
/// TLS from the first byte on, usually on port 993.
/// </summary>
SSL_ON_CONNECT,
/// <summary>
/// A plain connection which switches to TLS before signing in, usually on port 143. When the server does not offer the switch, AI Studio does not connect.
/// </summary>
STARTTLS,
}
@@ -0,0 +1,38 @@
using AIStudio.Tools.PluginSystem;
namespace AIStudio.Settings.DataModel;
public static class MailboxTransportSecurityExtensions
{
/// <summary>
/// The port IMAP with TLS from the start usually listens on.
/// </summary>
public const int SSL_ON_CONNECT_PORT = 993;
/// <summary>
/// The port IMAP with STARTTLS usually listens on.
/// </summary>
public const int STARTTLS_PORT = 143;
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(MailboxTransportSecurityExtensions).Namespace, nameof(MailboxTransportSecurityExtensions));
public static string GetName(this MailboxTransportSecurity transportSecurity) => transportSecurity switch
{
MailboxTransportSecurity.SSL_ON_CONNECT => TB("TLS from the start (usually port 993)"),
MailboxTransportSecurity.STARTTLS => TB("STARTTLS (usually port 143)"),
_ => TB("Unknown encryption"),
};
/// <summary>
/// The port the encryption usually comes with.
/// </summary>
/// <returns>The port, or null when there is no usual one.</returns>
public static int? GetUsualPort(this MailboxTransportSecurity transportSecurity) => transportSecurity switch
{
MailboxTransportSecurity.SSL_ON_CONNECT => SSL_ON_CONNECT_PORT,
MailboxTransportSecurity.STARTTLS => STARTTLS_PORT,
_ => null,
};
}
@@ -0,0 +1,31 @@
namespace AIStudio.Settings.DataModel;
/// <summary>
/// Where a chat may still send data, and which data source demands it.
/// </summary>
/// <remarks>
/// The data source travels along so a tool which is kept from running can name it, and the user
/// learns which mailbox stands in the way. It is its id rather than its name, because a mailbox
/// may be renamed after a chat read from it.
/// </remarks>
/// <param name="Restriction">Where the chat may still send data.</param>
/// <param name="DataSourceId">The id of the data source which demands the restriction, or an empty text when nothing restricts the chat.</param>
public sealed record OutboundDataRequirement(OutboundDataRestriction Restriction, string DataSourceId)
{
/// <summary>
/// Demands nothing: what a chat holds before it read from any mailbox, and what a result
/// demands which brought no content of one in.
/// </summary>
public static readonly OutboundDataRequirement NONE = new(OutboundDataRestriction.UNRESTRICTED, string.Empty);
/// <summary>
/// The stricter of this requirement and another one.
/// </summary>
/// <remarks>
/// On a tie, this one is kept, so the data source which set a level first stays the one which
/// is named, and a chat does not name another mailbox with every search.
/// </remarks>
/// <param name="other">The other requirement.</param>
/// <returns>The requirement whose restriction is the stricter one.</returns>
public OutboundDataRequirement StricterOf(OutboundDataRequirement other) => other.Restriction < this.Restriction ? other : this;
}
@@ -0,0 +1,32 @@
using AIStudio.Tools.PluginSystem;
namespace AIStudio.Settings.DataModel;
public static class OutboundDataRequirementExtensions
{
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(OutboundDataRequirementExtensions).Namespace, nameof(OutboundDataRequirementExtensions));
/// <summary>
/// Why a tool cannot run in a chat with this requirement, and how to get it back.
/// </summary>
/// <remarks>
/// One text for the tool selection and for a call which was turned down, so the user reads the
/// same in both places, and the model can pass it on. It names the setting as the mailbox
/// dialog labels it, so the user finds it there. The level is the one the setting had when the
/// chat read the mails: a change made later does not loosen the chat.
/// </remarks>
/// <param name="requirement">What the chat demands.</param>
/// <param name="mailboxes">The configured mailboxes, to find the name of the one which demands it.</param>
/// <returns>The text for the user.</returns>
public static string GetToolBlockedMessage(this OutboundDataRequirement requirement, IEnumerable<DataSourceMailbox> mailboxes)
{
var mailboxName = mailboxes
.Where(mailbox => string.Equals(mailbox.Id, requirement.DataSourceId, StringComparison.Ordinal))
.Select(mailbox => mailbox.Name)
.FirstOrDefault();
return string.IsNullOrWhiteSpace(mailboxName)
? string.Format(TB("This chat read mails from a mailbox which has been removed since, while its setting 'Where a chat may send data after reading mails' was '{0}'. This tool would send data beyond that, so it is not available in this chat. A new chat can use it again."), requirement.Restriction.GetName())
: string.Format(TB("This chat read mails from the mailbox '{0}' while its setting 'Where a chat may send data after reading mails' was '{1}'. This tool would send data beyond that, so it is not available in this chat. A new chat can use it again."), mailboxName, requirement.Restriction.GetName());
}
}
@@ -0,0 +1,31 @@
namespace AIStudio.Settings.DataModel;
/// <summary>
/// Where a chat may still send data, once it has read from a mailbox.
/// </summary>
/// <remarks>
/// The members run from the strictest to the most permissive, so the stricter of two levels is the
/// smaller one. Services which are configured in AI Studio, such as Confluence or the mailbox itself,
/// stay allowed on every level.
/// </remarks>
public enum OutboundDataRestriction
{
// The strictest level is deliberately the member with the underlying value 0: when the settings
// file holds a value TolerantEnumConverter cannot read, it falls back to that member. Falling back
// to a more permissive level would open a way out for mail content the user meant to keep in.
/// <summary>
/// Only the services configured in AI Studio.
/// </summary>
ONLY_CONFIGURED_SERVICES = 0,
/// <summary>
/// The configured services, and the addresses written in the chat, either by the user or in the result of a tool. No searches with third-party services.
/// </summary>
ONLY_LINKS_FROM_CHAT,
/// <summary>
/// No restriction beyond the ones of each tool.
/// </summary>
UNRESTRICTED,
}
@@ -0,0 +1,37 @@
using AIStudio.Tools.PluginSystem;
namespace AIStudio.Settings.DataModel;
public static class OutboundDataRestrictionExtensions
{
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(OutboundDataRestrictionExtensions).Namespace, nameof(OutboundDataRestrictionExtensions));
/// <summary>
/// The stricter of two restrictions.
/// </summary>
/// <param name="restriction">The one restriction.</param>
/// <param name="other">The other restriction.</param>
/// <returns>The restriction which allows less.</returns>
public static OutboundDataRestriction StricterOf(this OutboundDataRestriction restriction, OutboundDataRestriction other) => other < restriction ? other : restriction;
public static string GetName(this OutboundDataRestriction restriction) => restriction switch
{
OutboundDataRestriction.ONLY_CONFIGURED_SERVICES => TB("Only services configured in AI Studio"),
OutboundDataRestriction.ONLY_LINKS_FROM_CHAT => TB("Configured services and addresses from the chat"),
OutboundDataRestriction.UNRESTRICTED => TB("No restriction"),
_ => TB("Unknown restriction"),
};
/// <summary>
/// What a chat may still do on this level, once it has read from the mailbox.
/// </summary>
public static string GetDescription(this OutboundDataRestriction restriction) => restriction switch
{
OutboundDataRestriction.ONLY_CONFIGURED_SERVICES => TB("The chat may only use services configured in AI Studio, such as this mailbox or your Confluence. It reads no web pages, and it does not search the web."),
OutboundDataRestriction.ONLY_LINKS_FROM_CHAT => TB("The chat may also read web pages whose addresses stand in the chat, written by you or returned by a tool, exactly as they stand there. It does not search the web, and the AI cannot choose addresses of its own."),
OutboundDataRestriction.UNRESTRICTED => TB("The chat may use every tool you chose, web search and any web page included. Content of your mails can then reach third parties, e.g., inside a search query or the address of a web page."),
_ => TB("This version of AI Studio does not know this restriction, so it applies the strictest one."),
};
}
@@ -17,4 +17,5 @@ public enum PreviewFeatures
PRE_SPEECH_TO_TEXT_2026,
PRE_META_ASSISTANT_V1,
PRE_VISUAL_BRIEFING_ASSISTANT_2026,
PRE_MAILBOXES_2026,
}
@@ -17,7 +17,8 @@ public static class PreviewFeaturesExtensions
PreviewFeatures.PRE_SPEECH_TO_TEXT_2026 => TB("Transcription: Convert recordings and audio files into text"),
PreviewFeatures.PRE_META_ASSISTANT_V1 => TB("Assistant Builder: Generate and install assistant plugins"),
PreviewFeatures.PRE_VISUAL_BRIEFING_ASSISTANT_2026 => TB("Visual Briefing Assistant: Turn source material into an interactive briefing"),
PreviewFeatures.PRE_MAILBOXES_2026 => TB("Mailboxes: Let the AI search and read your e-mails, kept in a local index on your computer"),
_ => TB("Unknown preview feature")
};
@@ -23,8 +23,9 @@ public static class PreviewVisibilityExtensions
if (visibility >= PreviewVisibility.PROTOTYPE)
{
features.Add(PreviewFeatures.PRE_MAILBOXES_2026);
}
if (visibility >= PreviewVisibility.EXPERIMENTAL)
{
features.Add(PreviewFeatures.PRE_WRITER_MODE_2024);