{Login}
", + LoginPattern = "^a-.*", + }, + }; + + var result = ArchiveScanner.Scan(tempDir, "redlab.lo", templates); + + var job = Assert.Single(result.Jobs); + Assert.Equal(SendStatus.Ready, job.Status); + Assert.Equal("a-makarov", job.Login); + Assert.Equal("a.makarov@redlab.lo", job.Email); + Assert.Empty(result.UnrecognizedFiles); + } + + [Fact] + public void Scan_LoginDoesNotMatchTemplateLoginPattern_IsErrorInUnrecognized() + { + var filePath = Path.Combine(tempDir, "b-petrov_ssh_access.7z"); + WriteArchive(filePath); + + var templates = new List{Login}
", + LoginPattern = "^a-.*", + }, + }; + + var result = ArchiveScanner.Scan(tempDir, "redlab.lo", templates); + + Assert.Empty(result.Jobs); + var row = Assert.Single(result.UnrecognizedFiles); + Assert.Equal(SendStatus.Error, row.Status); + Assert.Contains("b-petrov", row.ErrorMessage); + Assert.Contains("SSH-доступ (только a-*)", row.ErrorMessage); + Assert.Contains("^a-.*", row.ErrorMessage); + } + + [Fact] + public void Scan_EmptyLoginPattern_MatchesAnyLogin_UnchangedLegacyBehavior() + { + var filePath = Path.Combine(tempDir, "z-unrelated_ssh_access.7z"); + WriteArchive(filePath); + + var templates = new List{Login}
", + LoginPattern = "", + }, + }; + + var result = ArchiveScanner.Scan(tempDir, "redlab.lo", templates); + + var job = Assert.Single(result.Jobs); + Assert.Equal(SendStatus.Ready, job.Status); + Assert.Equal("z-unrelated", job.Login); + } + + [Fact] + public void Scan_InvalidLoginPatternRegex_TreatedAsNoMatchRatherThanThrowing() + { + var filePath = Path.Combine(tempDir, "a-makarov_ssh_access.7z"); + WriteArchive(filePath); + + var templates = new List{Login}
", + // Unbalanced group - not a valid regex. + LoginPattern = "(a-", + }, + }; + + var result = ArchiveScanner.Scan(tempDir, "redlab.lo", templates); + + Assert.Empty(result.Jobs); + var row = Assert.Single(result.UnrecognizedFiles); + Assert.Equal(SendStatus.Error, row.Status); + } + } +} diff --git a/outlook-bulk-sender/BulkArchiveMailer.Core/AppSettings.cs b/outlook-bulk-sender/BulkArchiveMailer.Core/AppSettings.cs index c6a9b61..da54b72 100644 --- a/outlook-bulk-sender/BulkArchiveMailer.Core/AppSettings.cs +++ b/outlook-bulk-sender/BulkArchiveMailer.Core/AppSettings.cs @@ -24,6 +24,10 @@ namespace BulkArchiveMailer.Core [DataMember(Name = "bodyHtml")] public string BodyHtml { get; set; } = ""; + + ///Здравствуйте!
Во вложении — архив с вашим персональным SSH-ключом, готовым клиентским конфигом и краткой инструкцией по подключению ({Login}_ssh_access.7z).
Пароль от архива будет отправлен отдельным сообщением — не пересылайте архив вместе с паролем.
Если по какой-то причине не получается распаковать архив или подключиться — напишите в ответ на это письмо.
" + "bodyHtml": "Здравствуйте!
Во вложении — архив с вашим персональным SSH-ключом, готовым клиентским конфигом и краткой инструкцией по подключению ({Login}_ssh_access.7z).
Пароль от архива будет отправлен отдельным сообщением — не пересылайте архив вместе с паролем.
Если по какой-то причине не получается распаковать архив или подключиться — напишите в ответ на это письмо.
", + "loginPattern": "" }, { "displayName": "Обновление конфига", "fileSuffix": "config_update", "subject": "Обновление конфигурации доступа — {Login}", - "bodyHtml": "Здравствуйте!
Список доступных вам через jump-хост сервисов изменился. Во вложении — обновлённый клиентский конфиг и инструкция ({Login}_config_update.7z). Ваш существующий SSH-ключ по-прежнему действителен, переустанавливать его не нужно — достаточно заменить конфиг.
Пароль от архива будет отправлен отдельным сообщением — не пересылайте архив вместе с паролем.
" + "bodyHtml": "Здравствуйте!
Список доступных вам через jump-хост сервисов изменился. Во вложении — обновлённый клиентский конфиг и инструкция ({Login}_config_update.7z). Ваш существующий SSH-ключ по-прежнему действителен, переустанавливать его не нужно — достаточно заменить конфиг.
Пароль от архива будет отправлен отдельным сообщением — не пересылайте архив вместе с паролем.
", + "loginPattern": "" } ] } diff --git a/outlook-bulk-sender/README.md b/outlook-bulk-sender/README.md index fab2cd3..3c292a0 100644 --- a/outlook-bulk-sender/README.md +++ b/outlook-bulk-sender/README.md @@ -51,7 +51,7 @@ BulkArchiveMailer.Core/ net48, class library, БЕЗ ссылок н FilenameParser.cs парсинг "{login}_{суффикс}.7z" (суффикс - любой, сверяется с шаблонами в ArchiveScanner) EmailAddressBuilder.cs login -> "login.with.dots@domain" SendJob.cs модель строки предпросмотра (Login/Email/Kind/ArchivePath/…/Status) - ArchiveScanner.cs сканирование папки, сопоставление суффикса с шаблоном, конфликт "оба архива сразу" + ArchiveScanner.cs сканирование папки, сопоставление суффикса (и необязательного regex-фильтра логина) с шаблоном, конфликт "оба архива сразу" TemplateEngine.cs подстановка {Login}/{Email}/{Date} в шаблон MailTemplateDefinition.cs один шаблон (название, суффикс файла, тема, HTML-тело) - список произвольной длины SendLogStore.cs CSV-журнал отправок + дедуп по (Email, Kind, SHA-256 архива) @@ -96,13 +96,25 @@ Outlook в принципе не запустить. - **Шаблоны писем** — вкладка "Шаблоны" со списком произвольного числа шаблонов (кнопки "+ Добавить"/"Удалить"). Каждый шаблон — это название, суффикс имени файла (`{login}_<суффикс>.7z`, по которому шаблон подбирается - для конкретного архива), тема и HTML-тело с плейсхолдерами `{Login}`, - `{Email}`, `{Date}`. Суффикс обязателен и должен быть уникальным среди всех - шаблонов — по нему, а не по фиксированному перечислению видов архива, - `ArchiveScanner` теперь сопоставляет файл с шаблоном. Два исходных шаблона - (`ssh_access`/`config_update`) — обычные равноправные записи в этом - списке, без особой защиты от переименования/удаления; кнопка "Сбросить - шаблоны по умолчанию" восстанавливает их, если они были случайно удалены. + для конкретного архива), необязательный фильтр логина (regex) и тема с + HTML-телом с плейсхолдерами `{Login}`, `{Email}`, `{Date}`. Суффикс + обязателен и должен быть уникальным среди всех шаблонов — по нему, а не по + фиксированному перечислению видов архива, `ArchiveScanner` сопоставляет + файл с шаблоном. Два исходных шаблона (`ssh_access`/`config_update`) — + обычные равноправные записи в этом списке, без особой защиты от + переименования/удаления; кнопка "Сбросить шаблоны по умолчанию" + восстанавливает их, если они были случайно удалены. + - **Фильтр логина** — необязательное регулярное выражение (.NET regex, + без учёта регистра). Пустое значение (по умолчанию, в том числе у + шаблонов, мигрированных из конфига старого формата) означает "любой + логин" — прежнее поведение, когда только суффикс решает, подходит ли + файл под шаблон. Если поле заполнено, а логин из имени файла ему не + соответствует, файл не считается подходящим под этот шаблон: строка в + предпросмотре получает статус "Ошибка" с пояснением, какому шаблону и + какому паттерну логин не соответствует, — автоматический подбор другого + шаблона не выполняется. Некорректный regex (не компилируется) не даёт + сохранить настройки — окно "Настройки" укажет, в каком именно шаблоне + ошибка. - **Тестовый режим по умолчанию** — включён по умолчанию (письма сохраняются в Черновики, а не отправляются) до первого осознанного отключения оператором в окне предпросмотра.