feat: Add Gmail Catchall Labeler service with configuration and functionality
Build and Push Docker Image / build-and-push (push) Successful in 2m21s

- Introduced a new Docker Compose configuration for the Gmail Catchall Labeler service.
- Implemented GmailOptions class for managing configuration settings.
- Developed GmailApiClient to interact with the Gmail API, including methods for fetching messages and managing labels.
- Created models for Gmail messages, labels, and headers.
- Added IGmailClient interface to define the contract for Gmail client implementations.
- Implemented GmailOrganizer to handle the organization of emails based on recipient addresses.
- Developed LabelNameResolver for generating label names based on local parts of email addresses.
- Created RecipientExtractor to extract local parts from email headers.
- Implemented state management with OrganizerState and StateStore for persisting state information.
- Added Worker class to manage the background processing of email organization.
- Created unit tests for GmailOrganizer, LabelNameResolver, and RecipientExtractor to ensure functionality.
- Added necessary project files and configurations for building and running the application.
This commit is contained in:
2026-09-26 20:05:21 +02:00
commit 48488b72b5
29 changed files with 1279 additions and 0 deletions
@@ -0,0 +1,57 @@
using GmailCatchallLabeler.Configuration;
using GmailCatchallLabeler.Gmail;
using GmailCatchallLabeler.Labels;
namespace GmailCatchallLabeler.Tests;
public sealed class RecipientExtractorTests
{
private readonly RecipientExtractor extractor = new(new GmailOptions
{
CatchallDomain = "arnemoerman.be"
});
[Fact]
public void ExtractLocalParts_ReadsDeliveryAndRecipientHeaders()
{
GmailHeader[] headers =
[
new("Delivered-To", "tomscott@arnemoerman.be"),
new("To", "Example <every-foods@arnemoerman.be>, other@example.com"),
new("Subject", "Ignored")
];
var result = extractor.ExtractLocalParts(headers);
Assert.Equal(2, result.Count);
Assert.Contains("tomscott", result);
Assert.Contains("every-foods", result);
}
[Fact]
public void ExtractLocalParts_IsCaseInsensitiveAndDeduplicates()
{
GmailHeader[] headers =
[
new("X-Original-To", "News@ArneMoerman.be"),
new("Cc", "news@arnemoerman.be")
];
var result = extractor.ExtractLocalParts(headers);
Assert.Single(result);
Assert.Contains("News", result);
}
[Fact]
public void ExtractLocalParts_IgnoresOtherDomainsAndMalformedHeaders()
{
GmailHeader[] headers =
[
new("To", "other@example.com"),
new("Envelope-To", "not an address <")
];
Assert.Empty(extractor.ExtractLocalParts(headers));
}
}