V15 / 313404a015e8
ImapMailReader.cs
Services/EmailService/ImapMailReader.cs
Unchanged from the preceding versionCS
using MailKit;
using MailKit.Net.Imap;
using MailKit.Search;
using MailKit.Security;
namespace Lilith.EmailService;
public sealed class ImapMailReader : IMailReader
{
public async Task<MailContent> ReadMessageAsync(MailAccount account, string password, uint validity, uint uid, CancellationToken ct)
{
using var client = new ImapClient { Timeout = 20000 };
await client.ConnectAsync(account.Host, account.Port, SecureSocketOptions.SslOnConnect, ct);
await client.AuthenticateAsync(account.Address, password, ct);
await client.Inbox.OpenAsync(FolderAccess.ReadOnly, ct);
if (client.Inbox.UidValidity != validity) throw new InvalidOperationException("Mailbox identity changed; check the mailbox again.");
var id = new UniqueId(uid);
var summaries = await client.Inbox.FetchAsync(new[] { id }, MessageSummaryItems.Size | MessageSummaryItems.Flags, ct);
var summary = summaries.SingleOrDefault() ?? throw new FileNotFoundException("Message no longer exists.");
if (summary.Size is null || summary.Size > 2_000_000) throw new IOException("Message is too large for bounded reading.");
var message = await client.Inbox.GetMessageAsync(id, ct, new DownloadLimit());
var body = MailBodyText.Extract(message.TextBody, message.HtmlBody);
var sender = message.From.Mailboxes.FirstOrDefault();
string Bound(string? value, int max) => new((value ?? "").Where(c => !char.IsControl(c)).Take(max).ToArray());
var item = new MailItem(uid, Bound(sender?.Name, 200), Bound(sender?.Address, 320), Bound(message.Subject, 500), message.Date, !(summary.Flags ?? MessageFlags.None).HasFlag(MessageFlags.Seen));
await client.DisconnectAsync(true, ct);
return new("completed", account.Nickname, validity, item, body.Text, body.Truncated, "imap");
}
private sealed class DownloadLimit : ITransferProgress
{
public void Report(long bytesTransferred, long totalSize) => Report(bytesTransferred);
public void Report(long bytesTransferred) { if (bytesTransferred > 2_000_000) throw new IOException("Message exceeded the reading limit."); }
}
public async Task<int> CountUnreadAsync(MailAccount account, string password, CancellationToken ct)
{
using var client = new ImapClient { Timeout = 20000 };
await client.ConnectAsync(account.Host, account.Port, SecureSocketOptions.SslOnConnect, ct);
await client.AuthenticateAsync(account.Address, password, ct);
await client.Inbox.OpenAsync(FolderAccess.ReadOnly, ct);
int count = (await client.Inbox.SearchAsync(SearchQuery.NotSeen, ct)).Count;
await client.DisconnectAsync(true, ct);
return count;
}
public async Task<MailSnapshot> ReadAsync(MailAccount account, string password, string mode, uint validity, uint cursor, int limit, CancellationToken ct)
{
using var client = new ImapClient { Timeout = 20000 };
await client.ConnectAsync(account.Host, account.Port, SecureSocketOptions.SslOnConnect, ct);
await client.AuthenticateAsync(account.Address, password, ct);
var inbox = client.Inbox;
await inbox.OpenAsync(FolderAccess.ReadOnly, ct);
bool continuation = mode == "since_last_check" && validity == inbox.UidValidity && validity != 0;
var query = mode == "unread" ? SearchQuery.NotSeen : SearchQuery.All;
if (continuation)
{
if (cursor == uint.MaxValue) return new(inbox.UidValidity, []);
query = query.And(SearchQuery.Uids(new UniqueIdRange(new UniqueId(cursor + 1), UniqueId.MaxValue)));
}
var ids = await inbox.SearchAsync(query, ct);
// One extra item indicates more data. First checks intentionally seed the
// latest bounded inbox window; later checks drain arrivals oldest first.
var selected = (continuation ? ids.OrderBy(id => id.Id) : ids.OrderByDescending(id => id.Id)).Take(limit + 1).ToArray();
if (selected.Length == 0) return new(inbox.UidValidity, []);
var summaries = await inbox.FetchAsync(selected, MessageSummaryItems.UniqueId | MessageSummaryItems.Envelope | MessageSummaryItems.Flags, ct);
string Bound(string? value, int max) => new((value ?? "").Where(c => !char.IsControl(c)).Take(max).ToArray());
var items = summaries.Select(s => {
var envelope = s.Envelope ?? throw new InvalidDataException("IMAP server omitted a requested envelope.");
var sender = envelope.From?.Mailboxes.FirstOrDefault();
return new MailItem(s.UniqueId.Id, Bound(sender?.Name, 200), Bound(sender?.Address, 320), Bound(envelope.Subject, 500), envelope.Date, !(s.Flags ?? MessageFlags.None).HasFlag(MessageFlags.Seen));
});
await client.DisconnectAsync(true, ct);
return new(inbox.UidValidity, (continuation ? items.OrderBy(i => i.Uid) : items.OrderByDescending(i => i.Uid)).ToArray());
}
}