mirror of
https://github.com/MAKS-IT-COM/maksit-postclient.git
synced 2026-09-30 03:18:09 +02:00
(feature): show message size and keep html mail and attachments reliable
This commit is contained in:
parent
8c76e2f82e
commit
4f8665ab90
19
CHANGELOG.md
19
CHANGELOG.md
@ -6,6 +6,25 @@ The format follows [Keep a Changelog](https://keepachangelog.com/) and [Semantic
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [0.3.8] - 2026-09-21
|
||||||
|
|
||||||
|
HTML mail opens without a WebView2 crash dialog. The message list shows each item's size.
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Message list **Size** column (sortable) so heavy mail is easy to find and delete. IMAP uses RFC822.SIZE, POP3 uses LIST sizes, and downloaded `.eml` files fill in the rest.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Client, Shared, UI, and Tests types live in feature folders with matching namespaces (`Mail`, `Auth`, `Archive`, and the rest).
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Opening HTML mail no longer crashes WebView2 with `NavigateToString` (large inlined images, or the engine still starting). The reading pane loads from a local file, and WebView failures fall back to text without a crash window.
|
||||||
|
- Message list paperclip shows for mail whose attachments are not `Content-Disposition: attachment` (Outlook inline files, named parts, nested `.eml`). IMAP now reads BODYSTRUCTURE, opening or indexing a message keeps the flag, and a later folder refresh no longer clears it.
|
||||||
|
- Body indexing no longer stalls on a few messages that fail or take too long: those are skipped (or dropped if they are gone from the server) so the rest of the archive can finish. Hung IMAP/POP3 downloads are aborted after 25 seconds and the socket is dropped so the next message can start. Messages larger than 8 MB wait until you open them. When every body is done, the indexing line hides like the meaning index.
|
||||||
|
- Search results keep Reply / Move / Delete on the context menu: right-click selects the mail under the pointer, and a folder refresh no longer treats hits from other folders as an empty selection. After delete, the next selection is tracked instead of a leftover or already-removed row.
|
||||||
|
|
||||||
## [0.3.7] - 2026-09-20
|
## [0.3.7] - 2026-09-20
|
||||||
|
|
||||||
Selected mail keeps its preview while the folder list refreshes.
|
Selected mail keeps its preview while the folder list refreshes.
|
||||||
|
|||||||
@ -30,6 +30,11 @@
|
|||||||
<display_length compare="ge">768</display_length>
|
<display_length compare="ge">768</display_length>
|
||||||
</requires>
|
</requires>
|
||||||
<releases>
|
<releases>
|
||||||
|
<release version="0.3.8" date="2026-09-21">
|
||||||
|
<description>
|
||||||
|
<p>HTML mail no longer crashes WebView2 on Windows. Large messages with inline images load from a local file, and WebView failures fall back to text. The message list shows each item's size so heavy mail is easy to sort and delete, and the paperclip stays on mail whose files are not marked as attachments. Search results keep the message context menu after you select mail.</p>
|
||||||
|
</description>
|
||||||
|
</release>
|
||||||
<release version="0.3.7" date="2026-09-20">
|
<release version="0.3.7" date="2026-09-20">
|
||||||
<description>
|
<description>
|
||||||
<p>Selected mail keeps its preview while the folder list refreshes. Delete and move use the row's folder, and opening a message is not stuck behind a catalog scan.</p>
|
<p>Selected mail keeps its preview while the folder list refreshes. Delete and move use the row's folder, and opening a message is not stuck behind a catalog scan.</p>
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
<LangVersion>latest</LangVersion>
|
<LangVersion>latest</LangVersion>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Version>0.3.7</Version>
|
<Version>0.3.8</Version>
|
||||||
<Product>Postclient</Product>
|
<Product>Postclient</Product>
|
||||||
<AssemblyTitle>Postclient</AssemblyTitle>
|
<AssemblyTitle>Postclient</AssemblyTitle>
|
||||||
<Authors>Maksym Sadovnychyy</Authors>
|
<Authors>Maksym Sadovnychyy</Authors>
|
||||||
|
|||||||
@ -1,8 +1,7 @@
|
|||||||
using MaksIT.Results;
|
using MaksIT.Results;
|
||||||
using MaksIT.PostClient.Shared;
|
|
||||||
|
|
||||||
|
|
||||||
namespace MaksIT.PostClient.Client;
|
namespace MaksIT.PostClient.Client.Auth;
|
||||||
|
|
||||||
|
|
||||||
public sealed class MailAuthMaterial {
|
public sealed class MailAuthMaterial {
|
||||||
@ -1,7 +1,6 @@
|
|||||||
using MaksIT.PostClient.Shared;
|
|
||||||
|
|
||||||
|
|
||||||
namespace MaksIT.PostClient.Client;
|
namespace MaksIT.PostClient.Client.Auth;
|
||||||
|
|
||||||
|
|
||||||
internal static class MailAuthHint {
|
internal static class MailAuthHint {
|
||||||
@ -3,10 +3,9 @@ using System.Text.Json;
|
|||||||
using MaksIT.IdentityHub.Client;
|
using MaksIT.IdentityHub.Client;
|
||||||
using MaksIT.IdentityHub.Contracts.Identity;
|
using MaksIT.IdentityHub.Contracts.Identity;
|
||||||
using MaksIT.Results;
|
using MaksIT.Results;
|
||||||
using MaksIT.PostClient.Shared;
|
|
||||||
|
|
||||||
|
|
||||||
namespace MaksIT.PostClient.Client;
|
namespace MaksIT.PostClient.Client.Auth;
|
||||||
|
|
||||||
|
|
||||||
public sealed class MailAuthService : IMailAuthService {
|
public sealed class MailAuthService : IMailAuthService {
|
||||||
@ -1,9 +1,8 @@
|
|||||||
using MailKit;
|
using MailKit;
|
||||||
using MailKit.Security;
|
using MailKit.Security;
|
||||||
using MaksIT.PostClient.Shared;
|
|
||||||
|
|
||||||
|
|
||||||
namespace MaksIT.PostClient.Client;
|
namespace MaksIT.PostClient.Client.Auth;
|
||||||
|
|
||||||
|
|
||||||
internal static class MailKitAuth {
|
internal static class MailKitAuth {
|
||||||
@ -4,7 +4,7 @@ using System.Text;
|
|||||||
using MaksIT.Results;
|
using MaksIT.Results;
|
||||||
|
|
||||||
|
|
||||||
namespace MaksIT.PostClient.Client;
|
namespace MaksIT.PostClient.Client.Auth;
|
||||||
|
|
||||||
|
|
||||||
internal sealed class OAuthLoopback : IDisposable {
|
internal sealed class OAuthLoopback : IDisposable {
|
||||||
@ -3,7 +3,7 @@ using System.Text;
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
|
||||||
|
|
||||||
namespace MaksIT.PostClient.Client;
|
namespace MaksIT.PostClient.Client.Auth;
|
||||||
|
|
||||||
|
|
||||||
public static class OAuthPkce {
|
public static class OAuthPkce {
|
||||||
@ -1,7 +1,6 @@
|
|||||||
using MaksIT.PostClient.Shared;
|
|
||||||
|
|
||||||
|
|
||||||
namespace MaksIT.PostClient.Client;
|
namespace MaksIT.PostClient.Client.Auth;
|
||||||
|
|
||||||
|
|
||||||
internal sealed class OAuthProviderProfile {
|
internal sealed class OAuthProviderProfile {
|
||||||
@ -1,10 +1,9 @@
|
|||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using MaksIT.Results;
|
using MaksIT.Results;
|
||||||
using MaksIT.PostClient.Shared;
|
|
||||||
|
|
||||||
|
|
||||||
namespace MaksIT.PostClient.Client;
|
namespace MaksIT.PostClient.Client.Auth;
|
||||||
|
|
||||||
|
|
||||||
internal static class OAuthTokenClient {
|
internal static class OAuthTokenClient {
|
||||||
@ -1,7 +1,6 @@
|
|||||||
using MaksIT.PostClient.Shared;
|
|
||||||
|
|
||||||
|
|
||||||
namespace MaksIT.PostClient.Client;
|
namespace MaksIT.PostClient.Client.Auth;
|
||||||
|
|
||||||
|
|
||||||
internal static class OAuthTrace {
|
internal static class OAuthTrace {
|
||||||
@ -1,9 +1,8 @@
|
|||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using MaksIT.IdentityHub.Client;
|
using MaksIT.IdentityHub.Client;
|
||||||
using MaksIT.PostClient.Shared;
|
|
||||||
|
|
||||||
|
|
||||||
namespace MaksIT.PostClient.Client;
|
namespace MaksIT.PostClient.Client.Extensions;
|
||||||
|
|
||||||
|
|
||||||
public static class ServiceCollectionExtensions {
|
public static class ServiceCollectionExtensions {
|
||||||
|
|||||||
15
src/MaksIT.PostClient.Client/GlobalUsings.cs
Normal file
15
src/MaksIT.PostClient.Client/GlobalUsings.cs
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
global using MaksIT.PostClient.Client.Auth;
|
||||||
|
global using MaksIT.PostClient.Client.Import;
|
||||||
|
global using MaksIT.PostClient.Client.Mail;
|
||||||
|
global using MaksIT.PostClient.Client.Mime;
|
||||||
|
global using MaksIT.PostClient.Client.Search;
|
||||||
|
global using MaksIT.PostClient.Client.Update;
|
||||||
|
global using MaksIT.PostClient.Shared.App;
|
||||||
|
global using MaksIT.PostClient.Shared.Archive;
|
||||||
|
global using MaksIT.PostClient.Shared.Auth;
|
||||||
|
global using MaksIT.PostClient.Shared.Config;
|
||||||
|
global using MaksIT.PostClient.Shared.Content;
|
||||||
|
global using MaksIT.PostClient.Shared.Envelope;
|
||||||
|
global using MaksIT.PostClient.Shared.Localization;
|
||||||
|
global using MaksIT.PostClient.Shared.Mail;
|
||||||
|
global using MaksIT.PostClient.Shared.Search;
|
||||||
@ -1,9 +1,8 @@
|
|||||||
using MimeKit;
|
using MimeKit;
|
||||||
using MaksIT.PostClient.Shared;
|
|
||||||
using MaksIT.Results;
|
using MaksIT.Results;
|
||||||
|
|
||||||
|
|
||||||
namespace MaksIT.PostClient.Client;
|
namespace MaksIT.PostClient.Client.Import;
|
||||||
|
|
||||||
|
|
||||||
public sealed class LocalMailImport {
|
public sealed class LocalMailImport {
|
||||||
@ -1,8 +1,7 @@
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
using MaksIT.PostClient.Shared;
|
|
||||||
|
|
||||||
|
|
||||||
namespace MaksIT.PostClient.Client;
|
namespace MaksIT.PostClient.Client.Import;
|
||||||
|
|
||||||
|
|
||||||
internal static class MapiRestriction {
|
internal static class MapiRestriction {
|
||||||
@ -1,7 +1,7 @@
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
|
|
||||||
namespace MaksIT.PostClient.Client;
|
namespace MaksIT.PostClient.Client.Import;
|
||||||
|
|
||||||
|
|
||||||
public static class MboxReader {
|
public static class MboxReader {
|
||||||
@ -1,7 +1,7 @@
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
|
|
||||||
namespace MaksIT.PostClient.Client;
|
namespace MaksIT.PostClient.Client.Import;
|
||||||
|
|
||||||
|
|
||||||
internal static class OleCompound {
|
internal static class OleCompound {
|
||||||
@ -1,7 +1,7 @@
|
|||||||
using OfficeIMO.Email.Store;
|
using OfficeIMO.Email.Store;
|
||||||
|
|
||||||
|
|
||||||
namespace MaksIT.PostClient.Client;
|
namespace MaksIT.PostClient.Client.Import;
|
||||||
|
|
||||||
|
|
||||||
public static class OutlookPst {
|
public static class OutlookPst {
|
||||||
@ -1,10 +1,9 @@
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using XstReader;
|
using XstReader;
|
||||||
using XstReader.ElementProperties;
|
using XstReader.ElementProperties;
|
||||||
using MaksIT.PostClient.Shared;
|
|
||||||
|
|
||||||
|
|
||||||
namespace MaksIT.PostClient.Client;
|
namespace MaksIT.PostClient.Client.Import;
|
||||||
|
|
||||||
|
|
||||||
public static class OutlookRules {
|
public static class OutlookRules {
|
||||||
@ -1,9 +1,8 @@
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using MaksIT.PostClient.Shared;
|
|
||||||
|
|
||||||
|
|
||||||
namespace MaksIT.PostClient.Client;
|
namespace MaksIT.PostClient.Client.Import;
|
||||||
|
|
||||||
|
|
||||||
public static class OutlookRwz {
|
public static class OutlookRwz {
|
||||||
@ -1,4 +1,4 @@
|
|||||||
namespace MaksIT.PostClient.Client;
|
namespace MaksIT.PostClient.Client.Import;
|
||||||
|
|
||||||
|
|
||||||
public static class PstFile {
|
public static class PstFile {
|
||||||
@ -1,8 +1,7 @@
|
|||||||
using XstReader;
|
using XstReader;
|
||||||
using MaksIT.PostClient.Shared;
|
|
||||||
|
|
||||||
|
|
||||||
namespace MaksIT.PostClient.Client;
|
namespace MaksIT.PostClient.Client.Import;
|
||||||
|
|
||||||
|
|
||||||
public static class PstImport {
|
public static class PstImport {
|
||||||
@ -2,7 +2,7 @@ using MimeKit;
|
|||||||
using XstReader;
|
using XstReader;
|
||||||
|
|
||||||
|
|
||||||
namespace MaksIT.PostClient.Client;
|
namespace MaksIT.PostClient.Client.Import;
|
||||||
|
|
||||||
|
|
||||||
public static class PstMime {
|
public static class PstMime {
|
||||||
@ -1,7 +1,6 @@
|
|||||||
using MaksIT.PostClient.Shared;
|
|
||||||
|
|
||||||
|
|
||||||
namespace MaksIT.PostClient.Client;
|
namespace MaksIT.PostClient.Client.Import;
|
||||||
|
|
||||||
|
|
||||||
public static class PstStoreMigrator {
|
public static class PstStoreMigrator {
|
||||||
@ -1,4 +1,4 @@
|
|||||||
namespace MaksIT.PostClient.Client;
|
namespace MaksIT.PostClient.Client.Import;
|
||||||
|
|
||||||
|
|
||||||
public static class PstSubject {
|
public static class PstSubject {
|
||||||
@ -1,4 +1,4 @@
|
|||||||
namespace MaksIT.PostClient.Client;
|
namespace MaksIT.PostClient.Client.Import;
|
||||||
|
|
||||||
|
|
||||||
public static class ThunderbirdProfiles {
|
public static class ThunderbirdProfiles {
|
||||||
@ -1,8 +1,7 @@
|
|||||||
using MaksIT.PostClient.Shared;
|
|
||||||
using MaksIT.Results;
|
using MaksIT.Results;
|
||||||
|
|
||||||
|
|
||||||
namespace MaksIT.PostClient.Client;
|
namespace MaksIT.PostClient.Client.Mail;
|
||||||
|
|
||||||
|
|
||||||
public interface IMailSession : IAsyncDisposable {
|
public interface IMailSession : IAsyncDisposable {
|
||||||
@ -23,7 +22,8 @@ public interface IMailSession : IAsyncDisposable {
|
|||||||
Task<Result<MailMessageBody>> GetMessageAsync(
|
Task<Result<MailMessageBody>> GetMessageAsync(
|
||||||
string folder,
|
string folder,
|
||||||
uint id,
|
uint id,
|
||||||
CancellationToken cancellationToken = default);
|
CancellationToken cancellationToken = default,
|
||||||
|
bool interactive = true);
|
||||||
|
|
||||||
Task<Result<MailboxQuota?>> GetQuotaAsync(CancellationToken cancellationToken = default);
|
Task<Result<MailboxQuota?>> GetQuotaAsync(CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
@ -1,7 +1,6 @@
|
|||||||
using MaksIT.PostClient.Shared;
|
|
||||||
|
|
||||||
|
|
||||||
namespace MaksIT.PostClient.Client;
|
namespace MaksIT.PostClient.Client.Mail;
|
||||||
|
|
||||||
|
|
||||||
internal static class ImapFetchPolicy {
|
internal static class ImapFetchPolicy {
|
||||||
@ -3,10 +3,9 @@ using MailKit.Net.Imap;
|
|||||||
using MailKit.Search;
|
using MailKit.Search;
|
||||||
using MimeKit;
|
using MimeKit;
|
||||||
using MaksIT.Results;
|
using MaksIT.Results;
|
||||||
using MaksIT.PostClient.Shared;
|
|
||||||
|
|
||||||
|
|
||||||
namespace MaksIT.PostClient.Client;
|
namespace MaksIT.PostClient.Client.Mail;
|
||||||
|
|
||||||
|
|
||||||
public sealed class ImapMailSession : IMailSession {
|
public sealed class ImapMailSession : IMailSession {
|
||||||
@ -26,7 +25,9 @@ public sealed class ImapMailSession : IMailSession {
|
|||||||
MessageSummaryItems.UniqueId
|
MessageSummaryItems.UniqueId
|
||||||
| MessageSummaryItems.Envelope
|
| MessageSummaryItems.Envelope
|
||||||
| MessageSummaryItems.Flags
|
| MessageSummaryItems.Flags
|
||||||
| MessageSummaryItems.InternalDate;
|
| MessageSummaryItems.InternalDate
|
||||||
|
| MessageSummaryItems.Size
|
||||||
|
| MessageSummaryItems.BodyStructure;
|
||||||
|
|
||||||
public ImapMailSession(IMailAuthService auth) {
|
public ImapMailSession(IMailAuthService auth) {
|
||||||
_auth = auth;
|
_auth = auth;
|
||||||
@ -117,7 +118,7 @@ public sealed class ImapMailSession : IMailSession {
|
|||||||
IReadOnlySet<uint>? knownIds,
|
IReadOnlySet<uint>? knownIds,
|
||||||
CancellationToken cancellationToken,
|
CancellationToken cancellationToken,
|
||||||
int itemBudget) {
|
int itemBudget) {
|
||||||
var client = RequireImap();
|
var client = await EnsureImapAsync(cancellationToken).ConfigureAwait(false);
|
||||||
if (client is null)
|
if (client is null)
|
||||||
return Result<MailFolderSync>.UnprocessableEntity(null, "Not connected.");
|
return Result<MailFolderSync>.UnprocessableEntity(null, "Not connected.");
|
||||||
if (string.IsNullOrWhiteSpace(folder))
|
if (string.IsNullOrWhiteSpace(folder))
|
||||||
@ -238,26 +239,41 @@ public sealed class ImapMailSession : IMailSession {
|
|||||||
public Task<Result<MailMessageBody>> GetMessageAsync(
|
public Task<Result<MailMessageBody>> GetMessageAsync(
|
||||||
string folder,
|
string folder,
|
||||||
uint id,
|
uint id,
|
||||||
CancellationToken cancellationToken = default) =>
|
CancellationToken cancellationToken = default,
|
||||||
_io.RunAsync(() => GetMessageCoreAsync(folder, id, cancellationToken), cancellationToken, interactive: true);
|
bool interactive = true) =>
|
||||||
|
_io.RunAsync(() => GetMessageCoreAsync(folder, id, cancellationToken, interactive), cancellationToken, interactive);
|
||||||
|
|
||||||
private async Task<Result<MailMessageBody>> GetMessageCoreAsync(
|
private async Task<Result<MailMessageBody>> GetMessageCoreAsync(
|
||||||
string folder,
|
string folder,
|
||||||
uint id,
|
uint id,
|
||||||
CancellationToken cancellationToken) {
|
CancellationToken cancellationToken,
|
||||||
var client = RequireImap();
|
bool interactive) {
|
||||||
|
var client = await EnsureImapAsync(cancellationToken).ConfigureAwait(false);
|
||||||
if (client is null)
|
if (client is null)
|
||||||
return Result<MailMessageBody>.UnprocessableEntity(null, "Not connected.");
|
return Result<MailMessageBody>.UnprocessableEntity(null, "Not connected.");
|
||||||
|
|
||||||
|
using var linked = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||||
|
using var abort = linked.Token.Register(DropSocket);
|
||||||
|
if (!interactive)
|
||||||
|
linked.CancelAfter(MailFetch.BackfillTimeout);
|
||||||
|
MimeMessage mime;
|
||||||
try {
|
try {
|
||||||
var imapFolder = await GetMailFolderAsync(client, folder, cancellationToken).ConfigureAwait(false);
|
var imapFolder = await GetMailFolderAsync(client, folder, linked.Token).ConfigureAwait(false);
|
||||||
await OpenReadOnlyAsync(imapFolder, cancellationToken).ConfigureAwait(false);
|
await OpenReadOnlyAsync(imapFolder, linked.Token).ConfigureAwait(false);
|
||||||
var mime = await imapFolder.GetMessageAsync(new UniqueId(id), cancellationToken).ConfigureAwait(false);
|
mime = await imapFolder.GetMessageAsync(new UniqueId(id), linked.Token).ConfigureAwait(false);
|
||||||
return Result<MailMessageBody>.Ok(await MimeBody.FromMimeAsync(folder, id, mime, cancellationToken).ConfigureAwait(false));
|
}
|
||||||
|
catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested) {
|
||||||
|
return Result<MailMessageBody>.UnprocessableEntity(null, "Timed out.");
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException) {
|
||||||
|
throw;
|
||||||
}
|
}
|
||||||
catch (Exception ex) {
|
catch (Exception ex) {
|
||||||
return Result<MailMessageBody>.UnprocessableEntity(null, ex.Message);
|
return Result<MailMessageBody>.UnprocessableEntity(null, ex.Message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
abort.Unregister();
|
||||||
|
return Result<MailMessageBody>.Ok(await MimeBody.FromMimeAsync(folder, id, mime, cancellationToken).ConfigureAwait(false));
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<Result<MailboxQuota?>> GetQuotaAsync(CancellationToken cancellationToken = default) =>
|
public Task<Result<MailboxQuota?>> GetQuotaAsync(CancellationToken cancellationToken = default) =>
|
||||||
@ -685,6 +701,61 @@ public sealed class ImapMailSession : IMailSession {
|
|||||||
return _imap is { IsConnected: true, IsAuthenticated: true } ? _imap : null;
|
return _imap is { IsConnected: true, IsAuthenticated: true } ? _imap : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task<ImapClient?> EnsureImapAsync(CancellationToken cancellationToken) {
|
||||||
|
var live = RequireImap();
|
||||||
|
if (live is not null)
|
||||||
|
return live;
|
||||||
|
MailboxAccount? account;
|
||||||
|
MailAuthMaterial? material;
|
||||||
|
lock (_gate) {
|
||||||
|
account = _account;
|
||||||
|
material = _material;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (account is null || material is null || string.IsNullOrWhiteSpace(account.ImapHost))
|
||||||
|
return null;
|
||||||
|
var client = new ImapClient();
|
||||||
|
try {
|
||||||
|
await client.ConnectAsync(
|
||||||
|
account.ImapHost.Trim(),
|
||||||
|
account.ImapPort,
|
||||||
|
MailSocket.Incoming(account),
|
||||||
|
cancellationToken).ConfigureAwait(false);
|
||||||
|
await MailKitAuth.AuthenticateAsync(client, account, material, cancellationToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
client.Dispose();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (_gate) {
|
||||||
|
if (_imap is { IsConnected: true, IsAuthenticated: true } current) {
|
||||||
|
client.Dispose();
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
|
||||||
|
_imap = client;
|
||||||
|
}
|
||||||
|
|
||||||
|
return client;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DropSocket() {
|
||||||
|
ImapClient? client;
|
||||||
|
lock (_gate) {
|
||||||
|
client = _imap;
|
||||||
|
_imap = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (client is null)
|
||||||
|
return;
|
||||||
|
try {
|
||||||
|
client.Dispose();
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private string? AccountProvider() {
|
private string? AccountProvider() {
|
||||||
lock (_gate)
|
lock (_gate)
|
||||||
return _account?.Provider;
|
return _account?.Provider;
|
||||||
@ -1007,7 +1078,7 @@ public sealed class ImapMailSession : IMailSession {
|
|||||||
envelope?.Date ?? summary.InternalDate ?? DateTimeOffset.MinValue,
|
envelope?.Date ?? summary.InternalDate ?? DateTimeOffset.MinValue,
|
||||||
summary.Flags?.HasFlag(MessageFlags.Seen) == true,
|
summary.Flags?.HasFlag(MessageFlags.Seen) == true,
|
||||||
summary.Flags?.HasFlag(MessageFlags.Flagged) == true,
|
summary.Flags?.HasFlag(MessageFlags.Flagged) == true,
|
||||||
summary.Attachments.Any() || names.Any(EnvelopeClassifier.IsPostacertName),
|
summary.Attachments.Any() || MimeAttachments.HasFromStructure(summary.Body),
|
||||||
info,
|
info,
|
||||||
MailPriority.Resolve(keywords, xPriority, importance, priorityHeader),
|
MailPriority.Resolve(keywords, xPriority, importance, priorityHeader),
|
||||||
envelope?.MessageId,
|
envelope?.MessageId,
|
||||||
@ -1016,7 +1087,8 @@ public sealed class ImapMailSession : IMailSession {
|
|||||||
HeaderValue(summary, "X-Riferimento-Message-ID"),
|
HeaderValue(summary, "X-Riferimento-Message-ID"),
|
||||||
envelope?.InReplyTo,
|
envelope?.InReplyTo,
|
||||||
HeaderValue(summary, "In-Reply-To")),
|
HeaderValue(summary, "In-Reply-To")),
|
||||||
HeaderValue(summary, "References")));
|
HeaderValue(summary, "References")),
|
||||||
|
summary.Size ?? 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string FirstNonEmpty(params string?[] values) {
|
private static string FirstNonEmpty(params string?[] values) {
|
||||||
@ -1071,6 +1143,11 @@ public sealed class ImapMailSession : IMailSession {
|
|||||||
return;
|
return;
|
||||||
if (part is BodyPartBasic basic && !string.IsNullOrWhiteSpace(basic.FileName))
|
if (part is BodyPartBasic basic && !string.IsNullOrWhiteSpace(basic.FileName))
|
||||||
names.Add(basic.FileName);
|
names.Add(basic.FileName);
|
||||||
|
if (part is BodyPartMessage rfc) {
|
||||||
|
CollectPartNames(rfc.Body, names);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (part is BodyPartMultipart multi) {
|
if (part is BodyPartMultipart multi) {
|
||||||
foreach (var child in multi.BodyParts)
|
foreach (var child in multi.BodyParts)
|
||||||
CollectPartNames(child, names);
|
CollectPartNames(child, names);
|
||||||
@ -1,9 +1,8 @@
|
|||||||
using MimeKit;
|
using MimeKit;
|
||||||
using MaksIT.Results;
|
using MaksIT.Results;
|
||||||
using MaksIT.PostClient.Shared;
|
|
||||||
|
|
||||||
|
|
||||||
namespace MaksIT.PostClient.Client;
|
namespace MaksIT.PostClient.Client.Mail;
|
||||||
|
|
||||||
|
|
||||||
public interface IFileMailStore {
|
public interface IFileMailStore {
|
||||||
@ -137,14 +136,15 @@ public sealed class LocalStoreSession : IMailSession, IFileMailStore {
|
|||||||
public Task<Result<MailMessageBody>> GetMessageAsync(
|
public Task<Result<MailMessageBody>> GetMessageAsync(
|
||||||
string folder,
|
string folder,
|
||||||
uint id,
|
uint id,
|
||||||
CancellationToken cancellationToken = default) =>
|
CancellationToken cancellationToken = default,
|
||||||
|
bool interactive = true) =>
|
||||||
_io.RunAsync(async () => {
|
_io.RunAsync(async () => {
|
||||||
var path = MessagePath(folder, id);
|
var path = MessagePath(folder, id);
|
||||||
var body = await MimeBody.TryFromEmlAsync(folder, id, path, cancellationToken).ConfigureAwait(false);
|
var body = await MimeBody.TryFromEmlAsync(folder, id, path, cancellationToken).ConfigureAwait(false);
|
||||||
if (body is null)
|
if (body is null)
|
||||||
return Result<MailMessageBody>.NotFound(null, "Message file is missing.");
|
return Result<MailMessageBody>.NotFound(null, "Message file is missing.");
|
||||||
return Result<MailMessageBody>.Ok(body);
|
return Result<MailMessageBody>.Ok(body);
|
||||||
}, cancellationToken, interactive: true);
|
}, cancellationToken, interactive);
|
||||||
|
|
||||||
public Task<Result<MailboxQuota?>> GetQuotaAsync(CancellationToken cancellationToken = default) {
|
public Task<Result<MailboxQuota?>> GetQuotaAsync(CancellationToken cancellationToken = default) {
|
||||||
_ = cancellationToken;
|
_ = cancellationToken;
|
||||||
@ -1,7 +1,6 @@
|
|||||||
using MaksIT.PostClient.Shared;
|
|
||||||
|
|
||||||
|
|
||||||
namespace MaksIT.PostClient.Client;
|
namespace MaksIT.PostClient.Client.Mail;
|
||||||
|
|
||||||
|
|
||||||
public static class MailArchiveMap {
|
public static class MailArchiveMap {
|
||||||
@ -21,7 +20,8 @@ public static class MailArchiveMap {
|
|||||||
EnvelopeTipo = header.EnvelopeTipo,
|
EnvelopeTipo = header.EnvelopeTipo,
|
||||||
MessageId = header.MessageId,
|
MessageId = header.MessageId,
|
||||||
InReplyTo = header.InReplyTo,
|
InReplyTo = header.InReplyTo,
|
||||||
Labels = header.Labels
|
Labels = header.Labels,
|
||||||
|
Size = header.Size
|
||||||
};
|
};
|
||||||
|
|
||||||
public static MailMessageHeader ToHeader(MailArchiveHeader row) =>
|
public static MailMessageHeader ToHeader(MailArchiveHeader row) =>
|
||||||
@ -41,7 +41,8 @@ public static class MailArchiveMap {
|
|||||||
MessageId = row.MessageId,
|
MessageId = row.MessageId,
|
||||||
InReplyTo = row.InReplyTo,
|
InReplyTo = row.InReplyTo,
|
||||||
Labels = row.Labels,
|
Labels = row.Labels,
|
||||||
DeliveryStatus = ReceiptStatus.FromTipo(row.EnvelopeTipo)
|
DeliveryStatus = ReceiptStatus.FromTipo(row.EnvelopeTipo),
|
||||||
|
Size = row.Size
|
||||||
};
|
};
|
||||||
|
|
||||||
public static string BodyText(MailMessageBody body, bool unwrap) {
|
public static string BodyText(MailMessageBody body, bool unwrap) {
|
||||||
29
src/MaksIT.PostClient.Client/Mail/MailFetch.cs
Normal file
29
src/MaksIT.PostClient.Client/Mail/MailFetch.cs
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
namespace MaksIT.PostClient.Client.Mail;
|
||||||
|
|
||||||
|
|
||||||
|
public static class MailFetch {
|
||||||
|
public static readonly TimeSpan BackfillTimeout = TimeSpan.FromSeconds(25);
|
||||||
|
|
||||||
|
public const long MaxBackfillBytes = 8L * 1024 * 1024;
|
||||||
|
public static bool IsGone(IEnumerable<string>? messages) {
|
||||||
|
if (messages is null)
|
||||||
|
return false;
|
||||||
|
foreach (var message in messages) {
|
||||||
|
if (IsGone(message))
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool IsGone(string? message) {
|
||||||
|
if (string.IsNullOrWhiteSpace(message))
|
||||||
|
return false;
|
||||||
|
return message.Contains("no such message", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| message.Contains("message not found", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| message.Contains("the unique id is invalid", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| message.Contains("uid is invalid", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| message.Contains("has been expunged", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| message.Contains("could not be found", StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,7 +1,6 @@
|
|||||||
using MaksIT.PostClient.Shared;
|
|
||||||
|
|
||||||
|
|
||||||
namespace MaksIT.PostClient.Client;
|
namespace MaksIT.PostClient.Client.Mail;
|
||||||
|
|
||||||
|
|
||||||
internal static class MailFolderCatalog {
|
internal static class MailFolderCatalog {
|
||||||
@ -1,7 +1,6 @@
|
|||||||
using MaksIT.PostClient.Shared;
|
|
||||||
|
|
||||||
|
|
||||||
namespace MaksIT.PostClient.Client;
|
namespace MaksIT.PostClient.Client.Mail;
|
||||||
|
|
||||||
|
|
||||||
public sealed class MailFolderInfo {
|
public sealed class MailFolderInfo {
|
||||||
@ -34,11 +33,11 @@ public sealed class MailMessageHeader {
|
|||||||
|
|
||||||
public bool IsFlagged { get; set; }
|
public bool IsFlagged { get; set; }
|
||||||
|
|
||||||
public bool HasAttachments { get; init; }
|
public bool HasAttachments { get; set; }
|
||||||
|
|
||||||
public string Priority { get; set; } = MailPriority.Normal;
|
public string Priority { get; set; } = MailPriority.Normal;
|
||||||
|
|
||||||
public string EnvelopeKind { get; init; } = MaksIT.PostClient.Shared.EnvelopeKind.Ordinary;
|
public string EnvelopeKind { get; init; } = MaksIT.PostClient.Shared.Envelope.EnvelopeKind.Ordinary;
|
||||||
|
|
||||||
public string EnvelopeBadge { get; init; } = "";
|
public string EnvelopeBadge { get; init; } = "";
|
||||||
|
|
||||||
@ -51,6 +50,8 @@ public sealed class MailMessageHeader {
|
|||||||
public string DeliveryStatus { get; set; } = "";
|
public string DeliveryStatus { get; set; } = "";
|
||||||
|
|
||||||
public string Labels { get; set; } = "";
|
public string Labels { get; set; } = "";
|
||||||
|
|
||||||
|
public long Size { get; init; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -1,7 +1,6 @@
|
|||||||
using MaksIT.PostClient.Shared;
|
|
||||||
|
|
||||||
|
|
||||||
namespace MaksIT.PostClient.Client;
|
namespace MaksIT.PostClient.Client.Mail;
|
||||||
|
|
||||||
|
|
||||||
public sealed class MailSessionFactory : IMailSessionFactory {
|
public sealed class MailSessionFactory : IMailSessionFactory {
|
||||||
@ -1,4 +1,4 @@
|
|||||||
namespace MaksIT.PostClient.Client;
|
namespace MaksIT.PostClient.Client.Mail;
|
||||||
|
|
||||||
|
|
||||||
internal sealed class MailSessionGate : IDisposable {
|
internal sealed class MailSessionGate : IDisposable {
|
||||||
@ -1,8 +1,7 @@
|
|||||||
using MailKit.Security;
|
using MailKit.Security;
|
||||||
using MaksIT.PostClient.Shared;
|
|
||||||
|
|
||||||
|
|
||||||
namespace MaksIT.PostClient.Client;
|
namespace MaksIT.PostClient.Client.Mail;
|
||||||
|
|
||||||
|
|
||||||
internal static class MailSocket {
|
internal static class MailSocket {
|
||||||
@ -1,10 +1,9 @@
|
|||||||
using System.IO.Pipes;
|
using System.IO.Pipes;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using MaksIT.PostClient.Shared;
|
|
||||||
|
|
||||||
|
|
||||||
namespace MaksIT.PostClient.Client;
|
namespace MaksIT.PostClient.Client.Mail;
|
||||||
|
|
||||||
|
|
||||||
public sealed class MailWorkerHost : IDisposable {
|
public sealed class MailWorkerHost : IDisposable {
|
||||||
@ -2,10 +2,9 @@ using MailKit.Net.Pop3;
|
|||||||
using MimeKit;
|
using MimeKit;
|
||||||
using MimeKit.Utils;
|
using MimeKit.Utils;
|
||||||
using MaksIT.Results;
|
using MaksIT.Results;
|
||||||
using MaksIT.PostClient.Shared;
|
|
||||||
|
|
||||||
|
|
||||||
namespace MaksIT.PostClient.Client;
|
namespace MaksIT.PostClient.Client.Mail;
|
||||||
|
|
||||||
|
|
||||||
public sealed class Pop3MailSession : IMailSession {
|
public sealed class Pop3MailSession : IMailSession {
|
||||||
@ -94,7 +93,7 @@ public sealed class Pop3MailSession : IMailSession {
|
|||||||
IReadOnlySet<uint>? knownIds,
|
IReadOnlySet<uint>? knownIds,
|
||||||
CancellationToken cancellationToken,
|
CancellationToken cancellationToken,
|
||||||
int itemBudget) {
|
int itemBudget) {
|
||||||
var client = RequirePop();
|
var client = await EnsurePopAsync(cancellationToken).ConfigureAwait(false);
|
||||||
if (client is null)
|
if (client is null)
|
||||||
return Result<MailFolderSync>.UnprocessableEntity(null, "Not connected.");
|
return Result<MailFolderSync>.UnprocessableEntity(null, "Not connected.");
|
||||||
_ = folder;
|
_ = folder;
|
||||||
@ -109,12 +108,13 @@ public sealed class Pop3MailSession : IMailSession {
|
|||||||
var count = client.Count;
|
var count = client.Count;
|
||||||
if (count == 0)
|
if (count == 0)
|
||||||
return Result<MailFolderSync>.Ok(new MailFolderSync { Present = [] });
|
return Result<MailFolderSync>.Ok(new MailFolderSync { Present = [] });
|
||||||
|
var messageSizes = await MessageSizesAsync(client, cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
if (!chunk) {
|
if (!chunk) {
|
||||||
var indexes = Enumerable.Range(0, count).ToList();
|
var indexes = Enumerable.Range(0, count).ToList();
|
||||||
var headers = await client.GetMessageHeadersAsync(indexes, cancellationToken).ConfigureAwait(false);
|
var headers = await client.GetMessageHeadersAsync(indexes, cancellationToken).ConfigureAwait(false);
|
||||||
var rows = indexes
|
var rows = indexes
|
||||||
.Select((index, i) => ToHeader(index, i < headers.Count ? headers[i] : new HeaderList()))
|
.Select((index, i) => ToHeader(index, i < headers.Count ? headers[i] : new HeaderList(), SizeAt(messageSizes, index)))
|
||||||
.OrderByDescending(h => h.Date)
|
.OrderByDescending(h => h.Date)
|
||||||
.ThenByDescending(h => h.Id)
|
.ThenByDescending(h => h.Id)
|
||||||
.ToList();
|
.ToList();
|
||||||
@ -140,7 +140,7 @@ public sealed class Pop3MailSession : IMailSession {
|
|||||||
_listedIds.Add(id);
|
_listedIds.Add(id);
|
||||||
if (known.Contains(id))
|
if (known.Contains(id))
|
||||||
continue;
|
continue;
|
||||||
added.Add(ToHeader(slice[i], i < chunkHeaders.Count ? chunkHeaders[i] : new HeaderList()));
|
added.Add(ToHeader(slice[i], i < chunkHeaders.Count ? chunkHeaders[i] : new HeaderList(), SizeAt(messageSizes, slice[i])));
|
||||||
}
|
}
|
||||||
|
|
||||||
_listOffset = range.NextOffset;
|
_listOffset = range.NextOffset;
|
||||||
@ -167,25 +167,40 @@ public sealed class Pop3MailSession : IMailSession {
|
|||||||
public Task<Result<MailMessageBody>> GetMessageAsync(
|
public Task<Result<MailMessageBody>> GetMessageAsync(
|
||||||
string folder,
|
string folder,
|
||||||
uint id,
|
uint id,
|
||||||
CancellationToken cancellationToken = default) =>
|
CancellationToken cancellationToken = default,
|
||||||
_io.RunAsync(() => GetMessageCoreAsync(folder, id, cancellationToken), cancellationToken, interactive: true);
|
bool interactive = true) =>
|
||||||
|
_io.RunAsync(() => GetMessageCoreAsync(folder, id, cancellationToken, interactive), cancellationToken, interactive);
|
||||||
|
|
||||||
private async Task<Result<MailMessageBody>> GetMessageCoreAsync(
|
private async Task<Result<MailMessageBody>> GetMessageCoreAsync(
|
||||||
string folder,
|
string folder,
|
||||||
uint id,
|
uint id,
|
||||||
CancellationToken cancellationToken) {
|
CancellationToken cancellationToken,
|
||||||
var client = RequirePop();
|
bool interactive) {
|
||||||
|
var client = await EnsurePopAsync(cancellationToken).ConfigureAwait(false);
|
||||||
if (client is null)
|
if (client is null)
|
||||||
return Result<MailMessageBody>.UnprocessableEntity(null, "Not connected.");
|
return Result<MailMessageBody>.UnprocessableEntity(null, "Not connected.");
|
||||||
|
|
||||||
|
using var linked = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||||
|
using var abort = linked.Token.Register(DropSocket);
|
||||||
|
if (!interactive)
|
||||||
|
linked.CancelAfter(MailFetch.BackfillTimeout);
|
||||||
|
MimeMessage mime;
|
||||||
try {
|
try {
|
||||||
var index = (int)id;
|
var index = (int)id;
|
||||||
var mime = await client.GetMessageAsync(index, cancellationToken).ConfigureAwait(false);
|
mime = await client.GetMessageAsync(index, linked.Token).ConfigureAwait(false);
|
||||||
return Result<MailMessageBody>.Ok(await MimeBody.FromMimeAsync(Inbox, id, mime, cancellationToken).ConfigureAwait(false));
|
}
|
||||||
|
catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested) {
|
||||||
|
return Result<MailMessageBody>.UnprocessableEntity(null, "Timed out.");
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException) {
|
||||||
|
throw;
|
||||||
}
|
}
|
||||||
catch (Exception ex) {
|
catch (Exception ex) {
|
||||||
return Result<MailMessageBody>.UnprocessableEntity(null, ex.Message);
|
return Result<MailMessageBody>.UnprocessableEntity(null, ex.Message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
abort.Unregister();
|
||||||
|
return Result<MailMessageBody>.Ok(await MimeBody.FromMimeAsync(Inbox, id, mime, cancellationToken).ConfigureAwait(false));
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<Result<MailboxQuota?>> GetQuotaAsync(CancellationToken cancellationToken = default) =>
|
public Task<Result<MailboxQuota?>> GetQuotaAsync(CancellationToken cancellationToken = default) =>
|
||||||
@ -323,6 +338,61 @@ public sealed class Pop3MailSession : IMailSession {
|
|||||||
return _pop is { IsConnected: true, IsAuthenticated: true } ? _pop : null;
|
return _pop is { IsConnected: true, IsAuthenticated: true } ? _pop : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task<Pop3Client?> EnsurePopAsync(CancellationToken cancellationToken) {
|
||||||
|
var live = RequirePop();
|
||||||
|
if (live is not null)
|
||||||
|
return live;
|
||||||
|
MailboxAccount? account;
|
||||||
|
MailAuthMaterial? material;
|
||||||
|
lock (_gate) {
|
||||||
|
account = _account;
|
||||||
|
material = _material;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (account is null || material is null || string.IsNullOrWhiteSpace(account.ImapHost))
|
||||||
|
return null;
|
||||||
|
var client = new Pop3Client();
|
||||||
|
try {
|
||||||
|
await client.ConnectAsync(
|
||||||
|
account.ImapHost.Trim(),
|
||||||
|
account.ImapPort,
|
||||||
|
MailSocket.Incoming(account),
|
||||||
|
cancellationToken).ConfigureAwait(false);
|
||||||
|
await MailKitAuth.AuthenticateAsync(client, account, material, cancellationToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
client.Dispose();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (_gate) {
|
||||||
|
if (_pop is { IsConnected: true, IsAuthenticated: true } current) {
|
||||||
|
client.Dispose();
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
|
||||||
|
_pop = client;
|
||||||
|
}
|
||||||
|
|
||||||
|
return client;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DropSocket() {
|
||||||
|
Pop3Client? client;
|
||||||
|
lock (_gate) {
|
||||||
|
client = _pop;
|
||||||
|
_pop = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (client is null)
|
||||||
|
return;
|
||||||
|
try {
|
||||||
|
client.Dispose();
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async Task DisposeClientAsync() {
|
private async Task DisposeClientAsync() {
|
||||||
Pop3Client? client;
|
Pop3Client? client;
|
||||||
lock (_gate) {
|
lock (_gate) {
|
||||||
@ -342,7 +412,7 @@ public sealed class Pop3MailSession : IMailSession {
|
|||||||
client.Dispose();
|
client.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static MailMessageHeader ToHeader(int index, HeaderList headers) {
|
private static MailMessageHeader ToHeader(int index, HeaderList headers, long size) {
|
||||||
var date = DateTimeOffset.MinValue;
|
var date = DateTimeOffset.MinValue;
|
||||||
var rawDate = headers[HeaderId.Date];
|
var rawDate = headers[HeaderId.Date];
|
||||||
if (!string.IsNullOrWhiteSpace(rawDate))
|
if (!string.IsNullOrWhiteSpace(rawDate))
|
||||||
@ -353,6 +423,7 @@ public sealed class Pop3MailSession : IMailSession {
|
|||||||
headers.Select(h => new KeyValuePair<string, string>(h.Field, h.Value)),
|
headers.Select(h => new KeyValuePair<string, string>(h.Field, h.Value)),
|
||||||
[]);
|
[]);
|
||||||
var hasAttachments = contentType.Contains("multipart/mixed", StringComparison.OrdinalIgnoreCase)
|
var hasAttachments = contentType.Contains("multipart/mixed", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| contentType.Contains("name=", StringComparison.OrdinalIgnoreCase)
|
||||||
|| disposition.Contains("attachment", StringComparison.OrdinalIgnoreCase)
|
|| disposition.Contains("attachment", StringComparison.OrdinalIgnoreCase)
|
||||||
|| info.HasInnerMessage;
|
|| info.HasInnerMessage;
|
||||||
return MimeBody.ToListHeader(
|
return MimeBody.ToListHeader(
|
||||||
@ -372,9 +443,22 @@ public sealed class Pop3MailSession : IMailSession {
|
|||||||
headers[HeaderId.MessageId],
|
headers[HeaderId.MessageId],
|
||||||
MailId.Parent(
|
MailId.Parent(
|
||||||
FirstNonEmpty(headers["X-Riferimento-Message-ID"], headers[HeaderId.InReplyTo]),
|
FirstNonEmpty(headers["X-Riferimento-Message-ID"], headers[HeaderId.InReplyTo]),
|
||||||
headers[HeaderId.References]));
|
headers[HeaderId.References]),
|
||||||
|
size);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static async Task<IList<int>?> MessageSizesAsync(Pop3Client client, CancellationToken cancellationToken) {
|
||||||
|
try {
|
||||||
|
return await client.GetMessageSizesAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static long SizeAt(IList<int>? sizes, int index) =>
|
||||||
|
sizes is not null && index >= 0 && index < sizes.Count ? sizes[index] : 0;
|
||||||
|
|
||||||
private static string FirstNonEmpty(params string?[] values) {
|
private static string FirstNonEmpty(params string?[] values) {
|
||||||
foreach (var value in values) {
|
foreach (var value in values) {
|
||||||
if (!string.IsNullOrWhiteSpace(value))
|
if (!string.IsNullOrWhiteSpace(value))
|
||||||
@ -3,10 +3,9 @@ using MimeKit;
|
|||||||
using OfficeIMO.Email;
|
using OfficeIMO.Email;
|
||||||
using OfficeIMO.Email.Store;
|
using OfficeIMO.Email.Store;
|
||||||
using MaksIT.Results;
|
using MaksIT.Results;
|
||||||
using MaksIT.PostClient.Shared;
|
|
||||||
|
|
||||||
|
|
||||||
namespace MaksIT.PostClient.Client;
|
namespace MaksIT.PostClient.Client.Mail;
|
||||||
|
|
||||||
|
|
||||||
public sealed class PstMailSession : IMailSession {
|
public sealed class PstMailSession : IMailSession {
|
||||||
@ -122,8 +121,9 @@ public sealed class PstMailSession : IMailSession {
|
|||||||
public Task<Result<MailMessageBody>> GetMessageAsync(
|
public Task<Result<MailMessageBody>> GetMessageAsync(
|
||||||
string folder,
|
string folder,
|
||||||
uint id,
|
uint id,
|
||||||
CancellationToken cancellationToken = default) =>
|
CancellationToken cancellationToken = default,
|
||||||
_io.RunAsync(() => GetMessageCoreAsync(folder, id, cancellationToken), cancellationToken, interactive: true);
|
bool interactive = true) =>
|
||||||
|
_io.RunAsync(() => GetMessageCoreAsync(folder, id, cancellationToken), cancellationToken, interactive);
|
||||||
|
|
||||||
public Task<Result<MailboxQuota?>> GetQuotaAsync(CancellationToken cancellationToken = default) {
|
public Task<Result<MailboxQuota?>> GetQuotaAsync(CancellationToken cancellationToken = default) {
|
||||||
_ = cancellationToken;
|
_ = cancellationToken;
|
||||||
@ -308,6 +308,9 @@ public sealed class PstMailSession : IMailSession {
|
|||||||
return Result<MailMessageBody>.Ok(
|
return Result<MailMessageBody>.Ok(
|
||||||
await MimeBody.FromMimeAsync(folder, id, mime, cancellationToken, seen, flagged).ConfigureAwait(false));
|
await MimeBody.FromMimeAsync(folder, id, mime, cancellationToken, seen, flagged).ConfigureAwait(false));
|
||||||
}
|
}
|
||||||
|
catch (OperationCanceledException) {
|
||||||
|
throw;
|
||||||
|
}
|
||||||
catch (Exception ex) {
|
catch (Exception ex) {
|
||||||
return Result<MailMessageBody>.UnprocessableEntity(null, ex.Message);
|
return Result<MailMessageBody>.UnprocessableEntity(null, ex.Message);
|
||||||
}
|
}
|
||||||
@ -1,10 +1,9 @@
|
|||||||
using MailKit.Net.Smtp;
|
using MailKit.Net.Smtp;
|
||||||
using MimeKit;
|
using MimeKit;
|
||||||
using MaksIT.Results;
|
using MaksIT.Results;
|
||||||
using MaksIT.PostClient.Shared;
|
|
||||||
|
|
||||||
|
|
||||||
namespace MaksIT.PostClient.Client;
|
namespace MaksIT.PostClient.Client.Mail;
|
||||||
|
|
||||||
|
|
||||||
internal static class SmtpSender {
|
internal static class SmtpSender {
|
||||||
@ -2,7 +2,7 @@ using System.Text;
|
|||||||
using UglyToad.PdfPig;
|
using UglyToad.PdfPig;
|
||||||
|
|
||||||
|
|
||||||
namespace MaksIT.PostClient.Client;
|
namespace MaksIT.PostClient.Client.Mime;
|
||||||
|
|
||||||
|
|
||||||
public static class AttachmentText {
|
public static class AttachmentText {
|
||||||
@ -2,7 +2,7 @@ using System.IO.Compression;
|
|||||||
using ICSharpCode.SharpZipLib.Zip;
|
using ICSharpCode.SharpZipLib.Zip;
|
||||||
|
|
||||||
|
|
||||||
namespace MaksIT.PostClient.Client;
|
namespace MaksIT.PostClient.Client.Mime;
|
||||||
|
|
||||||
|
|
||||||
public static class AttachmentZip {
|
public static class AttachmentZip {
|
||||||
@ -1,9 +1,8 @@
|
|||||||
using MimeKit;
|
using MimeKit;
|
||||||
using MimeKit.Cryptography;
|
using MimeKit.Cryptography;
|
||||||
using MaksIT.PostClient.Shared;
|
|
||||||
|
|
||||||
|
|
||||||
namespace MaksIT.PostClient.Client;
|
namespace MaksIT.PostClient.Client.Mime;
|
||||||
|
|
||||||
|
|
||||||
public sealed class EnvelopeParse {
|
public sealed class EnvelopeParse {
|
||||||
@ -2,7 +2,7 @@ using System.Text;
|
|||||||
using System.Xml.Linq;
|
using System.Xml.Linq;
|
||||||
|
|
||||||
|
|
||||||
namespace MaksIT.PostClient.Client;
|
namespace MaksIT.PostClient.Client.Mime;
|
||||||
|
|
||||||
|
|
||||||
public sealed class FatturaPaDocument {
|
public sealed class FatturaPaDocument {
|
||||||
@ -1,7 +1,6 @@
|
|||||||
using MaksIT.PostClient.Shared;
|
|
||||||
|
|
||||||
|
|
||||||
namespace MaksIT.PostClient.Client;
|
namespace MaksIT.PostClient.Client.Mime;
|
||||||
|
|
||||||
|
|
||||||
public static class MessagePrint {
|
public static class MessagePrint {
|
||||||
@ -1,9 +1,9 @@
|
|||||||
|
using MailKit;
|
||||||
using MimeKit;
|
using MimeKit;
|
||||||
using MimeKit.Cryptography;
|
using MimeKit.Cryptography;
|
||||||
using MaksIT.PostClient.Shared;
|
|
||||||
|
|
||||||
|
|
||||||
namespace MaksIT.PostClient.Client;
|
namespace MaksIT.PostClient.Client.Mime;
|
||||||
|
|
||||||
|
|
||||||
internal static class MimeAttachments {
|
internal static class MimeAttachments {
|
||||||
@ -17,6 +17,53 @@ internal static class MimeAttachments {
|
|||||||
public static IReadOnlyList<string> Names(IEnumerable<MailFileAttachment> files) =>
|
public static IReadOnlyList<string> Names(IEnumerable<MailFileAttachment> files) =>
|
||||||
files.Select(f => f.Name).ToList();
|
files.Select(f => f.Name).ToList();
|
||||||
|
|
||||||
|
public static bool HasVisible(IEnumerable<MailFileAttachment> files, string? html) {
|
||||||
|
foreach (var file in files) {
|
||||||
|
if (!IsInlineImage(file, html))
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool IsInlineImage(MailFileAttachment file, string? html) {
|
||||||
|
if (string.IsNullOrWhiteSpace(file.ContentId))
|
||||||
|
return false;
|
||||||
|
if (!file.ContentType.StartsWith("image/", StringComparison.OrdinalIgnoreCase))
|
||||||
|
return false;
|
||||||
|
if (string.IsNullOrWhiteSpace(html))
|
||||||
|
return false;
|
||||||
|
return html.Contains("cid:" + file.ContentId, StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool HasFromStructure(BodyPart? part) =>
|
||||||
|
HasFromStructure(part, skipEnvelope: false);
|
||||||
|
|
||||||
|
private static bool HasFromStructure(BodyPart? part, bool skipEnvelope) {
|
||||||
|
if (part is null)
|
||||||
|
return false;
|
||||||
|
if (part is BodyPartMultipart multi) {
|
||||||
|
if (multi.ContentType.IsMimeType("multipart", "signed") && multi.BodyParts.Count > 0)
|
||||||
|
return HasFromStructure(multi.BodyParts[0], skipEnvelope);
|
||||||
|
foreach (var child in multi.BodyParts) {
|
||||||
|
if (HasFromStructure(child, skipEnvelope))
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (part is BodyPartMessage rfc) {
|
||||||
|
if (!skipEnvelope)
|
||||||
|
return true;
|
||||||
|
return HasFromStructure(rfc.Body, skipEnvelope: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (part is BodyPartBasic basic)
|
||||||
|
return IsVisiblePart(basic, skipEnvelope);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
private static void Walk(MimeEntity? entity, List<MailFileAttachment> list, bool skipEnvelope) {
|
private static void Walk(MimeEntity? entity, List<MailFileAttachment> list, bool skipEnvelope) {
|
||||||
if (entity is null)
|
if (entity is null)
|
||||||
return;
|
return;
|
||||||
@ -77,12 +124,44 @@ internal static class MimeAttachments {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool IsVisiblePart(BodyPartBasic part, bool skipEnvelope) {
|
||||||
|
var name = part.FileName ?? "";
|
||||||
|
if (skipEnvelope && IsEnvelopeName(name))
|
||||||
|
return false;
|
||||||
|
if (IsBodyText(part, name))
|
||||||
|
return false;
|
||||||
|
if (IsInlineImage(part))
|
||||||
|
return false;
|
||||||
|
if (part.IsAttachment || !string.IsNullOrWhiteSpace(name))
|
||||||
|
return true;
|
||||||
|
if (!string.IsNullOrWhiteSpace(part.ContentId))
|
||||||
|
return true;
|
||||||
|
var media = part.ContentType.MediaType ?? "";
|
||||||
|
if (media.Equals("text", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| media.Equals("multipart", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| media.Equals("message", StringComparison.OrdinalIgnoreCase))
|
||||||
|
return false;
|
||||||
|
return part.Octets > 0;
|
||||||
|
}
|
||||||
|
|
||||||
private static bool IsBodyText(MimePart part, string name) {
|
private static bool IsBodyText(MimePart part, string name) {
|
||||||
if (part is not TextPart || part.IsAttachment || !string.IsNullOrWhiteSpace(name))
|
if (part is not TextPart || part.IsAttachment || !string.IsNullOrWhiteSpace(name))
|
||||||
return false;
|
return false;
|
||||||
return part.ContentType.IsMimeType("text", "plain") || part.ContentType.IsMimeType("text", "html");
|
return part.ContentType.IsMimeType("text", "plain") || part.ContentType.IsMimeType("text", "html");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool IsBodyText(BodyPartBasic part, string name) {
|
||||||
|
if (part.IsAttachment || !string.IsNullOrWhiteSpace(name))
|
||||||
|
return false;
|
||||||
|
return part.ContentType.IsMimeType("text", "plain") || part.ContentType.IsMimeType("text", "html");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsInlineImage(BodyPartBasic part) {
|
||||||
|
if (part.IsAttachment || string.IsNullOrWhiteSpace(part.ContentId))
|
||||||
|
return false;
|
||||||
|
return string.Equals(part.ContentType.MediaType, "image", StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
private static bool IsEnvelopeName(string name) =>
|
private static bool IsEnvelopeName(string name) =>
|
||||||
EnvelopeClassifier.IsDaticertName(name)
|
EnvelopeClassifier.IsDaticertName(name)
|
||||||
|| EnvelopeClassifier.IsPostacertName(name)
|
|| EnvelopeClassifier.IsPostacertName(name)
|
||||||
@ -1,8 +1,7 @@
|
|||||||
using MimeKit;
|
using MimeKit;
|
||||||
using MaksIT.PostClient.Shared;
|
|
||||||
|
|
||||||
|
|
||||||
namespace MaksIT.PostClient.Client;
|
namespace MaksIT.PostClient.Client.Mime;
|
||||||
|
|
||||||
|
|
||||||
public static class MimeBody {
|
public static class MimeBody {
|
||||||
@ -15,7 +14,8 @@ public static class MimeBody {
|
|||||||
return null;
|
return null;
|
||||||
await using var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read);
|
await using var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read);
|
||||||
var mime = await MimeMessage.LoadAsync(stream, cancellationToken).ConfigureAwait(false);
|
var mime = await MimeMessage.LoadAsync(stream, cancellationToken).ConfigureAwait(false);
|
||||||
return await FromMimeAsync(folder, id, mime, cancellationToken).ConfigureAwait(false);
|
return await FromMimeAsync(folder, id, mime, cancellationToken, size: new FileInfo(path).Length)
|
||||||
|
.ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async Task<MailMessageBody> FromMimeAsync(
|
public static async Task<MailMessageBody> FromMimeAsync(
|
||||||
@ -24,7 +24,8 @@ public static class MimeBody {
|
|||||||
MimeMessage mime,
|
MimeMessage mime,
|
||||||
CancellationToken cancellationToken,
|
CancellationToken cancellationToken,
|
||||||
bool seen = false,
|
bool seen = false,
|
||||||
bool flagged = false) {
|
bool flagged = false,
|
||||||
|
long size = 0) {
|
||||||
using var buffer = new MemoryStream();
|
using var buffer = new MemoryStream();
|
||||||
await mime.WriteToAsync(buffer, cancellationToken).ConfigureAwait(false);
|
await mime.WriteToAsync(buffer, cancellationToken).ConfigureAwait(false);
|
||||||
var parsed = EnvelopeUnwrapper.Inspect(mime);
|
var parsed = EnvelopeUnwrapper.Inspect(mime);
|
||||||
@ -36,9 +37,11 @@ public static class MimeBody {
|
|||||||
id,
|
id,
|
||||||
mime,
|
mime,
|
||||||
parsed.Info,
|
parsed.Info,
|
||||||
outerFiles.Count > 0 || innerFiles.Count > 0,
|
MimeAttachments.HasVisible(outerFiles, mime.HtmlBody)
|
||||||
|
|| MimeAttachments.HasVisible(innerFiles, inner?.HtmlBody),
|
||||||
seen,
|
seen,
|
||||||
flagged);
|
flagged,
|
||||||
|
size > 0 ? size : buffer.Length);
|
||||||
return new MailMessageBody {
|
return new MailMessageBody {
|
||||||
Header = header,
|
Header = header,
|
||||||
Text = mime.TextBody ?? "",
|
Text = mime.TextBody ?? "",
|
||||||
@ -67,7 +70,8 @@ public static class MimeBody {
|
|||||||
EnvelopeInfo info,
|
EnvelopeInfo info,
|
||||||
bool hasAttachments,
|
bool hasAttachments,
|
||||||
bool seen = false,
|
bool seen = false,
|
||||||
bool flagged = false) =>
|
bool flagged = false,
|
||||||
|
long size = 0) =>
|
||||||
new() {
|
new() {
|
||||||
Id = id,
|
Id = id,
|
||||||
Folder = folder,
|
Folder = folder,
|
||||||
@ -87,7 +91,8 @@ public static class MimeBody {
|
|||||||
EnvelopeTipo = info.Tipo,
|
EnvelopeTipo = info.Tipo,
|
||||||
MessageId = mime.MessageId ?? "",
|
MessageId = mime.MessageId ?? "",
|
||||||
InReplyTo = MailId.Parent(mime.InReplyTo, string.Join(" ", mime.References)),
|
InReplyTo = MailId.Parent(mime.InReplyTo, string.Join(" ", mime.References)),
|
||||||
DeliveryStatus = info.EventStatus
|
DeliveryStatus = info.EventStatus,
|
||||||
|
Size = size
|
||||||
};
|
};
|
||||||
|
|
||||||
public static MailMessageHeader ToListHeader(
|
public static MailMessageHeader ToListHeader(
|
||||||
@ -102,7 +107,8 @@ public static class MimeBody {
|
|||||||
EnvelopeInfo info,
|
EnvelopeInfo info,
|
||||||
string priority = MailPriority.Normal,
|
string priority = MailPriority.Normal,
|
||||||
string? messageId = null,
|
string? messageId = null,
|
||||||
string? inReplyTo = null) =>
|
string? inReplyTo = null,
|
||||||
|
long size = 0) =>
|
||||||
new() {
|
new() {
|
||||||
Id = id,
|
Id = id,
|
||||||
Folder = folder,
|
Folder = folder,
|
||||||
@ -118,6 +124,7 @@ public static class MimeBody {
|
|||||||
EnvelopeTipo = info.Tipo,
|
EnvelopeTipo = info.Tipo,
|
||||||
MessageId = messageId ?? "",
|
MessageId = messageId ?? "",
|
||||||
InReplyTo = inReplyTo ?? "",
|
InReplyTo = inReplyTo ?? "",
|
||||||
DeliveryStatus = info.EventStatus
|
DeliveryStatus = info.EventStatus,
|
||||||
|
Size = size
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -1,7 +1,7 @@
|
|||||||
using MimeKit;
|
using MimeKit;
|
||||||
|
|
||||||
|
|
||||||
namespace MaksIT.PostClient.Client;
|
namespace MaksIT.PostClient.Client.Mime;
|
||||||
|
|
||||||
|
|
||||||
public sealed class MailRecipient {
|
public sealed class MailRecipient {
|
||||||
@ -1,10 +1,9 @@
|
|||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Net.Http.Headers;
|
using System.Net.Http.Headers;
|
||||||
using MaksIT.Results;
|
using MaksIT.Results;
|
||||||
using MaksIT.PostClient.Shared;
|
|
||||||
|
|
||||||
|
|
||||||
namespace MaksIT.PostClient.Client;
|
namespace MaksIT.PostClient.Client.Search;
|
||||||
|
|
||||||
|
|
||||||
public sealed class EmbeddingModelDownloader : IDisposable {
|
public sealed class EmbeddingModelDownloader : IDisposable {
|
||||||
@ -1,9 +1,8 @@
|
|||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using System.Runtime.Versioning;
|
using System.Runtime.Versioning;
|
||||||
using MaksIT.PostClient.Shared;
|
|
||||||
|
|
||||||
|
|
||||||
namespace MaksIT.PostClient.Client;
|
namespace MaksIT.PostClient.Client.Search;
|
||||||
|
|
||||||
|
|
||||||
public readonly record struct GpuAdapter(
|
public readonly record struct GpuAdapter(
|
||||||
@ -1,7 +1,6 @@
|
|||||||
using MaksIT.PostClient.Shared;
|
|
||||||
|
|
||||||
|
|
||||||
namespace MaksIT.PostClient.Client;
|
namespace MaksIT.PostClient.Client.Search;
|
||||||
|
|
||||||
|
|
||||||
public interface ISemanticSearchService : IDisposable {
|
public interface ISemanticSearchService : IDisposable {
|
||||||
@ -1,4 +1,4 @@
|
|||||||
namespace MaksIT.PostClient.Client;
|
namespace MaksIT.PostClient.Client.Search;
|
||||||
|
|
||||||
|
|
||||||
public interface ITextEmbedder : IDisposable {
|
public interface ITextEmbedder : IDisposable {
|
||||||
@ -3,10 +3,9 @@ using System.Diagnostics.CodeAnalysis;
|
|||||||
using Microsoft.ML.Tokenizers;
|
using Microsoft.ML.Tokenizers;
|
||||||
using Microsoft.ML.OnnxRuntime;
|
using Microsoft.ML.OnnxRuntime;
|
||||||
using Microsoft.ML.OnnxRuntime.Tensors;
|
using Microsoft.ML.OnnxRuntime.Tensors;
|
||||||
using MaksIT.PostClient.Shared;
|
|
||||||
|
|
||||||
|
|
||||||
namespace MaksIT.PostClient.Client;
|
namespace MaksIT.PostClient.Client.Search;
|
||||||
|
|
||||||
|
|
||||||
public sealed class OnnxGemmaEmbedder : ITextEmbedder {
|
public sealed class OnnxGemmaEmbedder : ITextEmbedder {
|
||||||
@ -1,7 +1,6 @@
|
|||||||
using MaksIT.PostClient.Shared;
|
|
||||||
|
|
||||||
|
|
||||||
namespace MaksIT.PostClient.Client;
|
namespace MaksIT.PostClient.Client.Search;
|
||||||
|
|
||||||
|
|
||||||
public sealed class SemanticSearchService : ISemanticSearchService {
|
public sealed class SemanticSearchService : ISemanticSearchService {
|
||||||
@ -1,11 +1,10 @@
|
|||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Net.Http.Headers;
|
using System.Net.Http.Headers;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using MaksIT.PostClient.Shared;
|
|
||||||
using MaksIT.Results;
|
using MaksIT.Results;
|
||||||
|
|
||||||
|
|
||||||
namespace MaksIT.PostClient.Client;
|
namespace MaksIT.PostClient.Client.Update;
|
||||||
|
|
||||||
|
|
||||||
public sealed class AppUpdateCheck {
|
public sealed class AppUpdateCheck {
|
||||||
@ -1,4 +1,4 @@
|
|||||||
namespace MaksIT.PostClient.Shared;
|
namespace MaksIT.PostClient.Shared.App;
|
||||||
|
|
||||||
|
|
||||||
public static class AppInfo {
|
public static class AppInfo {
|
||||||
@ -1,4 +1,4 @@
|
|||||||
namespace MaksIT.PostClient.Shared;
|
namespace MaksIT.PostClient.Shared.App;
|
||||||
|
|
||||||
|
|
||||||
public enum AppInstallKind {
|
public enum AppInstallKind {
|
||||||
@ -1,4 +1,4 @@
|
|||||||
namespace MaksIT.PostClient.Shared;
|
namespace MaksIT.PostClient.Shared.App;
|
||||||
|
|
||||||
|
|
||||||
public static class AppLog {
|
public static class AppLog {
|
||||||
@ -1,7 +1,7 @@
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
|
||||||
|
|
||||||
namespace MaksIT.PostClient.Shared;
|
namespace MaksIT.PostClient.Shared.App;
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -125,6 +125,38 @@ public static class AppPaths {
|
|||||||
public static string WebViewDirectory() =>
|
public static string WebViewDirectory() =>
|
||||||
Path.Combine(DataDirectory(), "webview");
|
Path.Combine(DataDirectory(), "webview");
|
||||||
|
|
||||||
|
public static bool IsWebViewFile(Uri? uri) {
|
||||||
|
if (uri is null || !uri.IsFile)
|
||||||
|
return false;
|
||||||
|
try {
|
||||||
|
var full = Path.GetFullPath(uri.LocalPath);
|
||||||
|
var root = Path.GetFullPath(WebViewDirectory());
|
||||||
|
var prefix = root.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar)
|
||||||
|
+ Path.DirectorySeparatorChar;
|
||||||
|
return full.StartsWith(prefix, StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void ClearStaleReadingDocuments() {
|
||||||
|
try {
|
||||||
|
var dir = WebViewDirectory();
|
||||||
|
if (!Directory.Exists(dir))
|
||||||
|
return;
|
||||||
|
foreach (var file in Directory.GetFiles(dir, "reading-*.html")) {
|
||||||
|
try {
|
||||||
|
File.Delete(file);
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static string LogsDirectory() {
|
public static string LogsDirectory() {
|
||||||
if (OperatingSystem.IsMacOS()) {
|
if (OperatingSystem.IsMacOS()) {
|
||||||
return Path.Combine(
|
return Path.Combine(
|
||||||
@ -1,7 +1,7 @@
|
|||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
|
|
||||||
|
|
||||||
namespace MaksIT.PostClient.Shared;
|
namespace MaksIT.PostClient.Shared.App;
|
||||||
|
|
||||||
|
|
||||||
public static class AppVersion {
|
public static class AppVersion {
|
||||||
@ -2,7 +2,7 @@ using System.Text;
|
|||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
|
||||||
namespace MaksIT.PostClient.Shared;
|
namespace MaksIT.PostClient.Shared.App;
|
||||||
|
|
||||||
|
|
||||||
/// <summary>Formats unhandled exceptions for a copyable dialog and a crash log file.</summary>
|
/// <summary>Formats unhandled exceptions for a copyable dialog and a crash log file.</summary>
|
||||||
@ -2,7 +2,7 @@ using System.Runtime.InteropServices;
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
|
||||||
|
|
||||||
namespace MaksIT.PostClient.Shared;
|
namespace MaksIT.PostClient.Shared.App;
|
||||||
|
|
||||||
|
|
||||||
public sealed class GitHubRelease {
|
public sealed class GitHubRelease {
|
||||||
@ -2,7 +2,7 @@ using System.Security.Cryptography;
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
|
|
||||||
namespace MaksIT.PostClient.Shared;
|
namespace MaksIT.PostClient.Shared.Archive;
|
||||||
|
|
||||||
|
|
||||||
public static class ArchiveFiles {
|
public static class ArchiveFiles {
|
||||||
@ -1,7 +1,7 @@
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
|
||||||
|
|
||||||
namespace MaksIT.PostClient.Shared;
|
namespace MaksIT.PostClient.Shared.Archive;
|
||||||
|
|
||||||
|
|
||||||
public sealed class FileSentReceiptStore : ISentReceiptStore {
|
public sealed class FileSentReceiptStore : ISentReceiptStore {
|
||||||
@ -1,4 +1,4 @@
|
|||||||
namespace MaksIT.PostClient.Shared;
|
namespace MaksIT.PostClient.Shared.Archive;
|
||||||
|
|
||||||
|
|
||||||
public sealed class SentDispatch {
|
public sealed class SentDispatch {
|
||||||
@ -1,7 +1,7 @@
|
|||||||
using MaksIT.Core.Extensions;
|
using MaksIT.Core.Extensions;
|
||||||
|
|
||||||
|
|
||||||
namespace MaksIT.PostClient.Shared;
|
namespace MaksIT.PostClient.Shared.Archive;
|
||||||
|
|
||||||
|
|
||||||
public sealed class LocalStoreSidecar {
|
public sealed class LocalStoreSidecar {
|
||||||
@ -1,7 +1,7 @@
|
|||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
|
|
||||||
|
|
||||||
namespace MaksIT.PostClient.Shared;
|
namespace MaksIT.PostClient.Shared.Archive;
|
||||||
|
|
||||||
|
|
||||||
public sealed class MailArchiveCatalog : IDisposable {
|
public sealed class MailArchiveCatalog : IDisposable {
|
||||||
@ -1,4 +1,4 @@
|
|||||||
namespace MaksIT.PostClient.Shared;
|
namespace MaksIT.PostClient.Shared.Archive;
|
||||||
|
|
||||||
|
|
||||||
public sealed class MailArchiveCopy {
|
public sealed class MailArchiveCopy {
|
||||||
@ -1,4 +1,4 @@
|
|||||||
namespace MaksIT.PostClient.Shared;
|
namespace MaksIT.PostClient.Shared.Archive;
|
||||||
|
|
||||||
|
|
||||||
public static class MailArchiveLayout {
|
public static class MailArchiveLayout {
|
||||||
@ -1,7 +1,7 @@
|
|||||||
using Microsoft.Data.Sqlite;
|
using Microsoft.Data.Sqlite;
|
||||||
|
|
||||||
|
|
||||||
namespace MaksIT.PostClient.Shared;
|
namespace MaksIT.PostClient.Shared.Archive;
|
||||||
|
|
||||||
|
|
||||||
public sealed class MailArchiveStore : IDisposable {
|
public sealed class MailArchiveStore : IDisposable {
|
||||||
@ -73,10 +73,12 @@ public sealed class MailArchiveStore : IDisposable {
|
|||||||
cmd.Transaction = tx;
|
cmd.Transaction = tx;
|
||||||
cmd.CommandText = """
|
cmd.CommandText = """
|
||||||
UPDATE messages
|
UPDATE messages
|
||||||
SET eml_path = $eml, body_text = $body, attachment_text = $att
|
SET eml_path = $eml, body_text = $body, attachment_text = $att,
|
||||||
|
size = CASE WHEN $size > 0 THEN $size ELSE size END
|
||||||
WHERE id = $id;
|
WHERE id = $id;
|
||||||
""";
|
""";
|
||||||
cmd.Parameters.AddWithValue("$eml", emlPath);
|
cmd.Parameters.AddWithValue("$eml", emlPath);
|
||||||
|
cmd.Parameters.AddWithValue("$size", BodySize(header.Size, emlPath));
|
||||||
cmd.Parameters.AddWithValue("$body", bodyText);
|
cmd.Parameters.AddWithValue("$body", bodyText);
|
||||||
cmd.Parameters.AddWithValue("$att", attachmentText);
|
cmd.Parameters.AddWithValue("$att", attachmentText);
|
||||||
cmd.Parameters.AddWithValue("$id", id);
|
cmd.Parameters.AddWithValue("$id", id);
|
||||||
@ -279,7 +281,8 @@ public sealed class MailArchiveStore : IDisposable {
|
|||||||
SELECT uid, folder, subject, from_addr, date_utc, is_seen, is_flagged, has_attachments,
|
SELECT uid, folder, subject, from_addr, date_utc, is_seen, is_flagged, has_attachments,
|
||||||
priority, envelope_kind, envelope_badge, envelope_tipo, message_id, in_reply_to, eml_path,
|
priority, envelope_kind, envelope_badge, envelope_tipo, message_id, in_reply_to, eml_path,
|
||||||
(SELECT GROUP_CONCAT(name, ', ') FROM message_labels l
|
(SELECT GROUP_CONCAT(name, ', ') FROM message_labels l
|
||||||
WHERE l.mailbox_id = messages.mailbox_id AND l.folder = messages.folder AND l.uid = messages.uid)
|
WHERE l.mailbox_id = messages.mailbox_id AND l.folder = messages.folder AND l.uid = messages.uid),
|
||||||
|
size
|
||||||
FROM messages
|
FROM messages
|
||||||
WHERE mailbox_id = $m AND folder = $f
|
WHERE mailbox_id = $m AND folder = $f
|
||||||
ORDER BY date_utc DESC, uid DESC;
|
ORDER BY date_utc DESC, uid DESC;
|
||||||
@ -348,14 +351,16 @@ public sealed class MailArchiveStore : IDisposable {
|
|||||||
lock (_gate) {
|
lock (_gate) {
|
||||||
using var cmd = _db.CreateCommand();
|
using var cmd = _db.CreateCommand();
|
||||||
var sql = """
|
var sql = """
|
||||||
SELECT mailbox_id, folder, uid FROM messages
|
SELECT mailbox_id, folder, uid, size FROM messages
|
||||||
WHERE (eml_path IS NULL OR eml_path = '')
|
WHERE (eml_path IS NULL OR eml_path = '')
|
||||||
""";
|
""";
|
||||||
sql += MailboxInSql(cmd, mailboxIds);
|
sql += MailboxInSql(cmd, mailboxIds);
|
||||||
sql += " ORDER BY date_utc DESC, uid DESC";
|
sql += """
|
||||||
|
ORDER BY CASE WHEN size <= 0 THEN 1 ELSE 0 END, size ASC, date_utc DESC, uid DESC
|
||||||
|
""";
|
||||||
if (limit > 0) {
|
if (limit > 0) {
|
||||||
sql += " LIMIT $n";
|
sql += " LIMIT $n";
|
||||||
cmd.Parameters.AddWithValue("$n", Math.Clamp(limit, 1, 256));
|
cmd.Parameters.AddWithValue("$n", Math.Clamp(limit, 1, 4096));
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd.CommandText = sql;
|
cmd.CommandText = sql;
|
||||||
@ -365,7 +370,8 @@ public sealed class MailArchiveStore : IDisposable {
|
|||||||
rows.Add(new MailArchiveUid(
|
rows.Add(new MailArchiveUid(
|
||||||
reader.GetString(0),
|
reader.GetString(0),
|
||||||
reader.GetString(1),
|
reader.GetString(1),
|
||||||
(uint)reader.GetInt64(2)));
|
(uint)reader.GetInt64(2),
|
||||||
|
reader.FieldCount > 3 && !reader.IsDBNull(3) ? reader.GetInt64(3) : 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
return rows;
|
return rows;
|
||||||
@ -535,7 +541,8 @@ public sealed class MailArchiveStore : IDisposable {
|
|||||||
SELECT m.uid, m.folder, m.subject, m.from_addr, m.date_utc, m.is_seen, m.is_flagged, m.has_attachments,
|
SELECT m.uid, m.folder, m.subject, m.from_addr, m.date_utc, m.is_seen, m.is_flagged, m.has_attachments,
|
||||||
m.priority, m.envelope_kind, m.envelope_badge, m.envelope_tipo, m.message_id, m.in_reply_to, m.eml_path,
|
m.priority, m.envelope_kind, m.envelope_badge, m.envelope_tipo, m.message_id, m.in_reply_to, m.eml_path,
|
||||||
(SELECT GROUP_CONCAT(name, ', ') FROM message_labels l
|
(SELECT GROUP_CONCAT(name, ', ') FROM message_labels l
|
||||||
WHERE l.mailbox_id = m.mailbox_id AND l.folder = m.folder AND l.uid = m.uid)
|
WHERE l.mailbox_id = m.mailbox_id AND l.folder = m.folder AND l.uid = m.uid),
|
||||||
|
m.size
|
||||||
FROM messages_fts
|
FROM messages_fts
|
||||||
JOIN messages m ON m.id = messages_fts.rowid
|
JOIN messages m ON m.id = messages_fts.rowid
|
||||||
WHERE messages_fts MATCH $q AND m.mailbox_id = $m AND m.folder = $f
|
WHERE messages_fts MATCH $q AND m.mailbox_id = $m AND m.folder = $f
|
||||||
@ -543,7 +550,8 @@ public sealed class MailArchiveStore : IDisposable {
|
|||||||
SELECT m.uid, m.folder, m.subject, m.from_addr, m.date_utc, m.is_seen, m.is_flagged, m.has_attachments,
|
SELECT m.uid, m.folder, m.subject, m.from_addr, m.date_utc, m.is_seen, m.is_flagged, m.has_attachments,
|
||||||
m.priority, m.envelope_kind, m.envelope_badge, m.envelope_tipo, m.message_id, m.in_reply_to, m.eml_path,
|
m.priority, m.envelope_kind, m.envelope_badge, m.envelope_tipo, m.message_id, m.in_reply_to, m.eml_path,
|
||||||
(SELECT GROUP_CONCAT(name, ', ') FROM message_labels l
|
(SELECT GROUP_CONCAT(name, ', ') FROM message_labels l
|
||||||
WHERE l.mailbox_id = m.mailbox_id AND l.folder = m.folder AND l.uid = m.uid)
|
WHERE l.mailbox_id = m.mailbox_id AND l.folder = m.folder AND l.uid = m.uid),
|
||||||
|
m.size
|
||||||
FROM messages m
|
FROM messages m
|
||||||
WHERE m.mailbox_id = $m AND m.folder = $f
|
WHERE m.mailbox_id = $m AND m.folder = $f
|
||||||
AND EXISTS (
|
AND EXISTS (
|
||||||
@ -666,6 +674,7 @@ public sealed class MailArchiveStore : IDisposable {
|
|||||||
m.priority, m.envelope_kind, m.envelope_badge, m.envelope_tipo, m.message_id, m.in_reply_to, m.eml_path,
|
m.priority, m.envelope_kind, m.envelope_badge, m.envelope_tipo, m.message_id, m.in_reply_to, m.eml_path,
|
||||||
(SELECT GROUP_CONCAT(name, ', ') FROM message_labels l
|
(SELECT GROUP_CONCAT(name, ', ') FROM message_labels l
|
||||||
WHERE l.mailbox_id = m.mailbox_id AND l.folder = m.folder AND l.uid = m.uid),
|
WHERE l.mailbox_id = m.mailbox_id AND l.folder = m.folder AND l.uid = m.uid),
|
||||||
|
m.size,
|
||||||
m.body_text, m.attachment_text, e.model_id, e.vector
|
m.body_text, m.attachment_text, e.model_id, e.vector
|
||||||
FROM messages m
|
FROM messages m
|
||||||
LEFT JOIN message_embeddings e ON e.message_id = m.id
|
LEFT JOIN message_embeddings e ON e.message_id = m.id
|
||||||
@ -678,12 +687,12 @@ public sealed class MailArchiveStore : IDisposable {
|
|||||||
if (!reader.Read())
|
if (!reader.Read())
|
||||||
return null;
|
return null;
|
||||||
var header = ReadHeader(reader);
|
var header = ReadHeader(reader);
|
||||||
var body = reader.IsDBNull(16) ? "" : reader.GetString(16);
|
var body = reader.IsDBNull(17) ? "" : reader.GetString(17);
|
||||||
var att = reader.IsDBNull(17) ? "" : reader.GetString(17);
|
var att = reader.IsDBNull(18) ? "" : reader.GetString(18);
|
||||||
var model = reader.IsDBNull(18) ? "" : reader.GetString(18);
|
var model = reader.IsDBNull(19) ? "" : reader.GetString(19);
|
||||||
float[]? vector = null;
|
float[]? vector = null;
|
||||||
if (!reader.IsDBNull(19))
|
if (!reader.IsDBNull(20))
|
||||||
vector = EmbeddingVector.FromBlob(reader.GetFieldValue<byte[]>(19));
|
vector = EmbeddingVector.FromBlob(reader.GetFieldValue<byte[]>(20));
|
||||||
var labels = string.IsNullOrWhiteSpace(header.Labels)
|
var labels = string.IsNullOrWhiteSpace(header.Labels)
|
||||||
? Array.Empty<string>()
|
? Array.Empty<string>()
|
||||||
: header.Labels.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);
|
: header.Labels.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);
|
||||||
@ -721,7 +730,8 @@ public sealed class MailArchiveStore : IDisposable {
|
|||||||
MessageId = copy.Header.MessageId,
|
MessageId = copy.Header.MessageId,
|
||||||
InReplyTo = copy.Header.InReplyTo,
|
InReplyTo = copy.Header.InReplyTo,
|
||||||
EmlPath = copy.Header.EmlPath,
|
EmlPath = copy.Header.EmlPath,
|
||||||
Labels = copy.Header.Labels
|
Labels = copy.Header.Labels,
|
||||||
|
Size = copy.Header.Size
|
||||||
};
|
};
|
||||||
var id = UpsertHeader(
|
var id = UpsertHeader(
|
||||||
tx,
|
tx,
|
||||||
@ -733,10 +743,12 @@ public sealed class MailArchiveStore : IDisposable {
|
|||||||
body.Transaction = tx;
|
body.Transaction = tx;
|
||||||
body.CommandText = """
|
body.CommandText = """
|
||||||
UPDATE messages
|
UPDATE messages
|
||||||
SET eml_path = $eml, body_text = $body, attachment_text = $att
|
SET eml_path = $eml, body_text = $body, attachment_text = $att,
|
||||||
|
size = CASE WHEN $size > 0 THEN $size ELSE size END
|
||||||
WHERE id = $id;
|
WHERE id = $id;
|
||||||
""";
|
""";
|
||||||
body.Parameters.AddWithValue("$eml", header.EmlPath ?? "");
|
body.Parameters.AddWithValue("$eml", header.EmlPath ?? "");
|
||||||
|
body.Parameters.AddWithValue("$size", BodySize(header.Size, header.EmlPath));
|
||||||
body.Parameters.AddWithValue("$body", copy.BodyText ?? "");
|
body.Parameters.AddWithValue("$body", copy.BodyText ?? "");
|
||||||
body.Parameters.AddWithValue("$att", copy.AttachmentText ?? "");
|
body.Parameters.AddWithValue("$att", copy.AttachmentText ?? "");
|
||||||
body.Parameters.AddWithValue("$id", id);
|
body.Parameters.AddWithValue("$id", id);
|
||||||
@ -878,7 +890,8 @@ public sealed class MailArchiveStore : IDisposable {
|
|||||||
SELECT uid, folder, subject, from_addr, date_utc, is_seen, is_flagged, has_attachments,
|
SELECT uid, folder, subject, from_addr, date_utc, is_seen, is_flagged, has_attachments,
|
||||||
priority, envelope_kind, envelope_badge, envelope_tipo, message_id, in_reply_to, eml_path,
|
priority, envelope_kind, envelope_badge, envelope_tipo, message_id, in_reply_to, eml_path,
|
||||||
(SELECT GROUP_CONCAT(name, ', ') FROM message_labels l
|
(SELECT GROUP_CONCAT(name, ', ') FROM message_labels l
|
||||||
WHERE l.mailbox_id = messages.mailbox_id AND l.folder = messages.folder AND l.uid = messages.uid)
|
WHERE l.mailbox_id = messages.mailbox_id AND l.folder = messages.folder AND l.uid = messages.uid),
|
||||||
|
size
|
||||||
FROM messages
|
FROM messages
|
||||||
WHERE mailbox_id = $m AND folder = $f
|
WHERE mailbox_id = $m AND folder = $f
|
||||||
AND (subject LIKE $q OR from_addr LIKE $q OR body_text LIKE $q OR attachment_text LIKE $q
|
AND (subject LIKE $q OR from_addr LIKE $q OR body_text LIKE $q OR attachment_text LIKE $q
|
||||||
@ -911,8 +924,8 @@ public sealed class MailArchiveStore : IDisposable {
|
|||||||
cmd.CommandText = $"""
|
cmd.CommandText = $"""
|
||||||
INSERT INTO messages (
|
INSERT INTO messages (
|
||||||
mailbox_id, folder, uid, message_id, subject, from_addr, date_utc, envelope_kind, envelope_badge,
|
mailbox_id, folder, uid, message_id, subject, from_addr, date_utc, envelope_kind, envelope_badge,
|
||||||
envelope_tipo, in_reply_to, is_seen, is_flagged, has_attachments, priority, trashed_utc)
|
envelope_tipo, in_reply_to, is_seen, is_flagged, has_attachments, priority, trashed_utc, size)
|
||||||
VALUES ($m, $f, $u, $mid, $sub, $from, $date, $kind, $badge, $tipo, $reply, $seen, $flag, $att, $pri, $trashed)
|
VALUES ($m, $f, $u, $mid, $sub, $from, $date, $kind, $badge, $tipo, $reply, $seen, $flag, $att, $pri, $trashed, $size)
|
||||||
ON CONFLICT(mailbox_id, folder, uid) DO UPDATE SET
|
ON CONFLICT(mailbox_id, folder, uid) DO UPDATE SET
|
||||||
message_id = excluded.message_id,
|
message_id = excluded.message_id,
|
||||||
subject = excluded.subject,
|
subject = excluded.subject,
|
||||||
@ -923,8 +936,9 @@ public sealed class MailArchiveStore : IDisposable {
|
|||||||
envelope_tipo = excluded.envelope_tipo,
|
envelope_tipo = excluded.envelope_tipo,
|
||||||
in_reply_to = excluded.in_reply_to,
|
in_reply_to = excluded.in_reply_to,
|
||||||
{flagAssign}
|
{flagAssign}
|
||||||
has_attachments = excluded.has_attachments,
|
has_attachments = CASE WHEN excluded.has_attachments != 0 THEN 1 ELSE messages.has_attachments END,
|
||||||
priority = excluded.priority;
|
priority = excluded.priority,
|
||||||
|
size = CASE WHEN excluded.size > 0 THEN excluded.size ELSE messages.size END;
|
||||||
""";
|
""";
|
||||||
var trash = MailRetention.IsTrash(header.Folder);
|
var trash = MailRetention.IsTrash(header.Folder);
|
||||||
var stampTrash = trash && (stampTrashTime || HasMessageElsewhere(tx, mailboxId, header.Folder, header.MessageId));
|
var stampTrash = trash && (stampTrashTime || HasMessageElsewhere(tx, mailboxId, header.Folder, header.MessageId));
|
||||||
@ -945,6 +959,7 @@ public sealed class MailArchiveStore : IDisposable {
|
|||||||
cmd.Parameters.AddWithValue("$att", header.HasAttachments ? 1 : 0);
|
cmd.Parameters.AddWithValue("$att", header.HasAttachments ? 1 : 0);
|
||||||
cmd.Parameters.AddWithValue("$pri", header.Priority);
|
cmd.Parameters.AddWithValue("$pri", header.Priority);
|
||||||
cmd.Parameters.AddWithValue("$trashed", trashed);
|
cmd.Parameters.AddWithValue("$trashed", trashed);
|
||||||
|
cmd.Parameters.AddWithValue("$size", header.Size);
|
||||||
cmd.ExecuteNonQuery();
|
cmd.ExecuteNonQuery();
|
||||||
using var idCmd = _db.CreateCommand();
|
using var idCmd = _db.CreateCommand();
|
||||||
idCmd.Transaction = tx;
|
idCmd.Transaction = tx;
|
||||||
@ -1011,6 +1026,8 @@ public sealed class MailArchiveStore : IDisposable {
|
|||||||
|
|
||||||
private static MailArchiveHeader ReadHeader(SqliteDataReader reader) {
|
private static MailArchiveHeader ReadHeader(SqliteDataReader reader) {
|
||||||
DateTimeOffset.TryParse(reader.GetString(4), out var date);
|
DateTimeOffset.TryParse(reader.GetString(4), out var date);
|
||||||
|
var emlPath = reader.IsDBNull(14) ? "" : reader.GetString(14);
|
||||||
|
var stored = reader.FieldCount > 16 && !reader.IsDBNull(16) ? reader.GetInt64(16) : 0;
|
||||||
return new MailArchiveHeader {
|
return new MailArchiveHeader {
|
||||||
Uid = (uint)reader.GetInt64(0),
|
Uid = (uint)reader.GetInt64(0),
|
||||||
Folder = reader.GetString(1),
|
Folder = reader.GetString(1),
|
||||||
@ -1026,11 +1043,18 @@ public sealed class MailArchiveStore : IDisposable {
|
|||||||
EnvelopeTipo = reader.GetString(11),
|
EnvelopeTipo = reader.GetString(11),
|
||||||
MessageId = reader.GetString(12),
|
MessageId = reader.GetString(12),
|
||||||
InReplyTo = reader.GetString(13),
|
InReplyTo = reader.GetString(13),
|
||||||
EmlPath = reader.IsDBNull(14) ? "" : reader.GetString(14),
|
EmlPath = emlPath,
|
||||||
Labels = reader.FieldCount > 15 && !reader.IsDBNull(15) ? reader.GetString(15) : ""
|
Labels = reader.FieldCount > 15 && !reader.IsDBNull(15) ? reader.GetString(15) : "",
|
||||||
|
Size = stored
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static long BodySize(long size, string? emlPath) =>
|
||||||
|
size > 0 ? size : FileSize(emlPath);
|
||||||
|
|
||||||
|
private static long FileSize(string? path) =>
|
||||||
|
!string.IsNullOrWhiteSpace(path) && File.Exists(path) ? new FileInfo(path).Length : 0;
|
||||||
|
|
||||||
internal static string FtsQuery(string text) {
|
internal static string FtsQuery(string text) {
|
||||||
var parts = text.Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
var parts = text.Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||||
if (parts.Length == 0)
|
if (parts.Length == 0)
|
||||||
@ -1067,6 +1091,7 @@ public sealed class MailArchiveStore : IDisposable {
|
|||||||
m.priority, m.envelope_kind, m.envelope_badge, m.envelope_tipo, m.message_id, m.in_reply_to, m.eml_path,
|
m.priority, m.envelope_kind, m.envelope_badge, m.envelope_tipo, m.message_id, m.in_reply_to, m.eml_path,
|
||||||
(SELECT GROUP_CONCAT(name, ', ') FROM message_labels l
|
(SELECT GROUP_CONCAT(name, ', ') FROM message_labels l
|
||||||
WHERE l.mailbox_id = m.mailbox_id AND l.folder = m.folder AND l.uid = m.uid),
|
WHERE l.mailbox_id = m.mailbox_id AND l.folder = m.folder AND l.uid = m.uid),
|
||||||
|
m.size,
|
||||||
e.vector
|
e.vector
|
||||||
FROM message_embeddings e
|
FROM message_embeddings e
|
||||||
JOIN messages m ON m.id = e.message_id
|
JOIN messages m ON m.id = e.message_id
|
||||||
@ -1078,7 +1103,7 @@ public sealed class MailArchiveStore : IDisposable {
|
|||||||
extra = [];
|
extra = [];
|
||||||
using var reader = cmd.ExecuteReader();
|
using var reader = cmd.ExecuteReader();
|
||||||
while (reader.Read()) {
|
while (reader.Read()) {
|
||||||
var blob = reader.IsDBNull(16) ? null : reader.GetFieldValue<byte[]>(16);
|
var blob = reader.IsDBNull(17) ? null : reader.GetFieldValue<byte[]>(17);
|
||||||
if (blob is null || blob.Length == 0)
|
if (blob is null || blob.Length == 0)
|
||||||
continue;
|
continue;
|
||||||
var vector = EmbeddingVector.FromBlob(blob);
|
var vector = EmbeddingVector.FromBlob(blob);
|
||||||
@ -1221,6 +1246,7 @@ public sealed class MailArchiveStore : IDisposable {
|
|||||||
body_text TEXT NOT NULL DEFAULT '',
|
body_text TEXT NOT NULL DEFAULT '',
|
||||||
attachment_text TEXT NOT NULL DEFAULT '',
|
attachment_text TEXT NOT NULL DEFAULT '',
|
||||||
trashed_utc TEXT NOT NULL DEFAULT '',
|
trashed_utc TEXT NOT NULL DEFAULT '',
|
||||||
|
size INTEGER NOT NULL DEFAULT 0,
|
||||||
UNIQUE(mailbox_id, folder, uid)
|
UNIQUE(mailbox_id, folder, uid)
|
||||||
);
|
);
|
||||||
CREATE VIRTUAL TABLE IF NOT EXISTS messages_fts USING fts5(
|
CREATE VIRTUAL TABLE IF NOT EXISTS messages_fts USING fts5(
|
||||||
@ -1242,6 +1268,7 @@ public sealed class MailArchiveStore : IDisposable {
|
|||||||
""";
|
""";
|
||||||
cmd.ExecuteNonQuery();
|
cmd.ExecuteNonQuery();
|
||||||
EnsureColumn("messages", "trashed_utc", "TEXT NOT NULL DEFAULT ''");
|
EnsureColumn("messages", "trashed_utc", "TEXT NOT NULL DEFAULT ''");
|
||||||
|
EnsureColumn("messages", "size", "INTEGER NOT NULL DEFAULT 0");
|
||||||
}
|
}
|
||||||
|
|
||||||
private void EnsureColumn(string table, string column, string definition) {
|
private void EnsureColumn(string table, string column, string definition) {
|
||||||
@ -1261,7 +1288,7 @@ public sealed class MailArchiveStore : IDisposable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public readonly record struct MailArchiveUid(string MailboxId, string Folder, uint Uid);
|
public readonly record struct MailArchiveUid(string MailboxId, string Folder, uint Uid, long Size = 0);
|
||||||
|
|
||||||
|
|
||||||
public readonly record struct EmbeddingWorkItem(
|
public readonly record struct EmbeddingWorkItem(
|
||||||
@ -1315,4 +1342,6 @@ public sealed class MailArchiveHeader {
|
|||||||
public string EmlPath { get; init; } = "";
|
public string EmlPath { get; init; } = "";
|
||||||
|
|
||||||
public string Labels { get; init; } = "";
|
public string Labels { get; init; } = "";
|
||||||
|
|
||||||
|
public long Size { get; init; }
|
||||||
}
|
}
|
||||||
@ -4,7 +4,7 @@ using System.Text.Json;
|
|||||||
using MaksIT.Results;
|
using MaksIT.Results;
|
||||||
|
|
||||||
|
|
||||||
namespace MaksIT.PostClient.Shared;
|
namespace MaksIT.PostClient.Shared.Auth;
|
||||||
|
|
||||||
|
|
||||||
public sealed class FileSecretStore : ISecretStore {
|
public sealed class FileSecretStore : ISecretStore {
|
||||||
@ -1,7 +1,7 @@
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
|
||||||
|
|
||||||
namespace MaksIT.PostClient.Shared;
|
namespace MaksIT.PostClient.Shared.Auth;
|
||||||
|
|
||||||
|
|
||||||
public static class HubDesktopSession {
|
public static class HubDesktopSession {
|
||||||
@ -1,7 +1,7 @@
|
|||||||
using MaksIT.Results;
|
using MaksIT.Results;
|
||||||
|
|
||||||
|
|
||||||
namespace MaksIT.PostClient.Shared;
|
namespace MaksIT.PostClient.Shared.Auth;
|
||||||
|
|
||||||
|
|
||||||
public interface ISecretStore {
|
public interface ISecretStore {
|
||||||
@ -1,4 +1,4 @@
|
|||||||
namespace MaksIT.PostClient.Shared;
|
namespace MaksIT.PostClient.Shared.Auth;
|
||||||
|
|
||||||
|
|
||||||
public static class IdentityHubAddress {
|
public static class IdentityHubAddress {
|
||||||
@ -1,4 +1,4 @@
|
|||||||
namespace MaksIT.PostClient.Shared;
|
namespace MaksIT.PostClient.Shared.Auth;
|
||||||
|
|
||||||
|
|
||||||
public static class MailAuthKind {
|
public static class MailAuthKind {
|
||||||
@ -1,7 +1,7 @@
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
|
||||||
|
|
||||||
namespace MaksIT.PostClient.Shared;
|
namespace MaksIT.PostClient.Shared.Auth;
|
||||||
|
|
||||||
|
|
||||||
public static class OAuthClientId {
|
public static class OAuthClientId {
|
||||||
@ -1,7 +1,7 @@
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
|
||||||
|
|
||||||
namespace MaksIT.PostClient.Shared;
|
namespace MaksIT.PostClient.Shared.Auth;
|
||||||
|
|
||||||
|
|
||||||
public static class OAuthClientSecret {
|
public static class OAuthClientSecret {
|
||||||
@ -1,4 +1,4 @@
|
|||||||
namespace MaksIT.PostClient.Shared;
|
namespace MaksIT.PostClient.Shared.Auth;
|
||||||
|
|
||||||
|
|
||||||
public static class OAuthLoginHint {
|
public static class OAuthLoginHint {
|
||||||
@ -1,4 +1,4 @@
|
|||||||
namespace MaksIT.PostClient.Shared;
|
namespace MaksIT.PostClient.Shared.Auth;
|
||||||
|
|
||||||
|
|
||||||
public static class OAuthMailScope {
|
public static class OAuthMailScope {
|
||||||
@ -1,4 +1,4 @@
|
|||||||
namespace MaksIT.PostClient.Shared;
|
namespace MaksIT.PostClient.Shared.Auth;
|
||||||
|
|
||||||
|
|
||||||
public sealed class OAuthTokenSet {
|
public sealed class OAuthTokenSet {
|
||||||
@ -1,4 +1,4 @@
|
|||||||
namespace MaksIT.PostClient.Shared;
|
namespace MaksIT.PostClient.Shared.Config;
|
||||||
|
|
||||||
|
|
||||||
public sealed class Configuration {
|
public sealed class Configuration {
|
||||||
@ -2,7 +2,7 @@ using System.Text.Json;
|
|||||||
using System.Text.Json.Nodes;
|
using System.Text.Json.Nodes;
|
||||||
|
|
||||||
|
|
||||||
namespace MaksIT.PostClient.Shared;
|
namespace MaksIT.PostClient.Shared.Config;
|
||||||
|
|
||||||
|
|
||||||
public sealed class ConfigurationFileService {
|
public sealed class ConfigurationFileService {
|
||||||
@ -1,4 +1,4 @@
|
|||||||
namespace MaksIT.PostClient.Shared;
|
namespace MaksIT.PostClient.Shared.Config;
|
||||||
|
|
||||||
|
|
||||||
public static class AppRegion {
|
public static class AppRegion {
|
||||||
@ -1,4 +1,4 @@
|
|||||||
namespace MaksIT.PostClient.Shared;
|
namespace MaksIT.PostClient.Shared.Config;
|
||||||
|
|
||||||
|
|
||||||
public sealed class FeatureSettings {
|
public sealed class FeatureSettings {
|
||||||
@ -1,8 +1,9 @@
|
|||||||
using System.Net;
|
using System.Net;
|
||||||
|
using System.Text;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
|
|
||||||
namespace MaksIT.PostClient.Shared;
|
namespace MaksIT.PostClient.Shared.Content;
|
||||||
|
|
||||||
|
|
||||||
public static class MessageHtml {
|
public static class MessageHtml {
|
||||||
@ -22,13 +23,14 @@ public static class MessageHtml {
|
|||||||
|
|
||||||
public static string Document(string? html) {
|
public static string Document(string? html) {
|
||||||
var body = (html ?? "").Trim();
|
var body = (html ?? "").Trim();
|
||||||
|
string document;
|
||||||
if (LooksLikeDocument(body)) {
|
if (LooksLikeDocument(body)) {
|
||||||
if (Head.IsMatch(body))
|
document = Head.IsMatch(body)
|
||||||
return Head.Replace(body, m => m.Value + Csp + Style, 1);
|
? Head.Replace(body, m => m.Value + Csp + Style, 1)
|
||||||
return Csp + Style + body;
|
: Csp + Style + body;
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
return """
|
document = """
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html><head>
|
<html><head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
@ -38,6 +40,16 @@ public static class MessageHtml {
|
|||||||
""" + body + "</body></html>";
|
""" + body + "</body></html>";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return Sanitize(document);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string Sanitize(string? html) {
|
||||||
|
if (string.IsNullOrEmpty(html))
|
||||||
|
return html ?? "";
|
||||||
|
var cleaned = html.Replace("\0", "", StringComparison.Ordinal);
|
||||||
|
return Encoding.UTF8.GetString(Encoding.UTF8.GetBytes(cleaned));
|
||||||
|
}
|
||||||
|
|
||||||
public static bool LooksLikeDocument(string? html) {
|
public static bool LooksLikeDocument(string? html) {
|
||||||
var value = (html ?? "").TrimStart();
|
var value = (html ?? "").TrimStart();
|
||||||
return value.StartsWith("<!DOCTYPE", StringComparison.OrdinalIgnoreCase)
|
return value.StartsWith("<!DOCTYPE", StringComparison.OrdinalIgnoreCase)
|
||||||
@ -2,7 +2,7 @@ using System.Net;
|
|||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
|
|
||||||
namespace MaksIT.PostClient.Shared;
|
namespace MaksIT.PostClient.Shared.Content;
|
||||||
|
|
||||||
|
|
||||||
public static class MessageText {
|
public static class MessageText {
|
||||||
@ -1,7 +1,7 @@
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
|
|
||||||
namespace MaksIT.PostClient.Shared;
|
namespace MaksIT.PostClient.Shared.Content;
|
||||||
|
|
||||||
|
|
||||||
public static class SimplePdf {
|
public static class SimplePdf {
|
||||||
@ -1,7 +1,7 @@
|
|||||||
using System.Xml;
|
using System.Xml;
|
||||||
|
|
||||||
|
|
||||||
namespace MaksIT.PostClient.Shared;
|
namespace MaksIT.PostClient.Shared.Envelope;
|
||||||
|
|
||||||
|
|
||||||
public static class DaticertParser {
|
public static class DaticertParser {
|
||||||
@ -1,4 +1,4 @@
|
|||||||
namespace MaksIT.PostClient.Shared;
|
namespace MaksIT.PostClient.Shared.Envelope;
|
||||||
|
|
||||||
|
|
||||||
public static class EnvelopeClassifier {
|
public static class EnvelopeClassifier {
|
||||||
@ -1,4 +1,4 @@
|
|||||||
namespace MaksIT.PostClient.Shared;
|
namespace MaksIT.PostClient.Shared.Envelope;
|
||||||
|
|
||||||
|
|
||||||
public sealed class EnvelopeInfo {
|
public sealed class EnvelopeInfo {
|
||||||
@ -1,4 +1,4 @@
|
|||||||
namespace MaksIT.PostClient.Shared;
|
namespace MaksIT.PostClient.Shared.Envelope;
|
||||||
|
|
||||||
|
|
||||||
public static class EnvelopeKind {
|
public static class EnvelopeKind {
|
||||||
@ -1,4 +1,4 @@
|
|||||||
namespace MaksIT.PostClient.Shared;
|
namespace MaksIT.PostClient.Shared.Envelope;
|
||||||
|
|
||||||
|
|
||||||
public static class ReceiptStatus {
|
public static class ReceiptStatus {
|
||||||
@ -1,7 +1,7 @@
|
|||||||
using System.Xml;
|
using System.Xml;
|
||||||
|
|
||||||
|
|
||||||
namespace MaksIT.PostClient.Shared;
|
namespace MaksIT.PostClient.Shared.Envelope;
|
||||||
|
|
||||||
|
|
||||||
public static class RemEvidenceParser {
|
public static class RemEvidenceParser {
|
||||||
9
src/MaksIT.PostClient.Shared/GlobalUsings.cs
Normal file
9
src/MaksIT.PostClient.Shared/GlobalUsings.cs
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
global using MaksIT.PostClient.Shared.App;
|
||||||
|
global using MaksIT.PostClient.Shared.Archive;
|
||||||
|
global using MaksIT.PostClient.Shared.Auth;
|
||||||
|
global using MaksIT.PostClient.Shared.Config;
|
||||||
|
global using MaksIT.PostClient.Shared.Content;
|
||||||
|
global using MaksIT.PostClient.Shared.Envelope;
|
||||||
|
global using MaksIT.PostClient.Shared.Localization;
|
||||||
|
global using MaksIT.PostClient.Shared.Mail;
|
||||||
|
global using MaksIT.PostClient.Shared.Search;
|
||||||
@ -1,4 +1,4 @@
|
|||||||
namespace MaksIT.PostClient.Shared;
|
namespace MaksIT.PostClient.Shared.Localization;
|
||||||
|
|
||||||
|
|
||||||
public sealed partial class UiCopy {
|
public sealed partial class UiCopy {
|
||||||
@ -298,6 +298,8 @@ public sealed partial class UiCopy {
|
|||||||
|
|
||||||
public string Date { get; init; } = "";
|
public string Date { get; init; } = "";
|
||||||
|
|
||||||
|
public string Size { get; init; } = "";
|
||||||
|
|
||||||
public string SelectMessage { get; init; } = "";
|
public string SelectMessage { get; init; } = "";
|
||||||
|
|
||||||
public string Open { get; init; } = "";
|
public string Open { get; init; } = "";
|
||||||
@ -757,6 +759,7 @@ public sealed partial class UiCopy {
|
|||||||
Bcc = "Bcc",
|
Bcc = "Bcc",
|
||||||
Subject = "Subject",
|
Subject = "Subject",
|
||||||
Date = "Date",
|
Date = "Date",
|
||||||
|
Size = "Size",
|
||||||
SelectMessage = "Select a message to read.",
|
SelectMessage = "Select a message to read.",
|
||||||
Open = "Open",
|
Open = "Open",
|
||||||
Save = "Save",
|
Save = "Save",
|
||||||
@ -1028,6 +1031,7 @@ public sealed partial class UiCopy {
|
|||||||
Bcc = "Ccn",
|
Bcc = "Ccn",
|
||||||
Subject = "Oggetto",
|
Subject = "Oggetto",
|
||||||
Date = "Data",
|
Date = "Data",
|
||||||
|
Size = "Dimensione",
|
||||||
SelectMessage = "Seleziona un messaggio da leggere.",
|
SelectMessage = "Seleziona un messaggio da leggere.",
|
||||||
Open = "Apri",
|
Open = "Apri",
|
||||||
Save = "Salva",
|
Save = "Salva",
|
||||||
@ -1,4 +1,4 @@
|
|||||||
namespace MaksIT.PostClient.Shared;
|
namespace MaksIT.PostClient.Shared.Localization;
|
||||||
|
|
||||||
|
|
||||||
public sealed partial class UiCopy {
|
public sealed partial class UiCopy {
|
||||||
@ -152,6 +152,7 @@ public sealed partial class UiCopy {
|
|||||||
Bcc = "Bcc",
|
Bcc = "Bcc",
|
||||||
Subject = "Betreff",
|
Subject = "Betreff",
|
||||||
Date = "Datum",
|
Date = "Datum",
|
||||||
|
Size = "Größe",
|
||||||
SelectMessage = "Wählen Sie eine Nachricht zum Lesen.",
|
SelectMessage = "Wählen Sie eine Nachricht zum Lesen.",
|
||||||
Open = "Öffnen",
|
Open = "Öffnen",
|
||||||
Save = "Speichern",
|
Save = "Speichern",
|
||||||
@ -1,4 +1,4 @@
|
|||||||
namespace MaksIT.PostClient.Shared;
|
namespace MaksIT.PostClient.Shared.Localization;
|
||||||
|
|
||||||
|
|
||||||
public sealed partial class UiCopy {
|
public sealed partial class UiCopy {
|
||||||
@ -152,6 +152,7 @@ public sealed partial class UiCopy {
|
|||||||
Bcc = "Cco",
|
Bcc = "Cco",
|
||||||
Subject = "Asunto",
|
Subject = "Asunto",
|
||||||
Date = "Fecha",
|
Date = "Fecha",
|
||||||
|
Size = "Tamaño",
|
||||||
SelectMessage = "Seleccione un mensaje para leer.",
|
SelectMessage = "Seleccione un mensaje para leer.",
|
||||||
Open = "Abrir",
|
Open = "Abrir",
|
||||||
Save = "Guardar",
|
Save = "Guardar",
|
||||||
@ -1,4 +1,4 @@
|
|||||||
namespace MaksIT.PostClient.Shared;
|
namespace MaksIT.PostClient.Shared.Localization;
|
||||||
|
|
||||||
|
|
||||||
public sealed partial class UiCopy {
|
public sealed partial class UiCopy {
|
||||||
@ -152,6 +152,7 @@ public sealed partial class UiCopy {
|
|||||||
Bcc = "Cci",
|
Bcc = "Cci",
|
||||||
Subject = "Objet",
|
Subject = "Objet",
|
||||||
Date = "Date",
|
Date = "Date",
|
||||||
|
Size = "Taille",
|
||||||
SelectMessage = "Sélectionnez un message à lire.",
|
SelectMessage = "Sélectionnez un message à lire.",
|
||||||
Open = "Ouvrir",
|
Open = "Ouvrir",
|
||||||
Save = "Enregistrer",
|
Save = "Enregistrer",
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user