В этой статье
Письмо исчезло. Начинаем с того, что действительно произошло
Привет, коллеги. Меня зовут Максим, я системный администратор. Отправитель показывает письмо в «Отправленных», бухгалтерия проверяет «Входящие», а между ними начинается знакомый разговор: «Наш сервер всё отправил — ищите у себя». Через полчаса в переписке уже три администратора, два скриншота и ни одного события, которое связывает их версии происходящего.
Другой вариант неприятнее: письмо с договором пришло одному адресату, но не пришло второму. Или шлюз показывает зелёный статус, очередь Exchange пуста, а документ так и не дошёл. Бизнес теряет время не только из-за задержки почты: люди повторно отправляют вложения, меняют адреса и создают несколько похожих сообщений, которые теперь тоже нужно различать.
Я предлагаю заменить спор о том, чей сервер исправен, расследованием одной конкретной передачи. Мы построим временную линию, установим последний доказанный этап и сформулируем следующий проверяемый вопрос. Для этого понадобится меньше команд, чем для массового перезапуска служб, но больше внимания к смыслу результатов.
Основной инструментальный контур статьи — локальный Exchange Server Subscription Edition и Windows PowerShell 5.1 в Exchange Management Shell. Многие механизмы и команды присутствуют в Exchange 2016/2019; наличие команды не делает старую установку поддерживаемой. Для Exchange Online требуется отдельный облачный message trace: приведённые локальные команды туда переносить нельзя. Имена EX01, EX02, GW01 и домены example.com/example.net условные. Сквозной эпизод ниже — учебная модель, а не отчёт о реальной аварии.
Четыре разные проблемы, которые называют потерей письма
Прежде чем искать потерю, нужно определить, какое наблюдение отсутствует. Я разделяю заявления «письмо не пришло» на четыре рабочих класса.
- Не подтверждена передача (submission gap). Клиент сохранил копию, но мы пока не установили приём транспортом. Пример: пользователь приложил скриншот, а исходных заголовков и времени нет. Проверять карантин внешнего адресата на этом этапе рано.
- Подтверждён приём, но нет следующей передачи (transport gap). Узел принял сообщение и удерживает его в очереди, обрабатывает или поместил в карантин. Здесь важно найти объект и его текущее состояние, а не убедиться, что служба запущена.
- Разошлись ветки адресатов (recipient divergence). Из двух получателей один внутренний, другой внешний. Их маршруты и результаты независимы; успех первого ничего не доказывает о втором.
- Доставка состоялась, видимость изменилась (post-delivery gap). Транспорт зарегистрировал доставку, но правило, клиент или человек переместил письмо. Повторная диагностика SMTP уже не отвечает на исходный вопрос.
Это классификация для расследования, а не перечень всех функций почтовой системы. Она нужна, чтобы не перескакивать через границы доказательств. У каждого перехода есть собственный источник: клиент, транспорт, шлюз, хранилище ящика. Важнее найти недостающий переход, чем собрать побольше зелёных статусов.
Мы пройдём четыре уровня: зафиксируем исходное сообщение, восстановим ветки внутри Exchange, проверим SMTP-границу, затем разберём обработку следующего узла и видимость в ящике. После этого «ищите у себя» превратится в запрос конкретного журнала за конкретный интервал.
Уровень 1. Заводим карточку письма, а не ищем тему «Договор»
В учебном эпизоде сотрудник sender@example.com отправил договор двум адресатам: внутреннему audit@example.com и внешнему finance@example.net. Audit письмо видит, finance — нет. Начинать с поиска слова «договор» неудобно: повторные отправки и ответы будут выглядеть одинаково.
Прошу сохранить оригинал в EML либо исходные заголовки доступным способом. Пересылка «для примера» создаёт другое сообщение; скриншот скрывает машинные поля. Для карточки нужны дата и время с часовым поясом, точные адреса отправителя и проблемного получателя, полный Message-ID, маршрут по доступным Received, размер и тема как вспомогательные признаки. Если письмо содержит персональные данные, исходник остаётся в согласованном месте расследования.
Отдельно фиксирую, что обозначает адрес: видимый From не всегда совпадает с конвертным MAIL FROM, а To/Cc не перечисляют всех SMTP-получателей. Bcc, группы и перенаправления делают это особенно заметным. Возврат ошибки обычно связан с адресом конверта, поэтому вопрос «отправителю ничего не пришло» требует проверки правильного ящика.
В нашей карточке появляется идентификатор <invoice-20261026-01@example.com> и окно 09:55–10:15 по времени рабочей сессии. Сам идентификатор не является подписью подлинности: его можно повторить или подделать. Я сопоставляю его с временем, адресатами и соседними узлами. Если сторонняя система пересобрала письмо, придётся документировать соответствие старого и нового объекта, а не объявлять их одним письмом по совпадению темы.
250: какое именно действие сервер подтвердил
Наивная проверка выглядит так: в журнале есть 250 — значит, доставлено. Но SMTP использует успешные ответы на разных шагах. Ответ на RCPT TO подтверждает принятие конкретного адресата на этом этапе. Он не подтверждает приём тела сообщения. Даже успешный EHLO ничего не говорит о судьбе договора.
Нас интересует завершение транзакции: после передачи содержимого через DATA принимающая сторона возвращает финальный успешный ответ. Для принятого набора получателей узел берёт на себя ответственность за дальнейшую обработку. Он может быть промежуточным шлюзом, а не сервером ящика. Именно поэтому «принято» и «появилось у пользователя» нельзя объединять. Основа такого разграничения — SMTP, RFC 5321.
Есть и неприятная пограничная ситуация: принимающий сервер уже принял содержимое, но отправляющий не увидел финальный ответ из-за разрыва соединения. Повторная попытка способна создать дубликат. Поэтому таймаут на отправителе не всегда доказывает отсутствие приёма у получателя. Здесь нужны обе стороны одной транзакции.
Мы не будем вручную повторно отправлять рабочий договор до восстановления цепочки. Если письмо уже в карантине, ещё одна копия добавит путаницы. Если оно доставлено, повтор может запустить бизнес-действие дважды. Сначала установим, какой компонент и для каких RCPT завершил передачу, затем решим, нужно ли воспроизведение на безвредном тестовом письме.
| Наблюдение | Что подтверждено | Чего ещё не знаем |
|---|---|---|
| Письмо в «Отправленных» | Клиент сохранил отправленную копию | Какой следующий узел принял оригинал |
| 250 после DATA | Узел принял сообщение для согласованных RCPT | Доставлено ли оно в конечный ящик |
| SEND / SMTP | Exchange передал письмо следующему SMTP-узлу | Что сделал следующий узел |
| DELIVER / STOREDRIVER | Exchange зарегистрировал доставку в локальный ящик | В какой папке письмо сейчас и видел ли его человек |
Подготовка: журналы существуют не потому, что нам понадобились
Перед поиском я проверяю версию Exchange, нужные транспортные серверы, настройки tracking и доступный интервал хранения. Запрос к EX01 не охватывает EX02 автоматически. Edge, сторонний шлюз и облачный участок требуют собственных источников и прав. Список серверов в примере — явный набор для учебного маршрута; в реальной системе его составляют по топологии и фактической трассе.
# Exchange Management Shell, Windows PowerShell 5.1.
# Replace EX01 / EX02 with the servers on the actual route.
$Servers = @('EX01','EX02')
Get-ExchangeServer | Select-Object Name,AdminDisplayVersion,ServerRole
foreach ($Server in $Servers) {
Get-TransportService -Identity $Server -ErrorAction Stop |
Format-List Name,MessageTrackingLogEnabled,MessageTrackingLogPath,
MessageTrackingLogMaxAge,MessageTrackingLogMaxDirectorySize,
SendProtocolLogPath,ReceiveProtocolLogPath
}
Get-Service MSExchangeTransportLogSearch | Select-Object Name,Status
Get-TimeZone | Select-Object Id,DisplayName
Get-Date -Format o
Проверка службы в блоке относится к локальному компьютеру: команда Get-Service без удалённого параметра не проверяет все перечисленные узлы. На каждом нужном сервере отдельно выясняем доступность поиска. Ошибка доступа или остановленная Transport Log Search не равны отсутствию почтового события. Роль и разрешения выдаём под задачу; раздавать Organization Management ради одного поиска не требуется.
Сырые журналы tracking и SMTP используют UTC. Для параметров Start/End и вывода оболочки нужно зафиксировать фактический временной контекст, особенно при удалённой сессии. В статье даты создаются через конструктор DateTime, чтобы не зависеть от формата день/месяц. Не приписывайте Z локальному времени механическим добавлением символа: это меняет смысл отметки без преобразования. Для сверки используйте известное тестовое письмо и исходный UTC-журнал.
Сначала небольшое окно, затем обоснованное расширение. Слишком узкое окно пропустит задержку, слишком широкое усложнит корреляцию. Истёкшее хранение нельзя исправить сегодняшним включением logging; оно создаст доказательства только для следующих событий. Microsoft отдельно описывает поиск и его зависимости.
Уровень 2. Находим письмо и сохраняем неполноту поиска
Теперь ищем полный Message-ID, включая угловые скобки, если они присутствуют в оригинале. Важно сохранить имя опрошенного сервера: транспортные поля ClientHostname и ServerHostname описывают событие, а не заменяют сведения об источнике нашей выгрузки.
# Run in EMS on an Exchange server in the incident's time zone.
# Sample names and date must be replaced. Start with a narrow window.
$Servers = @('EX01','EX02')
$Start = [datetime]::new(2026,10,26,9,55,0)
$End = [datetime]::new(2026,10,26,10,15,0)
$MessageId = '<invoice-20261026-01@example.com>'
$Rows = @()
$Failures = @()
foreach ($Server in $Servers) {
try {
$Rows += @(Get-MessageTrackingLog -Server $Server -Start $Start -End $End -MessageId $MessageId -ResultSize Unlimited -ErrorAction Stop |
Select-Object @{Name='QueriedServer';Expression={$Server}},*)
} catch {
$Failures += [pscustomobject]@{Server=$Server;Error=$_.Exception.Message}
}
}
$Rows | Sort-Object Timestamp,QueriedServer |
Format-Table Timestamp,QueriedServer,EventId,Source,Recipients -Wrap
$Failures | Format-Table -Wrap
# Empty rows plus failed servers is incomplete evidence, not "not received".
Полученные строки ещё не являются готовым маршрутом. Сортировка помогает прочитать последовательность, но часы могут расходиться, а разные серверы записывают разные стороны передачи. Сначала проверяем смысл и связь событий, затем вычисляем длительности. Отрицательная «задержка» в одну секунду — повод проверить время, а не открыть новую теорию ускорения SMTP.
Особенно важна переменная Failures. Если EX02 не ответил, итог «письмо нигде не найдено» недопустим. Корректно: «на EX01 за указанное окно совпадений нет, EX02 не исследован из-за ошибки». Такой результат менее эффектен, зато следующий администратор понимает, что нужно сделать.
Если исходного идентификатора пока нет, используем адреса и окно, чтобы получить кандидатов:
# Alternative when the Message-ID is not yet available.
# Reuse $Servers, $Start and $End from the previous block.
$Candidates = foreach ($Server in $Servers) {
Get-MessageTrackingLog -Server $Server -Start $Start -End $End -Sender 'sender@example.com' -Recipients 'finance@example.net' -ResultSize Unlimited -ErrorAction Stop |
Select-Object @{Name='QueriedServer';Expression={$Server}},Timestamp,EventId,MessageId,NetworkMessageId,Recipients,MessageSubject
}
$Candidates | Sort-Object Timestamp | Format-Table -Wrap
# Confirm the candidate with original headers. Then search by exact Message-ID.
Совпадение темы не выбирает кандидата автоматически. При нескольких отправках сравниваем исходные заголовки и уточняем идентификатор. Параметр ResultSize Unlimited убирает стандартное ограничение количества найденных строк, но не возвращает удалённые журналы и не отменяет фильтры. Не начинаем с поиска за месяц по всей организации. Параметры и применимость приведены в справке Get-MessageTrackingLog.
Три идентификатора и одна ловушка с несколькими получателями
Я держу рядом три уровня идентичности. Message-ID связывает исходное сообщение между системами, пока промежуточная обработка его сохраняет. NetworkMessageId помогает проследить связанные копии внутри Exchange. InternalMessageId относится к обработке на конкретном сервере: одно число нельзя искать как универсальный номер во всех узлах.
Если нашли подтверждённую ветку Exchange, расширяем поиск по её NetworkMessageId. В отличие от стартового поиска, здесь намеренно нет фильтра по одному получателю: он мог бы скрыть расширение группы и соседнюю ветку.
# Reuse $Rows, $Servers, $Start and $End from the exact-ID search.
$NetworkIds = @($Rows | Where-Object { $_.NetworkMessageId } |
Select-Object -ExpandProperty NetworkMessageId -Unique)
if ($NetworkIds.Count -ne 1) {
throw 'Select one confirmed message lineage; do not merge unrelated IDs.'
}
$NetworkId = [string]$NetworkIds[0]
$Lineage = foreach ($Server in $Servers) {
Get-MessageTrackingLog -Server $Server -Start $Start -End $End -NetworkMessageId $NetworkId -ResultSize Unlimited -ErrorAction Stop |
Select-Object @{Name='QueriedServer';Expression={$Server}},*
}
$Lineage | Sort-Object Timestamp,QueriedServer |
Format-List Timestamp,QueriedServer,EventId,Source,MessageId,
InternalMessageId,NetworkMessageId,Recipients,RecipientStatus,
RelatedRecipientAddress,Reference,ConnectorId,SourceContext
Защита Count в примере останавливает автоматическое объединение нескольких линий. Если идентификаторов несколько, это повод выбрать подтверждённый объект, а не снять проверку. В среде с пересозданием сообщений линейная модель может превратиться в граф: исходное письмо, новое уведомление, перенаправленная копия. Связь подтверждают журналы соответствующего компонента.
Для группы важны фактические адресаты после раскрытия; для перенаправления — исходный и новый адрес. Поле Reference читают в контексте типа события, а не как один и тот же вид ссылки в каждой строке. Recipients и RecipientStatus сохраняют совместно: их позиции соответствуют друг другу. Сведение массивов к первой строке легко превращает ошибку одного адресата в успех всего письма.
В нашем случае audit@example.com остаётся внутри, finance@example.net уходит через GW01. Обе ветки законны, но проверять их нужно раздельно. Описание полей и событий tracking у Microsoft служит словарём; вывод о маршруте мы делаем по совокупности строк.
Читаем события как переходы, а не как коллекцию хороших слов
Для первого прохода достаточно небольшого словаря. RECEIVE обозначает приём в транспорт, SEND — передачу дальше, DELIVER — доставку в локальный ящик; DEFER говорит о задержке, FAIL — о неудаче. Всегда читаем пару EventId и Source, получателя и диагностические поля, а не одно название.
Наивный вывод «вижу SEND — закрываю заявку» проваливается на нашем маршруте: отправка с EX01 в шлюз не является внешней доставкой. Аналогично строка RECEIVE на EX02 не означает, что сообщение уже записано в нужный ящик. Нам нужна последовательность переходов конкретной ветки.
Раскрытие группы, перенаправление и разделение сообщения объясняют, почему после одного входа событий становится несколько. Служебные события резервирования транспорта тоже не нужно выдавать за независимые доставки человеку. Если запись означает отбрасывание, исследуем причину и обработчик: само слово DROP не является заключением «антиспам признал письмо вирусом».
Пока в учебной линии получается следующее: EX01 принял сообщение, внутренняя ветка дошла до EX02 и завершилась DELIVER, внешняя получила SEND в сторону GW01. Это уже полезный результат. Мы сняли подозрение с общей отправки и доказали внутренний исход, но не выяснили судьбу внешнего адресата.
Я записываю это прямо в карточку. Вместо «почта работает» — «для audit доставка зарегистрирована; для finance подтверждена передача GW01, дальнейший этап не установлен». Следующий уровень расследования начинается именно на этой границе.
Очередь: фотография настоящего, а не история письма
Перед разбором SMTP смотрим, не находится ли проблемная копия ещё у нас. Очередь показывает текущее состояние транспорта; tracking — историю событий. Один источник не заменяет другой. Пустая очередь спустя час совместима с успешной передачей, истечением срока, удалением, карантином на другом узле и многими другими исходами.
# Current state only; execute on the transport server holding the message.
$Server = 'EX01'
$MessageId = '<invoice-20261026-01@example.com>'
Get-Queue -Server $Server -ErrorAction Stop |
Select-Object Identity,Status,MessageCount,DeliveryType,NextHopDomain,LastError
# Server-side exact filter. Escape quotes before interpolating the identifier.
$SafeId = $MessageId.Replace("'","''")
Get-Message -Server $Server -Filter "InternetMessageId -eq '$SafeId'" -IncludeRecipientInfo -ResultSize Unlimited -ErrorAction Stop |
Format-List Identity,InternetMessageId,Status,DateReceived,ExpirationTime,
FromAddress,Recipients,LastError,RetryCount
В фильтре используется InternetMessageId — имя свойства объекта очереди, а не параметр MessageId из команды поиска tracking. Это небольшое отличие важно, когда команды переносят из одной консоли в другую. Описание выборок есть в Get-Message, устройство очередей — в документации транспорта.
Если объект найден, сохраняем Identity, адресатов, LastError, время и следующую точку маршрута. Retry вместе с ошибкой DNS, TLS или отказом удалённого узла направляет дальнейшую проверку. Ошибка относится к наблюдаемой попытке, а не обязательно ко всему интервалу жизни письма. Чтобы утверждать, что попытки повторяются, нужны несколько наблюдений либо история.
Не выполняем Remove-Message, массовый Retry-Queue и пересоздание Mail.que ради диагностики. Эти действия меняют состояние и способны уничтожить важную часть картины. В нашем эпизоде точного сообщения на EX01 уже нет, а SEND сохранился. Это не доказательство конечной доставки; это основание идти к журналу передачи в GW01, не трогая другие письма.
Уровень 3. Восстанавливаем SMTP-сеанс до финального ответа
Tracking отвечает, какой переход зарегистрировал Exchange. Protocol log позволяет увидеть разговор с соседним узлом. В загруженном файле строки разных подключений перемешаны: читаем их по session-id и sequence-number, одновременно проверяя local-endpoint, remote-endpoint и connector-id. Но один SMTP-сеанс способен содержать несколько почтовых транзакций — номер сессии ещё не выбирает нужное письмо.
Сначала узнаём фактический путь журнала у нужного транспортного сервиса. Front End и Hub — разные участки. Для отправки EX01 к шлюзу нас интересует SMTP Send на реальном исходящем узле, а не автоматически первый каталог SmtpReceive. Поля и пути описаны в документации protocol logging.
Если журналирование отсутствовало, включение поможет только при следующем согласованном тесте. Внимание: Verbose создаёт дополнительные данные и нагрузку на хранение; журнал содержит адреса и сведения об инфраструктуре. Ограничьте изменение одним проверенным коннектором, зафиксируйте прежнее значение, проверьте место и правила хранения. Ниже сначала показан WhatIf; команду изменения выполняет администратор только после согласования, затем возвращает исходный уровень.
# Change only a reviewed, explicit on-premises Send connector.
# For an EdgeSync-subscribed Edge, manage the organization configuration.
$ConnectorName = 'To-Mail-Gateway'
$Saved = @(Get-SendConnector -Identity $ConnectorName -ErrorAction Stop)
if ($Saved.Count -ne 1 -or [string]$Saved[0].Name -ne $ConnectorName) {
throw 'The exact connector was not resolved.'
}
$Before = [string]$Saved[0].ProtocolLoggingLevel
# Record $Before in the change ticket BEFORE enabling logging.
$Saved[0] | Format-List Identity,SourceTransportServers,ProtocolLoggingLevel
Set-SendConnector -Identity $Saved[0].Identity -ProtocolLoggingLevel Verbose -WhatIf
# After approval, remove -WhatIf for this connector only.
# Send one harmless test; inspect the actual source server's protocol log.
# Rollback in this same session, using the captured original value:
# Set-SendConnector -Identity $Saved[0].Identity -ProtocolLoggingLevel $Before
# Get-SendConnector -Identity $Saved[0].Identity | Format-List Identity,ProtocolLoggingLevel
Проверяем появление реальных строк для выбранного коннектора, а не только значение Verbose. Для подписанного через EdgeSync Edge изменения согласуют с организационной конфигурацией, а не правят локально вслепую. Синтаксис и область настроек — Configure protocol logging.
# Run locally on the source Exchange server with access to its log directory.
# Select ONE copied log file around the incident; do not scan the whole disk.
$Path = 'C:\Incident\SEND2026102607-1.log'
if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) { throw 'Log file missing.' }
$Lines = @(Get-Content -LiteralPath $Path -ErrorAction Stop)
$Header = $Lines | Where-Object { $_ -like '#Fields:*' } | Select-Object -First 1
if (-not $Header) { throw 'Exchange CSV #Fields header missing.' }
$Fields = ($Header -replace '^#Fields:\s*','').Split(',')
$Events = @($Lines | Where-Object { $_ -and -not $_.StartsWith('#') } |
ConvertFrom-Csv -Header $Fields)
$Events | Group-Object 'session-id' | Select-Object Name,Count
# Replace this with a session identified by endpoints and the transaction window.
$SessionId = 'REPLACE_WITH_SESSION_ID'
$Events | Where-Object { $_.'session-id' -eq $SessionId } |
Sort-Object @{Expression={[long]$_.'sequence-number'}} |
Format-Table 'date-time','sequence-number',event,data,context -Wrap
Этот разборщик читает один скопированный Exchange CSV, не Postfix и не произвольный шлюз. Ищем нужные MAIL FROM, RCPT TO, завершение DATA и ответ другой стороны внутри подтверждённой транзакции. Тело письма protocol log не является обязанным сохранять; отсутствие полного Message-ID в нём не означает отсутствие письма. Связь устанавливаем по времени, узлам, контексту и доступным внутренним номерам.
Уровень 4. Шлюз принял письмо — что произошло после этого?
В учебном сеансе GW01 ответил успешным кодом после DATA. Теперь у нас есть сильное подтверждение передачи на шлюз, но ровно до шлюза. Следующий запрос его администратору содержит Message-ID, точный RCPT, UTC-окно, адрес EX01, адрес принимающего интерфейса и локальный номер, если шлюз вернул его в ответе.
Не предполагаем, что антиспам обязан немедленно передать принятую копию дальше. Он может поставить её в очередь анализа, удержать по политике или направить в карантин. Проверяем объект, решение движка, правило, время решения и последующее действие. «Вирусов нет» отвечает только на один вопрос; письмо могло попасть под иной механизм контроля. Зелёное состояние движка тоже не описывает судьбу конкретного объекта.
В нашей модели журнал GW01 связывает приём с объектом Q-1842 и помещением в карантин по политике вложений. Этот номер выдуман для схемы; форматы реальных идентификаторов зависят от продукта. Записи передачи следующему MX для finance нет в исследованном окне. Мы нашли остановку маршрута и можем объяснить пользователю причину, но ещё не имеем права написать «доставлено».
Выпуск из карантина — отдельное согласованное действие после проверки безопасности. Сохраняем исходное решение и идентификатор выпуска: продукт может создать новую копию с другой трассой. Если шлюз вместо карантина передал письмо внешнему серверу, просим подтверждение той транзакции и продолжаем цепочку на стороне получателя. Не подменяем эту проверку утверждением «внешний MX ответил на ping».

DELIVER есть, а письма не видно: меняем участок расследования
Вернёмся к внутреннему audit: допустим, сотрудник тоже заявил, что письма нет, хотя ветка заканчивается DELIVER. Это уже другой вопрос. Доставка в ящик не означает, что письмо обязано сейчас лежать во «Входящих», что клиент синхронизирован или что человек его прочитал.
С согласия владельца проверяем ящик через веб-клиент без узкого фильтра представления: нужный ли аккаунт открыт, не используется ли общий ящик, нет ли письма в другой папке, архиве или удалённых. Сравниваем с Outlook. Если веб-клиент видит письмо, а Outlook нет, транспорт перестаёт быть главным подозреваемым; исследуем клиентское представление и синхронизацию.
Далее — текущие правила и пересылка:
# Read-only inspection; requires the corresponding Exchange RBAC permissions.
$Mailbox = 'audit@example.com'
Get-Mailbox -Identity $Mailbox -ErrorAction Stop |
Format-List PrimarySmtpAddress,RecipientTypeDetails,Database,
ForwardingAddress,ForwardingSmtpAddress,DeliverToMailboxAndForward
Get-InboxRule -Mailbox $Mailbox -IncludeHidden -ErrorAction Stop |
Format-List Identity,Name,Enabled,Priority,Description,
MoveToFolder,DeleteMessage,RedirectTo,ForwardTo,
ForwardAsAttachmentTo,StopProcessingRules
# Do not disable/delete rules merely because they exist.
# Compare with the incident time and approved mailbox behavior.
Скрытое правило само по себе не доказательство атаки: среди них бывают системные механизмы. Подозрительное действие требует проверки автора, назначения и временного контекста. Также текущая конфигурация не доказывает, что ровно она действовала в момент инцидента. Для исторического вывода нужны доступные журналы аудита с подходящей областью и сроком хранения. Отсутствующий аудит задним числом не появится.
Свойства правил и IncludeHidden описаны в Get-InboxRule. Здесь мы только читаем: массовое отключение правил или удаление профиля клиента уничтожает состояние раньше, чем мы поняли его роль. Если найдена несанкционированная пересылка, это отдельная задача безопасности с сохранением следов и ограничением доступа, а не просто проблема удобства почты.
Сводим цепочку: где факт, где вывод, где пока нет данных
Теперь у учебного инцидента появляется проверяемое объяснение. Время в таблице условное, приведено в UTC; это не измеренные задержки Exchange. Таблица не заменяет исходные строки — она показывает, какие выводы из них допустимы.
| Время UTC | Узел | Получатель / действие | Вывод |
|---|---|---|---|
| 07:00:01 | EX01 | finance и audit: RECEIVE | Вход в транспорт подтверждён |
| 07:00:03 | EX02 | audit: DELIVER | Внутренняя ветка доставлена |
| 07:00:04 | EX01 → GW01 | finance: SEND, финальный 250 | Шлюз принял внешнюю ветку |
| 07:00:06 | GW01 | finance: quarantine Q-1842 | Дальнейшей SMTP-передачи нет |
| 07:25:00 | GW01 | Проверка администратором | Письмо найдено; выпуск ещё не выполнен |
Факт: внутренняя ветка имеет событие доставки. Факт: шлюз принял внешнюю ветку и связал её с карантином. Вывод: для finance остановка находится на политике GW01, а не на отправке из Outlook. Пока не выполнен разрешённый выпуск и не подтверждён дальнейший маршрут, внешняя доставка остаётся незавершённой.
Представим другой исход: у EX01 есть SEND, а шлюзовые журналы уже истекли. Тогда заключение короче: «подтверждена передача GW01; последующая судьба по доступным данным не установлена». Недостаточно данных. Это не слабость расследования, а точная граница его возможностей. Для следующего случая понадобится улучшение хранения, а не уверенность в невидимом результате.
Третья развилка: наш шлюз получил таймаут, а получатель нашёл входной приём. Нужно сопоставить его запись с нашей попыткой и проверить повторы, а не удалять «лишние» копии по теме. Четвёртая: карантин отсутствует, но downstream-очередь содержит точный объект с TLS-ошибкой. Тогда меняется рабочая гипотеза и следующий запрос; первоначальное подозрение на антиспам не должно переживать противоречащие факты.
Хорошая временная линия полезна именно тем, что допускает эти развилки. Она не построена так, чтобы любой результат подтверждал заранее выбранного виновника.
Сохраняем доказательства и передаём задачу следующей стороне
В конце поиска недостаточно приложить Format-Table: он предназначен для экрана и может обрезать поля. Сохраняем исходные объекты отдельно от компактной таблицы, явный список серверов, окно и ошибки запросов. Для короткой цепочки это занимает минуты, зато избавляет коллегу от повторного сбора уже истёкших журналов.
# Reuse $Rows and $Failures from the exact-ID search.
# Supply a NEW directory in an approved, access-controlled incident location.
$Out = 'C:\Incident\Mail-20261026-001'
if (Test-Path -LiteralPath $Out) { throw 'Use a new evidence directory.' }
$null = New-Item -ItemType Directory -Path $Out -ErrorAction Stop
$Rows | Export-Clixml -LiteralPath (Join-Path $Out 'tracking-raw.xml') -Depth 8
$Rows | Sort-Object Timestamp,QueriedServer |
Select-Object Timestamp,QueriedServer,EventId,Source,MessageId,
NetworkMessageId,InternalMessageId,ConnectorId,
@{Name='Recipients';Expression={$_.Recipients -join ';'}},
@{Name='RecipientStatus';Expression={$_.RecipientStatus -join ';'}},
@{Name='Reference';Expression={$_.Reference -join ';'}} |
Export-Csv -LiteralPath (Join-Path $Out 'timeline.csv') -NoTypeInformation -Encoding UTF8
$Failures | Export-Clixml -LiteralPath (Join-Path $Out 'query-failures.xml')
[pscustomobject]@{CollectedAt=(Get-Date -Format o);TimeZone=(Get-TimeZone).Id;Start=$Start;End=$End;Servers=$Servers} |
ConvertTo-Json -Depth 4 | Set-Content -LiteralPath (Join-Path $Out 'scope.json') -Encoding UTF8
Get-ChildItem -LiteralPath $Out -File | Get-FileHash -Algorithm SHA256 |
Export-Csv -LiteralPath (Join-Path $Out 'sha256.csv') -NoTypeInformation -Encoding UTF8
Папка в примере должна находиться в заранее согласованном месте с ограниченными ACL. New-Item не делает её защищённой автоматически. Выгрузки содержат адреса и внутренние имена; не прикладывайте их без очистки к открытому обращению или комментарию. SHA256 помогает заметить изменение копии после сбора, но не доказывает подлинность исходной системы и не заменяет процесс хранения доказательств.
Сырые protocol logs и записи шлюза сохраняем отдельно с их именами, часовым поясом, узлом-источником и границами охвата. CSV в блоке упрощает некоторые массивы для чтения; XML оставляет больше исходной структуры. Его тоже нельзя считать полным архивом почтовой системы. Если экспорт открывают в таблицах, импортируйте недоверенные текстовые поля как текст, а не исполняемые формулы.
Хороший запрос другой стороне звучит так: «Для finance@example.net письмо с таким Message-ID принято GW01 от EX01 в указанное UTC-время. Нужны локальный ID, решение фильтрации и следующий SMTP-исход. Наша подтверждённая граница — финальный успешный ответ GW01». Не нужны тело договора, пароли и полная выгрузка всех писем организации. Нужны ровно данные для следующего перехода.
Проверка без риска: одно тестовое письмо, несколько независимых исходов
Расследование можно подготовить до настоящей потери договора. Выбираем согласованные тестовые ящики, создаём безвредное сообщение с уникальной меткой в теме и отправляем штатным разрешённым клиентом. Сохраняем настоящий Message-ID из получившегося оригинала. Уникальная тема помогает найти объект, но не заменяет идентификатор.
- Проверяем внутренний маршрут: какие серверы видны в tracking, какой получатель завершился доставкой, совпадает ли временной контекст с сырым журналом.
- Проверяем разрешённую внешнюю отправку: сопоставляем наш SEND, ответ шлюза, дальнейший приём и наличие в тестовом ящике.
- Если продукт поддерживает безопасную тестовую политику карантина, применяем её только к согласованному тестовому отправителю/получателю. Не отправляем вредоносное вложение ради «реалистичности».
- Для двух адресатов фиксируем два исхода. Если у одного есть DELIVER, второй не должен автоматически получить статус «успешно».
- Возвращаем временные настройки logging и тестовую политику по зафиксированному плану. Проверяем фактическое состояние после отката и сохраняем результат.
| Испытание | Ожидаемое доказательство | Недостаточный результат |
|---|---|---|
| Внутренний тест | Ветка получателя заканчивается DELIVER | Есть только SUBMIT |
| Внешний разрешённый тест | SEND + приём следующей стороны + доступность в тестовом ящике | Есть только 250 шлюза |
| Разрешённая карантинная проба | ID объекта + причина + ветка получателя | Служба антиспама Running |
| Письмо двум адресатам | Отдельный исход для каждого RCPT | Один SEND объявлен общей доставкой |
| Разрыв журналирования | Пробел отмечен, вывод ограничен | Пустой поиск назван доказательством отсутствия |
Я не предлагаю тестировать повторную доставку настоящим платёжным документом, останавливать транспорт или выпускать подозрительные объекты всем адресатам. Здесь достаточно безопасных сообщений и ограниченных правил. Команды статьи сверены по документации; выполнения в вашей Exchange-среде это не заменяет. Перед рабочим применением проверяем RBAC, версию, временной контекст и нагрузку поиска на пилотном окне.
Если тест прошёл, результатом становится небольшой регламент: кто хранит какой журнал, как получить исходник, как связать шлюзовой ID с Exchange и кто отвечает за следующий участок. Такой регламент ценнее очередной общей проверки, что порт 25 открыт.
Что должно остаться после расследования
Мы прошли от пользовательского «отправлено» до подтверждённого перехода между узлами, разделили судьбы адресатов и дошли до карантина либо конечного ящика. Главное здесь — дисциплина формулировок. Отсутствие письма в клиенте, пустая очередь и успешная SMTP-передача отвечают на разные вопросы.
Ключевая мысль: у каждого подтверждения есть граница, и расследование продвигается только тогда, когда следующий источник продолжает эту же цепочку. Если граница закончилась на шлюзе, не называем её ящиком. Если доказательств после шлюза нет, фиксируем пробел. Если ящик получил письмо, переносим проверку к его дальнейшей обработке.
Спасибо, что дочитали. Надеюсь, этот порядок поможет закрывать почтовые инциденты объяснением, которое можно проверить, а не договорённостью между уставшими администраторами. Когда сроки хранения и идентификаторы согласованы заранее, даже сложная цепочка из нескольких продуктов становится управляемой.
- На какой границе вашей почтовой системы чаще всего пропадает наблюдаемость: клиент, шлюз, облачный участок или действия после доставки?
- Как вы связываете объекты после карантинного выпуска, перенаправления и пересоздания письма сторонним фильтром?
- Можете ли вы сегодня отличить «письмо не принимали» от «нужный журнал уже удалён» и чем подтверждаете ответ?
Поделитесь в комментариях сценариями без конфиденциальных заголовков и адресов. Отдельного разбора заслуживает случай, когда Exchange намеренно перестаёт принимать почту из-за состояния ресурсов: сервер запущен, но транспорт защищает себя. Это уже другое расследование — с back pressure и ценой отключения его защиты.



