MailSharp / MailSharp.MailClient / Services / ContactStore.cs
Code · 74 lines · 3028 bytes
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74using LiteDB;
using MailSharp.MailClient.Models;

namespace MailSharp.MailClient.Services;

public interface IContactStore
{
	string? GetDisplayName(int accountId, string email);
	void SetDisplayName(int accountId, string email, string displayName);

	// Cached contact address list, keyed by account. Null means "never populated yet" - the
	// caller should fetch from IMAP once and call SaveContacts.
	List<ContactAddress>? GetCachedContacts(int accountId);
	void SaveContacts(int accountId, List<ContactAddress> contacts);

	// Merges newly-seen recipients (from a send/reply/forward) into the existing cache without
	// re-fetching the whole Sent folder. No-op if the cache hasn't been populated yet - the next
	// full list request will pick these up anyway.
	void AddContacts(int accountId, IEnumerable<ContactAddress> contacts);
}

// Stores the user's own edits to a contact's display name, plus the cached contact address list
// itself, keyed by account. The address list is expensive to build (a full walk of the Sent
// folder over IMAP - see IImapService.GetSentContactsAsync), so it's fetched once and then kept
// up to date incrementally as mail is sent, rather than being re-derived on every page load.
public class LiteDbContactStore : IContactStore, IDisposable
{
	private readonly LiteDatabase _db;

	public LiteDbContactStore(IWebHostEnvironment env)
	{
		var appDataDir = Path.Combine(env.ContentRootPath, "App_Data");
		Directory.CreateDirectory(appDataDir);
		_db = new LiteDatabase(Path.Combine(appDataDir, "contacts.db"));
	}

	private ILiteCollection<ContactOverride> Overrides => _db.GetCollection<ContactOverride>("contactOverrides");
	private ILiteCollection<ContactCache> Caches => _db.GetCollection<ContactCache>("contactCaches");

	private static string Key(int accountId, string email) => $"{accountId}:{email.Trim().ToLowerInvariant()}";

	public string? GetDisplayName(int accountId, string email) =>
		Overrides.FindById(Key(accountId, email))?.DisplayName;

	public void SetDisplayName(int accountId, string email, string displayName) =>
		Overrides.Upsert(new ContactOverride { Id = Key(accountId, email), DisplayName = displayName });

	public List<ContactAddress>? GetCachedContacts(int accountId) =>
		Caches.FindById(accountId)?.Contacts;

	public void SaveContacts(int accountId, List<ContactAddress> contacts) =>
		Caches.Upsert(new ContactCache { Id = accountId, Contacts = contacts });

	public void AddContacts(int accountId, IEnumerable<ContactAddress> contacts)
	{
		var cache = Caches.FindById(accountId);
		if (cache == null) return;

		var byEmail = cache.Contacts.ToDictionary(c => c.Email, StringComparer.OrdinalIgnoreCase);
		var changed = false;
		foreach (var c in contacts)
		{
			if (string.IsNullOrWhiteSpace(c.Email) || byEmail.ContainsKey(c.Email)) continue;
			byEmail[c.Email] = c;
			changed = true;
		}

		if (!changed) return;
		cache.Contacts = [.. byEmail.Values.OrderBy(c => c.Email)];
		Caches.Update(cache);
	}

	public void Dispose() => _db.Dispose();
}