| | | 1 | | using System.Text.RegularExpressions; |
| | | 2 | | using OzonDomains.Models; |
| | | 3 | | |
| | | 4 | | namespace Services.EmailRoutingServices; |
| | | 5 | | |
| | | 6 | | public class RoutingRuleEvaluator : IRoutingRuleEvaluator |
| | | 7 | | { |
| | | 8 | | public RuleEvaluationResult Evaluate(InboxMessageDto message, InboxAttachmentDto attachment, List<EmailRoutingRule> |
| | | 9 | | { |
| | 289 | 10 | | foreach (var rule in rules.OrderBy(x => x.Priority)) |
| | | 11 | | { |
| | 69 | 12 | | var reason = ValidateRule(rule, message, attachment); |
| | 69 | 13 | | if (reason == null) |
| | | 14 | | { |
| | 49 | 15 | | return new RuleEvaluationResult { IsMatch = true, Rule = rule }; |
| | | 16 | | } |
| | | 17 | | } |
| | | 18 | | |
| | 14 | 19 | | return new RuleEvaluationResult |
| | 14 | 20 | | { |
| | 14 | 21 | | IsMatch = false, |
| | 14 | 22 | | RejectReason = "Не найдено подходящее активное правило." |
| | 14 | 23 | | }; |
| | 49 | 24 | | } |
| | | 25 | | |
| | | 26 | | private static string? ValidateRule(EmailRoutingRule rule, InboxMessageDto message, InboxAttachmentDto attachment) |
| | | 27 | | { |
| | 69 | 28 | | if (!rule.IsActive) |
| | | 29 | | { |
| | 2 | 30 | | return "Правило выключено."; |
| | | 31 | | } |
| | | 32 | | |
| | 67 | 33 | | if (!string.IsNullOrWhiteSpace(rule.AllowedSenders)) |
| | | 34 | | { |
| | 21 | 35 | | var allowed = SplitCsv(rule.AllowedSenders); |
| | 21 | 36 | | var sender = (message.SenderEmail ?? string.Empty).Trim().ToLowerInvariant(); |
| | 21 | 37 | | if (!IsSenderAllowed(sender, allowed)) |
| | | 38 | | { |
| | 10 | 39 | | return "Отправитель не разрешен правилом."; |
| | | 40 | | } |
| | | 41 | | } |
| | | 42 | | |
| | 57 | 43 | | if (!MatchesPattern(rule.SubjectPattern, message.Subject)) |
| | | 44 | | { |
| | 1 | 45 | | return "Тема письма не соответствует правилу."; |
| | | 46 | | } |
| | | 47 | | |
| | 56 | 48 | | if (!MatchesPattern(rule.BodyPattern, message.BodyText)) |
| | | 49 | | { |
| | 1 | 50 | | return "Тело письма не соответствует правилу."; |
| | | 51 | | } |
| | | 52 | | |
| | 55 | 53 | | if (!MatchesPattern(rule.FileNamePattern, attachment.FileName)) |
| | | 54 | | { |
| | 1 | 55 | | return "Имя файла не соответствует правилу."; |
| | | 56 | | } |
| | | 57 | | |
| | 54 | 58 | | if (!string.IsNullOrWhiteSpace(rule.AllowedExtensions)) |
| | | 59 | | { |
| | 13 | 60 | | var allowedExts = SplitCsv(rule.AllowedExtensions) |
| | 24 | 61 | | .Select(x => x.StartsWith('.') ? x : "." + x) |
| | 13 | 62 | | .ToHashSet(StringComparer.OrdinalIgnoreCase); |
| | 13 | 63 | | if (!allowedExts.Contains(attachment.Extension ?? string.Empty)) |
| | | 64 | | { |
| | 3 | 65 | | return "Расширение файла не разрешено."; |
| | | 66 | | } |
| | | 67 | | } |
| | | 68 | | |
| | 51 | 69 | | if (rule.MinFileSizeBytes.HasValue && attachment.FileSizeBytes < rule.MinFileSizeBytes.Value) |
| | | 70 | | { |
| | 1 | 71 | | return "Размер файла меньше минимально допустимого."; |
| | | 72 | | } |
| | | 73 | | |
| | 50 | 74 | | if (rule.MaxFileSizeBytes.HasValue && attachment.FileSizeBytes > rule.MaxFileSizeBytes.Value) |
| | | 75 | | { |
| | 1 | 76 | | return "Размер файла больше максимально допустимого."; |
| | | 77 | | } |
| | | 78 | | |
| | 49 | 79 | | return null; |
| | | 80 | | } |
| | | 81 | | |
| | | 82 | | private static bool MatchesPattern(string? pattern, string? input) |
| | | 83 | | { |
| | 168 | 84 | | if (string.IsNullOrWhiteSpace(pattern)) |
| | | 85 | | { |
| | 148 | 86 | | return true; |
| | | 87 | | } |
| | | 88 | | |
| | 20 | 89 | | return Regex.IsMatch(input ?? string.Empty, pattern, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); |
| | | 90 | | } |
| | | 91 | | |
| | | 92 | | private static List<string> SplitCsv(string csv) |
| | | 93 | | { |
| | 34 | 94 | | return csv.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) |
| | 47 | 95 | | .Select(x => x.Trim().ToLowerInvariant()) |
| | 47 | 96 | | .Where(x => !string.IsNullOrWhiteSpace(x)) |
| | 34 | 97 | | .ToList(); |
| | | 98 | | } |
| | | 99 | | |
| | | 100 | | private static bool IsSenderAllowed(string sender, List<string> allowedItems) |
| | | 101 | | { |
| | 21 | 102 | | if (string.IsNullOrWhiteSpace(sender)) |
| | | 103 | | { |
| | 1 | 104 | | return false; |
| | | 105 | | } |
| | | 106 | | |
| | 73 | 107 | | foreach (var item in allowedItems) |
| | | 108 | | { |
| | | 109 | | // Exact sender match, e.g. supplier@example.com |
| | 22 | 110 | | if (sender.Equals(item, StringComparison.OrdinalIgnoreCase)) |
| | | 111 | | { |
| | 7 | 112 | | return true; |
| | | 113 | | } |
| | | 114 | | |
| | | 115 | | // Domain match from UI hint, e.g. @@domain.ru |
| | 15 | 116 | | if (item.StartsWith("@@") && sender.EndsWith(item[1..], StringComparison.OrdinalIgnoreCase)) |
| | | 117 | | { |
| | 4 | 118 | | return true; |
| | | 119 | | } |
| | | 120 | | } |
| | | 121 | | |
| | 9 | 122 | | return false; |
| | 11 | 123 | | } |
| | | 124 | | } |