3e09e74fc8
- Move inline DTOs to GmailDtos.cs, pure parse helpers to GmailParsing.cs - Batch per-message existence checks in CreateSuggestedJob/RefreshLinkedThreads - Remove redundant second pass in RelinkThread, reuse existing HashSet - Replace ToListAsync+scan with FirstOrDefaultAsync for GmailReviewDecisions lookups Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
115 lines
4.7 KiB
C#
115 lines
4.7 KiB
C#
namespace JobTrackerApi.Services;
|
||
|
||
// Pure parsing/formatting helpers used by GmailController, split out for readability (no
|
||
// behaviour change). All are static and side-effect free.
|
||
public static class GmailParsing
|
||
{
|
||
public static string ApplySyncBoundary(string query, int lookbackDays, bool includeSpamTrash)
|
||
{
|
||
var bounded = (query ?? string.Empty).Trim();
|
||
if (!bounded.Contains("newer_than:", StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
bounded = string.IsNullOrWhiteSpace(bounded)
|
||
? $"newer_than:{lookbackDays}d"
|
||
: $"{bounded} newer_than:{lookbackDays}d";
|
||
}
|
||
|
||
if (!includeSpamTrash)
|
||
{
|
||
if (!bounded.Contains("in:spam", StringComparison.OrdinalIgnoreCase)) bounded += " -in:spam";
|
||
if (!bounded.Contains("in:trash", StringComparison.OrdinalIgnoreCase)) bounded += " -in:trash";
|
||
}
|
||
|
||
return bounded.Trim();
|
||
}
|
||
|
||
public static bool LooksLikeJobRelatedThread(IReadOnlyList<GmailQueryMatchedMessage> orderedMessages)
|
||
{
|
||
var sample = string.Join("\n", orderedMessages.Select(item => string.Join(" ", new[] { item.Message.Subject, item.Message.From, item.Message.Snippet }.Where(value => !string.IsNullOrWhiteSpace(value)))));
|
||
if (string.IsNullOrWhiteSpace(sample)) return false;
|
||
return sample.Contains("interview", StringComparison.OrdinalIgnoreCase)
|
||
|| sample.Contains("application", StringComparison.OrdinalIgnoreCase)
|
||
|| sample.Contains("recruit", StringComparison.OrdinalIgnoreCase)
|
||
|| sample.Contains("role", StringComparison.OrdinalIgnoreCase)
|
||
|| sample.Contains("position", StringComparison.OrdinalIgnoreCase)
|
||
|| sample.Contains("offer", StringComparison.OrdinalIgnoreCase)
|
||
|| sample.Contains("follow up", StringComparison.OrdinalIgnoreCase)
|
||
|| sample.Contains("follow-up", StringComparison.OrdinalIgnoreCase)
|
||
|| sample.Contains("rejection", StringComparison.OrdinalIgnoreCase);
|
||
}
|
||
|
||
public static string ToConfidence(int score)
|
||
{
|
||
return score switch
|
||
{
|
||
>= 30 => "high",
|
||
>= 16 => "medium",
|
||
_ => "low"
|
||
};
|
||
}
|
||
|
||
public static string? ExtractFirstEmail(string? value)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(value)) return null;
|
||
var match = System.Text.RegularExpressions.Regex.Match(value, @"[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}", System.Text.RegularExpressions.RegexOptions.IgnoreCase);
|
||
return match.Success ? match.Value : null;
|
||
}
|
||
|
||
public static string? ExtractRecruiterName(string? value)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(value)) return null;
|
||
var trimmed = value.Split('<')[0].Trim().Trim('"');
|
||
return string.IsNullOrWhiteSpace(trimmed) || trimmed.Contains('@') ? null : trimmed;
|
||
}
|
||
|
||
public static string? ExtractCompanyName(string? from, string? subject)
|
||
{
|
||
var subjectText = (subject ?? string.Empty).Trim();
|
||
if (!string.IsNullOrWhiteSpace(subjectText))
|
||
{
|
||
var parts = subjectText.Split(new[] { '-', '–', '|' }, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||
if (parts.Length >= 2) return parts[0];
|
||
}
|
||
|
||
var recruiterName = ExtractRecruiterName(from);
|
||
return recruiterName is { Length: > 0 } && recruiterName.Contains(' ') ? recruiterName.Split(' ').Last() : null;
|
||
}
|
||
|
||
public static string? ExtractRoleFromSubject(string? subject)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(subject)) return null;
|
||
var trimmed = subject.Trim();
|
||
if (trimmed.Contains("interview", StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
return trimmed.Replace("interview", string.Empty, StringComparison.OrdinalIgnoreCase).Trim(' ', '-', ':');
|
||
}
|
||
return trimmed.Length <= 120 ? trimmed : trimmed[..120];
|
||
}
|
||
|
||
public static string BuildPopupHtml(bool success, string message)
|
||
{
|
||
var escaped = System.Net.WebUtility.HtmlEncode(message);
|
||
var status = success ? "connected" : "error";
|
||
var title = success ? "Gmail connected" : "Gmail connection failed";
|
||
var serializedMessage = System.Text.Json.JsonSerializer.Serialize(message);
|
||
return $@"<!doctype html>
|
||
<html>
|
||
<head>
|
||
<meta charset=""utf-8"" />
|
||
<title>Gmail connection</title>
|
||
</head>
|
||
<body style=""font-family:Segoe UI,Arial,sans-serif;padding:24px;line-height:1.5;"">
|
||
<h2>{title}</h2>
|
||
<p>{escaped}</p>
|
||
<p>You can close this window.</p>
|
||
<script>
|
||
if (window.opener) {{
|
||
window.opener.postMessage({{ source: 'jobtracker-gmail-oauth', status: '{status}', message: {serializedMessage} }}, '*');
|
||
}}
|
||
window.close();
|
||
</script>
|
||
</body>
|
||
</html>";
|
||
}
|
||
}
|