Статья размещена автором Бетке Сергей Сергеевич

Автоматизация контроля MS Exchange Server с Power Shell

Предпосылки

Учитывая возросшую в последнее время важность для бизнеса электронной почты, возникла необходимость в её постоянном контроле. А контролировать руками — не наш метод. И возникла идея автоматизации… Речь пойдёт об анализе логов SMTP сервера средствами powershell.

Скриптом PowerShell через LogParser будем обрабатывать по расписанию логи SMTP сервера, анализировать их. По результатам анализа высылать письма (отчёты) на postmaster@ для принятия дальнейших мер администраторами.

Конкретнее предстоит решить следующие задачи:

  • Проверяю в исходящих сессиях временные отказы (4ХХ) и постоянные отказы (5ХХ). По каждому из них необходим анализ и письма:
    • о фактах грейлистинга готовить письмо на postmaster@ домена, использующего грейлистинг, копия — своим администраторам
    • Если отказ 502 на ehlo — добавить домен в исключения, работать далее с ним только через SMTP (не ESMTP), о чём необходимо информировать администраторов.
  • Также необходим контроль и за входящей почтой от некоторых доменов (по списку адресов, размещённому в отдельном файле). При любом отказе (временном или постоянном) с нашей стороны — полный анализ и письмо своим администраторам.

LogParser

Качаем с сайта MS. Для начала потребовалась обёртка вокруг LogParser для его "удобного" использования в power shell:

function Get-LPInputFormat{
<#
  .Synopsis
      Возвращает объект - парсер журнала заданного типа.
  .Description
      Возвращает объект - парсер журнала заданного типа.
  .Parameter inputType
      Тип журнала, заданный строкой.
  .Example
            Поиск временных ошибок в журнале SMTP:
            Invoke-LPExecute ('
                "SELECT time, c-ip, cs-username, cs-method, cs-uri-stem, cs-uri-query, sc-status" + `
                " FROM $lastLogName" +`
                " WHERE cs-username='OutboundConnectionResponse' AND cs-uri-query LIKE '4%'", `
                Get-LPInputFormat("w3c") `
            )
#>
  param (
  [Parameter(
   Mandatory=$true,
   Position=0,
   ValueFromPipeline=$false,
   HelpMessage="Тип журнала, заданный строкой."
  )]
        [string]$inputType
  )
 switch($inputType.ToLower()) {
  "ads"      {$inputobj = New-Object -comObject MSUtil.LogQuery.ADSInputFormat}
  "bin"      {$inputobj = New-Object -comObject MSUtil.LogQuery.IISBINInputFormat}
  "csv"      {$inputobj = New-Object -comObject MSUtil.LogQuery.CSVInputFormat}
  "etw"      {$inputobj = New-Object -comObject MSUtil.LogQuery.ETWInputFormat}
  "evt"      {$inputobj = New-Object -comObject MSUtil.LogQuery.EventLogInputFormat}
  "fs"       {$inputobj = New-Object -comObject MSUtil.LogQuery.FileSystemInputFormat}
  "httperr"  {$inputobj = New-Object -comObject MSUtil.LogQuery.HttpErrorInputFormat}
  "iis"      {$inputobj = New-Object -comObject MSUtil.LogQuery.IISIISInputFormat}
  "iisodbc"  {$inputobj = New-Object -comObject MSUtil.LogQuery.IISODBCInputFormat}
  "ncsa"     {$inputobj = New-Object -comObject MSUtil.LogQuery.IISNCSAInputFormat}
  "netmon"   {$inputobj = New-Object -comObject MSUtil.LogQuery.NetMonInputFormat}
  "reg"      {$inputobj = New-Object -comObject MSUtil.LogQuery.RegistryInputFormat}
  "textline" {$inputobj = New-Object -comObject MSUtil.LogQuery.TextLineInputFormat}
  "textword" {$inputobj = New-Object -comObject MSUtil.LogQuery.TextWordInputFormat}
  "tsv"      {$inputobj = New-Object -comObject MSUtil.LogQuery.TSVInputFormat}
  "urlscan"  {$inputobj = New-Object -comObject MSUtil.LogQuery.URLScanLogInputFormat}
  "w3c"      {$inputobj = New-Object -comObject MSUtil.LogQuery.W3CInputFormat}
  "xml"      {$inputobj = New-Object -comObject MSUtil.LogQuery.XMLInputFormat}
  }
  return $inputobj
}

function Invoke-LPExecute {
 <#
  .Synopsis
      Исполняет запрос через средство LogParser.
  .Description
      Представляет командлет для исполнения запросов через Log Parser. Возвращает объект RecordSet.
  .Parameter query
      Запрос в SQL синтаксисе Log Parser
  .Parameter inputType
   Интерфейс типизированного парсера для обрабатываемого журнала (создаваемый через Get-LPInputFormat).
            Если параметр не указан, тип журнала определяется Log Parser автоматически.
  .Example
            Поиск временных ошибок в журнале SMTP:
            Invoke-LPExecute ('
                "SELECT time, c-ip, cs-username, cs-method, cs-uri-stem, cs-uri-query, sc-status" + `
                " FROM $lastLogName" +`
                " WHERE cs-username='OutboundConnectionResponse' AND cs-uri-query LIKE '4%'" `
            )
    #>
param (
  [Parameter(
   Mandatory=$true,
   Position=0,
   ValueFromPipeline=$false,
   HelpMessage="Запрос в SQL синтаксисе Log Parser."
  )]
        [string]$query,
  [Parameter(
   Mandatory=$false,
   Position=1,
   ValueFromPipeline=$false,
   HelpMessage="Интерфейс типизированного парсера для обрабатываемого журнала."
  )]
        $inputType
  )
 $LPQuery = new-object -com MSUtil.LogQuery
 if($inputType) {
     $LPRecordSet = $LPQuery.Execute($query, $inputType) 
 } else {
  $LPRecordSet = $LPQuery.Execute($query)
 }
    return $LPRecordSet
}

function Get-LPRecord {
<#
  .Synopsis
      Возвращает PowerShell custom object из текущей записи Log Parser recordset.
  .Description
      Возвращает PowerShell custom object из текущей записи Log Parser recordset.
  .Parameter LPRecordSet
      RecordSet, результат Invoke-LPExecute
#>
param (
  [Parameter(
   Mandatory=$true,
   Position=0,
   ValueFromPipeline=$false,
   HelpMessage="RecordSet, результат Invoke-LPExecute."
  )]
        $LPRecordSet
)
 $LPRecord = new-Object System.Management.Automation.PSObject
 if (-not $LPRecordSet.atEnd()) {
  $Record = $LPRecordSet.getRecord()
  for ([int]$i = 0; $i -lt $LPRecordSet.getColumnCount(); $i++) {        
   $LPRecord | add-member `
                -memberType NoteProperty `
                -name $LPRecordSet.getColumnName($i) `
                -value $Record.getValue($i)
  }
 }
 return $LPRecord
}
 
function Get-LPRecordSet {
<#
  .Synopsis
      Исполняет запрос через средство LogParser.
  .Description
      Представляет командлет для исполнения запросов через Log Parser. Но возвращает уже массив объектов PS.
  .Parameter query
      Запрос в SQL синтаксисе Log Parser
  .Parameter inputType
   Интерфейс типизированного парсера для обрабатываемого журнала (создаваемый через Get-LPInputFormat).
            Если параметр не указан, тип журнала определяется Log Parser автоматически.
  .Example
            Поиск временных ошибок в журнале SMTP:
            Get-LPRecordSet ('
                "SELECT time, c-ip, cs-username, cs-method, cs-uri-stem, cs-uri-query, sc-status" + `
                " FROM $lastLogName" +`
                " WHERE cs-username='OutboundConnectionResponse' AND cs-uri-query LIKE '4%'" `
            )
#>
param (
  [Parameter(
   Mandatory=$true,
   Position=0,
   ValueFromPipeline=$false,
   HelpMessage="Запрос в SQL синтаксисе Log Parser."
  )]
        [string]$query,
  [Parameter(
   Mandatory=$false,
   Position=1,
   ValueFromPipeline=$false,
   HelpMessage="Интерфейс типизированного парсера для обрабатываемого журнала."
  )]
        $inputType
)
 if($inputType) {
        $LPRecordSet = Invoke-LPExecute $query, $inputType
 } else {
        $LPRecordSet = Invoke-LPExecute $query
 }
 $LPRecords = new-object System.Management.Automation.PSObject[] 0
 for(; -not $LPRecordSet.atEnd(); $LPRecordSet.moveNext()) {
  $LPRecords += Get-LPRecord($LPRecordSet)
 }
 $LPRecordSet.Close()
 return $LPRecords
}

function Get-LPTableResults {
<#
  .Synopsis
      Исполняет запрос через средство LogParser и возвращает таблицу типизированных данных.
  .Description
      Представляет командлет для исполнения запросов через Log Parser. Но возвращает уже таблицу типизированных данных PS.
  .Parameter query
      Запрос в SQL синтаксисе Log Parser
  .Parameter inputType
   Интерфейс типизированного парсера для обрабатываемого журнала (создаваемый через Get-LPInputFormat).
            Если параметр не указан, тип журнала определяется Log Parser автоматически.
  .Example
            Поиск временных ошибок в журнале SMTP:
            Get-LPTableResults ('
                "SELECT time, c-ip, cs-username, cs-method, cs-uri-stem, cs-uri-query, sc-status" + `
                " FROM $lastLogName" +`
                " WHERE cs-username='OutboundConnectionResponse' AND cs-uri-query LIKE '4%'" `
            )
#>  param (
  [Parameter(
   Mandatory=$true,
   Position=0,
   ValueFromPipeline=$false,
   HelpMessage="Запрос в SQL синтаксисе Log Parser."
  )]
        [string]$query,
  [Parameter(
   Mandatory=$false,
   Position=1,
   ValueFromPipeline=$false,
   HelpMessage="Интерфейс типизированного парсера для обрабатываемого журнала."
  )]
        $inputType
)
 if($inputType) {
        $LPRecordSet = Invoke-LPExecute $query, $inputType
 } else {
        $LPRecordSet = Invoke-LPExecute $query
 }
    $tab = new-object System.Data.DataTable("Results")
    for($i = 0; $i -lt $LPRecordSet.getColumnCount(); $i++) {
        $col = new-object System.Data.DataColumn
        $ct = $LPRecordSet.getColumnType($i)
        $col.ColumnName = $LPRecordSet.getColumnName($i)
        switch ($LPRecordSet.getColumnType($i)) {
            "1"     { $col.DataType = [System.Type]::GetType("System.Int32") }
            "2"     { $col.DataType = [System.Type]::GetType("System.Double") }
            "4"     { $col.DataType = [System.Type]::GetType("System.DateTime") }
            default { $col.DataType = [System.Type]::GetType("System.String") }
        }
        $tab.Columns.Add($col)
    }
    for(; -not $LPRecordSet.atEnd(); $LPRecordSet.moveNext()) {
        $rowLP = $LPRecordSet.getRecord()
        $row = $tab.NewRow()
        for ($i = 0; ($i -lt $LPRecordSet.getColumnCount()); $i++) {
            $columnName = $rsLP.getColumnName($i)
            $row.$columnName = $rowLP.getValue($i)
        }
        $tab.Rows.Add($row)
    }
    $ds = new-object System.Data.DataSet
    $ds.Tables.Add($tab)
    return $ds.Tables["Results"]
}

Export-ModuleMember `
    "Get-LPInputFormat", `
    "Invoke-LPExecute", `
    "Get-LPRecord", `
    "Get-LPRecordSet", `
    "Get-LPTableResults"

На авторство не претендую никоим образом, по "LogParser + powershell" найдёте ещё и не то.

Анализируем журнал SMTP

А теперь непосредственно к нашему скрипту.

# включаем обработку и регистрацию ошибок в журнале
$ErrorActionPreference = "Stop" # генерировать исключение в случае ошибки
trap {
    $evt=new-object System.Diagnostics.EventLog("Application")
    $evt.Source=$myinvocation.mycommand.name
    $evt.WriteEntry(`
        $_.Exception.Message,`
        [System.Diagnostics.EventLogEntryType]::Error `
    )
    write-debug $_.Exception.Message
}ImportSystemModules 
Import-Module "c:\temp\itgLogParser.psm1"[string]$fullTimeFormat = "dd.MM.yyyy HH:mm:ss"
# диапазон времени, в пределах которого будем искать записи одной сессии
[System.TimeSpan]$logTimeRangeForSession = New-TimeSpan -minutes 1# почтовый сервер, через который будем осуществлять отправку почты, адреса
[Net.Mail.SmtpClient]$SMTPclient = new-object Net.Mail.SmtpClient("localhost")
[string]$emailFrom = postmaster@mydomain
[string]$emailTo   = postmaster@mydomain# дата, за которую будем проводить анализ
[System.DateTime]$logDate = (Get-Date).AddDays(-1)# суффикс файла с перечнем контрольных доменов
[string]$controlDomainsFileSuffix = " - контрольные домены.txt"
# вычислим файл контрольных доменов
$scriptPath = $myinvocation.mycommand.path
if (-not $scriptPath) { $scriptPath = "c:\temp\itgCheckMailFlow.ps1" }
[string]$controlDomainsFilePath = [System.IO.Path]::Combine( `
    [System.IO.Path]::GetDirectoryName($scriptPath), `
    [System.IO.Path]::GetFileNameWithoutExtension($scriptPath) + $controlDomainsFileSuffix `
)# получим расположение каталога журналов smtp и "вычислим" имя файла журнала
$SMTPServiceIndex = 1
$SMTPServerSettings = Get-WmiObject `
    -namespace "root\MicrosoftIISv2" `
    -class IIsSmtpServerSetting `
    -filter ("Name='SmtpSvc/" + $SMTPServiceIndex + "'")
    # период ведения журнала должен быть установлен в 1 день. именно на это мы рассчитываем.
# поэтому если настройки будут иные - будем ругаться
if ($SMTPServerSettings.LogFilePeriod -ne 1) {
    throw new-object System.ApplicationException("Период ведения журнала SMTP сервера должен быть установлен в 1 день!")
}
# при определении имени файла журнала нам необходима информация о типе журнала
[string]$logPrefix = "ex"
[string]$logSuffix = ".log"
$logName = "$($SMTPServerSettings.LogFileDirectory)\SMTPSVC${SMTPServiceIndex}\${LogPrefix}$($logDate.ToString("yyMMdd"))$LogSuffix"# ============================================================================================================
# общие функции, осуществляющие поиск проблем по запросу, затем формирующие необходимый пакет информации на
# каждую проблему для последующего анализа и формирования отчёта
# в журналах IIS поля LogFilename, LogRow, UserIP, UserName, Date, Time, ServiceInstance, HostName, 
# ServerIP, TimeTaken, BytesSent, BytesReceived, StatusCode, Win32StatusCode, RequestType, Target, Parameters
# в журнале w3c (который используем) поля time c-ip cs-username cs-method cs-uri-query sc-status
# ============================================================================================================
function Get-ProblemRecords {
 <#
  .Synopsis
      Выполняем заданный запрос для поиска проблем и возвращает массив PS с записями о проблемах и 
            сопутсвтующей информацией.
  .Description
      Выполняем заданный запрос (полученный с использованием $queryWhere) через LogParser для поиска
            проблем и возвращает массив PS с записями о проблемах и сопутсвтующей информацией, как то:
                - запись из журнала, содержающая проблему
                - выдержка из журнала по каждой проблеме
  .Parameter queryWhere
      Часть запроса (Where секция) в SQL синтаксисе Log Parser для поиска проблемы
  .Parameter logName
      Путь к файлу журнала, по которому будет выполнен запрос
 #>
   param (
  [Parameter(
   Mandatory=$true,
   Position=0,
   ValueFromPipeline=$false,
   HelpMessage="Запрос в SQL синтаксисе Log Parser."
  )]
        [string]$queryWhere,
  [Parameter(
   Mandatory=$true,
   Position=1,
   ValueFromPipeline=$false,
   HelpMessage="Путь к файлу журнала."
  )]
        [string]$logName
 )
    # запрос к журналу
    $mailErrorsRecords = Get-LPRecordSet (
@"
        SELECT
            TO_TIMESTAMP(TIMESTAMP('$($logDate.ToString("dd.MM.yyyy"))', 'dd.MM.yyyy'), time) AS FullTime, 
            c-ip AS UserIP, 
            cs-username AS Host, 
            cs-username AS Direction, 
            cs-method AS RequestType, 
            cs-uri-query AS Response,
            cs-uri-query AS RequestParams,
            sc-status AS Status
        FROM $logName
        WHERE $queryWhere
"@
    )
    [int]$mailErrorsCount = 0
    if ($mailErrorsRecords) {
        if ($mailErrorsRecords.length) {
            $mailErrorsCount = $mailErrorsRecords.length
        } else {
            $mailErrorsCount = 1
        }
    }
    # получен перечень проблем, теперь по каждой проблеме получим сессию
    $result = new-Object System.Management.Automation.PSObject
    $result | add-member `
        -memberType NoteProperty `
        -name factsCount `
        -value $mailErrorsCount
    if ($mailErrorsCount) { $facts = $mailErrorsRecords | % {
        # начнём формирование выходного массива
        $problemObj = new-Object System.Management.Automation.PSObject
    $problemObj | add-member `
            -memberType NoteProperty `
            -name fact `
            -value $_
    if ($_.Direction -like 'OutboundConnection*') {
       $problemObj | add-member `
                -memberType NoteProperty `
                -name direction `
                -value "Outbound"
        } else {
       $problemObj | add-member `
                -memberType NoteProperty `
                -name direction `
                -value "Inbound"
        }
        $sessionLog = Get-LPRecordSet (
@"
            SELECT
                TO_TIMESTAMP(TIMESTAMP('$($logDate.ToString("dd.MM.yyyy"))', 'dd.MM.yyyy'), time) AS FullTime, 
                c-ip AS UserIP, 
                cs-username AS Host, 
                cs-username AS Direction, 
                cs-method AS RequestType, 
                cs-uri-query AS Response,
                cs-uri-query AS RequestParams,
                sc-status AS Status
            FROM $logName
            WHERE
                (
                    (FullTime > TIMESTAMP('$(($_.FullTime - $logTimeRangeForSession).ToString($fullTimeFormat))', '$fullTimeFormat'))
                    AND (FullTime < TIMESTAMP('$(($_.FullTime + $logTimeRangeForSession).ToString($fullTimeFormat))', '$fullTimeFormat'))
                )
                AND (UserIP LIKE '$($_.UserIP)')
"@
        )
        # имеем журнал одной сессии.
        # включаем его в результирующий объект
   $problemObj | add-member `
            -memberType NoteProperty `
            -name sessionLog `
            -value $sessionLog
                
        # теперь также выделим адрес "виновника" контагента, его домен, сервер, с которым была попытка установить связь,
        # его ответ и IP
        $problemObj | add-member `
            -memberType NoteProperty `
            -name UserIP `
            -value $_.UserIP
        switch ($problemObj.direction) {
            "Inbound"  {$headerStr = ($sessionLog | where { $_.RequestType -match "^(?:EHLO|HELO)$" } | select -first 1).RequestParams}
            "Outbound" {$headerStr = ($sessionLog | where { $_.Response -match "^220" } | select -first 1).Response}
        }
        $userHeader = if ( `
            $headerStr `
            -match "((?:[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?\.)+(?:aero|asia|biz|cat|com|coop|edu|gov|info|int|jobs|mil|mobi|museum|name|net|org|pro|tel|travel|[a-zA-Z]{2}))" `
        ) { $matches[0] } 
        else { "" }
        $problemObj | add-member `
            -memberType NoteProperty `
            -name UserHeader `
            -value $userHeader
        $addrStr = ($sessionLog | where { $_.RequestType -eq "RCPT" } | select -first 1).Response
        if ($addrStr -match "([a-zA-Z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-zA-Z0-9!#$%&'*+/=?^_`{|}~-]+)*)@((?:[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?\.)+(?:aero|asia|biz|cat|com|coop|edu|gov|info|int|jobs|mil|mobi|museum|name|net|org|pro|tel|travel|[a-zA-Z]{2}))") {
            $emailAddr = $matches[0] 
            $emailLName = $matches[1]
            $emailDomain = $matches[2]
        } else {
            $emailAddr = ""
            $emailLName = ""
            $emailDomain = ""
        }
        $problemObj | add-member -memberType NoteProperty -name ToAddr -value $emailAddr
        $problemObj | add-member -memberType NoteProperty -name ToDomain -value $emailDomain
  $problemObj | add-member -memberType NoteProperty -name ToLName -value $emailLName
        # определим адрес отправителя
        $addrStr = ($sessionLog | where { $_.RequestType -eq "MAIL" } | select -first 1).Response
        if ($addrStr -match "([a-zA-Z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-zA-Z0-9!#$%&'*+/=?^_`{|}~-]+)*)@((?:[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?\.)+(?:aero|asia|biz|cat|com|coop|edu|gov|info|int|jobs|mil|mobi|museum|name|net|org|pro|tel|travel|[a-zA-Z]{2}))") {
            $emailAddr = $matches[0] 
            $emailLName = $matches[1]
            $emailDomain = $matches[2]
        } else {
            $emailAddr = ""
            $emailLName = ""
            $emailDomain = ""
        }
        $problemObj | add-member -memberType NoteProperty -name FromAddr -value $emailAddr
        $problemObj | add-member -memberType NoteProperty -name FromDomain -value $emailDomain
   $problemObj | add-member -memberType NoteProperty -name FromLName -value $emailLName
        # отформатируем фрагмент журнала так, как нам требуется, и поместим его в переменную
        [string]$logPart = `
@"
===============================================================================
проблема:
"@
        $format = switch ($problemObj.direction) {
            Inbound {
                @{Label="time"; Expression={$_.FullTime.ToString($fullTimeFormat)}; width=20}, `
                @{Label="status"; Expression={$_.Status}; width=4}, `
                @{Label="content"; Expression={"$($_.RequestType) $($_.Response)"}; width=80}
            }
            Outbound {
                @{Label="time"; Expression={$_.FullTime.ToString($fullTimeFormat)}; width=20}, `
                @{Label="content"; Expression={"$($_.RequestType) $($_.Response)"}; width=80}
            }
        }
        $logPart += $_ | format-table `
            -property $format `
            -wrap `
            -hideTableHeaders `
            | out-string
        $logPart += 
@"
    server IP: $($problemObj.UserIP), server header: $($problemObj.UserHeader)
    To addr: $($problemObj.ToAddr), To domain: $($problemObj.ToDomain), To lname: $($problemObj.ToLName)
    From addr: $($problemObj.FromAddr), From domain: $($problemObj.FromDomain), From lname: $($problemObj.FromLName)"@
        $format = switch ($problemObj.direction) {
            Inbound {
                @{Label="time"; Expression={$_.FullTime.ToString($fullTimeFormat)}; width=20}, `
                @{Label="dest. IP"; Expression={$_.UserIP}; width=16}, `
                @{Label="status"; Expression={$_.Status}; width=6}, `
                @{Label="content"; Expression={"$($_.RequestType) $($_.Response)"}; width=70}
            }
            Outbound {
                @{Label="time"; Expression={$_.FullTime.ToString($fullTimeFormat)}; width=20}, `
                @{Label="dest. IP"; Expression={$_.UserIP}; width=16}, `
                @{Label="content"; Expression={"$($_.RequestType) $($_.Response)"}; width=80}
            }
        }
        $logPart += $sessionLog | format-table `
            -property $format `
            -wrap `
            | out-string
        $logPart += `
@"
===============================================================================
"@
   $problemObj | add-member `
            -memberType NoteProperty `
            -name sessionLogFormatted `
            -value $logPart
        # возвращаем сформированный объект
        $problemObj
    } }
    if ($facts) {
        $result | add-member `
            -memberType NoteProperty `
            -name facts `
            -value $facts
    }
    $result
}# ============================================================================================================
# ============================================================================================================
# ============================================================================================================
# подготовим и направим отчёт по всем проблемным исходящим сессиям проверяемого журнала
$outputMailErrors = Get-ProblemRecords `
    -queryWhere `
@"
    (Direction = 'OutboundConnectionResponse') AND (
        (Response LIKE '5__%')
        OR (Response LIKE '4__%')
    )
"@ `
    -logName $logName
# отправляем письмо администраторам
if ($outputMailErrors.factsCount) {
    $SMTPclient.Send( `
        $emailFrom, `
        $emailTo, `
        "Обнаружены проблемы в исходящем SMTP трафике ($($outputMailErrors.factsCount))", `
@"
При проверке журналов SMTP сервера обнаружены потенциально проблемные SMTP сессии.

Проблемы обнаружены для следующих доменов и адресов:
$($outputMailErrors.facts | % {$_.ToDomain} | where {$_ -ne ""} | select -unique | %{
    $domain = $_
    "`n" + $domain
    $outputMailErrors.facts | where {$_.ToDomain -eq $domain} |% {$_.ToAddr} | where {$_ -ne ""} | select -unique | %{"`n`t"+$_}
})
Выдержка из журнала SMTP сервера:
$($outputMailErrors.facts | % { $_.sessionLogFormatted })
С Уважением,
$emailFrom.
Скрипт: $($myinvocation.mycommand.path)
"@ `
)
}# ============================================================================================================
# ============================================================================================================
# ============================================================================================================
# подготовим и направим отчёт по отказам на исходящие ESMTP сессии (EHLO)
if ($outputMailErrors.facts) {
    $ESMTPErrors = $outputMailErrors.facts | where {
        $_.fact | where { 
            ($_.Response -like "500*") `
            -or ($_.Response -like "502*")
        }
    }
$SMTPclient.Send( `
    $emailFrom, `
    $emailTo, `
    "Обнаружены домены, не поддерживающие ESMTP", `
    @"
При проверке журналов SMTP сервера обнаружены ESMTP сессии, в рамках которых получен отказ на EHLO.
Следует включить данные домены в перечень доменов, обслуживаемых исключительно по SMTP - в SMTP connector
"SMTP, internet, user messages".
Обнаружены домены:
$($ESMTPErrors | % {$_.ToDomain} | select -unique | % {$_ + "`n"})
Выдержка из журнала SMTP сервера:
$($ESMTPErrors | % {$_.sessionLogFormatted})
P.S.>
Скрипт: $($myinvocation.mycommand.path)
С Уважением,
$emailFrom.
Cкрипт: $($myinvocation.mycommand.path)
@ `
)
}# ============================================================================================================ # ============================================================================================================ # ============================================================================================================ # по постоянным ошибкам 5ХХ отправляем письмо не только администратору, но и пользователю, # с почтой которого были проблемы
if ($outputMailErrors.facts) {     $outputUsersMailErrors = $outputMailErrors.facts | where {         $_.fact | where {             ($_.Response -like "5*") `         }     }
# отправляем письмо пользователям, копию - админам     $outputUsersMailErrors | %{$_.FromAddr} | select -unique | % {         $senderAddr = $_
        $SMTPclient.Send( `             $emailFrom, `             "<$emailTo>,<$senderAddr>", `             "Обнаружены проблемы при доставке Вашей почты за $($logDate.ToString('dd.MM.yyyy'))", ` @" Уважаемый $senderAddr.
Данное письмо сформировано автоматически сервисом проверки почтового трафика $($myinvocation.mycommand.name). При проверке журналов почтового сервера за $($logDate.ToString('dd.MM.yyyy')) обнаружены возможные проблемы при доставке Вашей почты следующим контрагентам:
$($outputUsersMailErrors | where {$_.FromAddr -eq $senderAddr} | % {$_.ToAddr} | select -unique | % { $_ + "`n"})
Данная информация также передана в отдел ИТ для анализа и принятия предупреждающих мер. С целью уточнения факта доставки указанных почтовых сообщений следует обратиться в отдел ИТ.
Выдержка из журнала SMTP сервера (время - по Гринвичу):
$($outputUsersMailErrors | where {$_.FromAddr -eq $senderAddr} | % { $_.sessionLogFormatted })
С Уважением, $emailFrom.
Скрипт: $($myinvocation.mycommand.path)
"@ `
)
}
}# ============================================================================================================
# ============================================================================================================
# ============================================================================================================
# попробуем проанализировать факты грейлистинга и по ним выслать письмо администратору
if ($outputMailErrors.facts) {
    $graylistingErrors = $outputMailErrors.facts | where {
        ($_.ToDomain -ne "") `
        -and ($_.fact | where { `
            ($_.Response -like "4*") `
        }) `
    }
    # отправляем письмо админам
    $graylistingErrors | %{$_.ToDomain} | select -unique | % {
        $toDomain = $_
        $SMTPclient.Send( `
            $emailFrom, `
            $emailTo, `
            "Грейлистинг $toDomain", `
@"
Добрый день, postmaster@$($toDomain).
Мы используем Вашу почтовую систему для работы с нашими клиентами.
Обнаружили использование грейлистинга $($logDate.ToString('dd.MM.yyyy')), время - GMT+0:
$($graylistingErrors | where {$_.ToDomain -eq $toDomain} | % { $_.sessionLogFormatted })
Зачастую, нам необходимо доставлять почту в кратчайшие сроки.
В связи с чем прошу Вас рассмотреть возможность включить наш MTA в белый список в обход грейлистинга: мы - это novgaro.ru, в dns имеется spf запись, по которой можно включить отправителя - наш MTA: mail.novgaro.ru. [213.148.164.198].
P.S. Мы выполняем все требования по борьбе со спамом, репутация отправителя - 

http://www.senderbase.org/senderbase_queries/detailip?search_string=

Также наш MTA фигурирует в ряде белых списков: http://multirbl.valli.org/lookup/ Уважением,
С
$emailFrom.
Скрипт: $($myinvocation.mycommand.path)
"@ `
)
}
}# ============================================================================================================ # ============================================================================================================ # ============================================================================================================ # проанализируем входящую почту. найдём все проблемы для "контрольных" доменов # алгоритм следующий. отказ может поступить на любом этапе сессии. поэтому сначала предлагаю осуществить # поиск host header через поиск всех предложений MAIL для контрольных адресов. затем снова делаем запрос # уже фильтруя по host header и статусам с ошибками (4XX и 5XX). таким образом мы уже и получим проблемные # сессии для контрольных адресов # загрузим контрольный список шаблонов адресов $controlDomains = get-content -path $controlDomainsFilePath <# сформируем часть запроса по доменам в виде (RequestParams LIKE '%@domain.com%') OR (RequestParams LIKE '%@domain2%') OR (RequestParams LIKE '%@domain3%') OR (RequestParams LIKE '%@domain4%') #> 
$controlRecords = Get-LPRecordSet (
@"         SELECT             c-ip AS UserIP,             cs-username AS Host,             cs-method AS RequestType,             cs-uri-query AS RequestParams         FROM $logName         WHERE (             (RequestType = 'MAIL') AND ( $(($controlDomains | % {"`n`t`t`t`t(RequestParams LIKE '%$_%')"}) -join " OR ")             )         )
"@
)

$inputMailErrors = Get-ProblemRecords `     -queryWhere `
@"     (         (UserIP IN ($(($controlRecords | % {"'$($_.UserIP)'"} | select -unique) -join "; ")))         AND (             (Status >= 400)         )     )
"@ `
-
logName $logName
# отправляем письмо администраторам
if ($inputMailErrors.factsCount) {
    $SMTPclient.Send( `
        $emailFrom, `
        $emailTo, `
        "Обнаружены проблемы со входящей почтой контрольных контрагентов ($($inputMailErrors.factsCount))", `
@"
При проверке журналов SMTP сервера обнаружены потенциально проблемные SMTP сессии в части входящей почты
от контрольных контрагентов. Проблемы обнаружены для следующих доменов и адресов:
$($inputMailErrors.facts | % {$_.FromDomain} | where {$_ -ne ""} | select -unique | %{
    $domain = $_
    "`n" + $domain
    $inputMailErrors.facts | where {$_.FromDomain -eq $domain} |% {$_.FromAddr} | where {$_ -ne ""} | select -unique | %{"`n`t"+$_}
})
Выдержка из журнала SMTP сервера:
$($inputMailErrors.facts | % { $_.sessionLogFormatted })
С Уважением,
$emailFrom.
Скрипт: $($myinvocation.mycommand.path)
"@ `
)
}# ============================================================================================================
# ============================================================================================================
# ============================================================================================================
# Запись в журнал о завершении работы
$evt=new-object System.Diagnostics.EventLog("Application")
$evt.Source=$myinvocation.mycommand.name
$evt.WriteEntry(`
    "Скрипт успешно завершил работу.",`
    [System.Diagnostics.EventLogEntryType]::Information `
)

Отзывы » (23)

  1. Переписал формирование писем с использованием командлета Group-Object. Письма стали читабельнее при большом количестве доменов:

    # ============================================================================================================
    # ============================================================================================================
    # ============================================================================================================
    # подготовим и направим отчёт по всем проблемным исходящим сессиям проверяемого журнала
    
    $outputMailErrors = Get-ProblemRecords `
        -queryWhere `
    @"
        (Direction = 'OutboundConnectionResponse') AND (
            (Response LIKE '5__%')
            OR (Response LIKE '4__%')
        )
    "@ `
        -logName $logName
    
    # отправляем письмо администраторам
    if ($outputMailErrors.factsCount) {
        $SMTPclient.Send( `
            $emailFrom, `
            $emailTo, `
            "[ГАРО-ITG $($myinvocation.mycommand.name)] Обнаружены проблемы в исходящем SMTP трафике ($($outputMailErrors.factsCount))", `
    @"
    При проверке журналов SMTP сервера обнаружены потенциально проблемные SMTP сессии.
    Проблемы обнаружены для следующих доменов и адресов:
    $($outputMailErrors.facts | ?{$_.ToDomain} | group ToDomain | sort name | %{
        "`n$($_.name) ($($_.count)):"
        $_.group | group ToAddr | sort name | %{
            "`n`t$($_.name) ($($_.count))"
        }
    })
    
    Выдержка из журнала SMTP сервера:
    
    $($outputMailErrors.facts | group ToDomain | sort name | %{
    @"
    
    |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
    ======== Домен $($_.name) ($($_.count)) :
    |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
    "@
        $_.group | % { $_.sessionLogFormatted }
    })
    
    P.S.>
    Скрипт: $($myinvocation.mycommand.path)
    
    С Уважением,
    postmaster@novgaro.ru.
    "@ `
        )
    }
    
    # ============================================================================================================
    # ============================================================================================================
    # ============================================================================================================
    # подготовим и направим отчёт по отказам на исходящие ESMTP сессии (EHLO)
    
    if ($outputMailErrors.facts) {
        $ESMTPErrors = $outputMailErrors.facts | where {
            $_.fact | where {
                ($_.Response -like "500*") `
                -or ($_.Response -like "502*")
            }
        }
    
    if ($ESMTPErrors) {
    $SMTPclient.Send( `
        $emailFrom, `
        $emailTo, `
        "[ГАРО-ITG $($myinvocation.mycommand.name)] Обнаружены домены, не поддерживающие ESMTP", `
        @"
    При проверке журналов SMTP сервера обнаружены ESMTP сессии, в рамках которых получен отказ на EHLO.
    Следует включить данные домены в перечень доменов, обслуживаемых исключительно по SMTP - в SMTP connector
    "SMTP, internet, user messages".
    
    Обнаружены домены:
    
    $($ESMTPErrors | group ToDomain | sort name | %{
        "`n$($_.name) ($($_.count))"
    })
    
    Выдержка из журнала SMTP сервера:
    
    $($ESMTPErrors | group ToDomain | sort name | %{
    @"
    
    |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
    ======== Домен $($_.name) ($($_.count)) :
    |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
    "@
        $_.group | % { $_.sessionLogFormatted }
    })
    
    P.S.>
    Скрипт: $($myinvocation.mycommand.path)
    
    С Уважением,
    postmaster@novgaro.ru.
    "@ `
    )
    }
    }
    
    # ============================================================================================================
    # ============================================================================================================
    # ============================================================================================================
    # Станислав предложил по постоянным ошибкам 5ХХ отправлять письмо не только администратору, но и пользователю,
    # с почтой которого были проблемы
    
    if ($outputMailErrors.facts) {
        $outputUsersMailErrors = $outputMailErrors.facts | where {
            $_.fact | where {
                ($_.Response -like "5*") `
            }
        }
    
    # отправляем письмо пользователям, копию - админам
        $outputUsersMailErrors | ?{$_.FromAddr} | group FromAddr | % {
            $SMTPclient.Send( `
                $emailFrom, `
                "<$emailTo>,<$($_.name)>", `
                "[ГАРО-ITG $($myinvocation.mycommand.name)] Обнаружены проблемы при доставке Вашей почты за $($logDate.ToString('dd.MM.yyyy'))", `
    @"
    Уважаемый / уважаемая $($_.name).
    
    Данное письмо сформировано автоматически сервисом проверки почтового трафика $($myinvocation.mycommand.name).
    При проверке журналов почтового сервера за $($logDate.ToString('dd.MM.yyyy')) обнаружены возможные проблемы
    при доставке Вашей почты следующим контрагентам:
    
    $($_.group | group ToDomain | sort name | %{
        "`n$($_.name) ($($_.count)):"
        $_.group | group ToAddr | sort name | %{
            "`n`t$($_.name) ($($_.count))"
        }
    })
    
    Данная информация также передана в отдел ИТ для анализа и принятия предупреждающих мер. С целью уточнения
    факта доставки указанных почтовых сообщений следует обратиться в отдел ИТ.
    
    Выдержка из журнала SMTP сервера (время - по Гринвичу):
    
    $($_.group | group ToDomain | sort name | %{
    @"
    
    |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
    ======== Домен $($_.name) ($($_.count)) :
    |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
    "@
        $_.group | % { $_.sessionLogFormatted }
    })
    
    С Уважением,
    postmaster@novgaro.ru.
    "@ `
        )
        }
    }
    
    # ============================================================================================================
    # ============================================================================================================
    # ============================================================================================================
    # попробуем проанализировать факты грейлистинга и по ним выслать письмо администратору
    
    if ($outputMailErrors.facts) {
        $graylistingErrors = $outputMailErrors.facts | where {
            ($_.ToDomain -ne "") `
            -and ($_.fact | where { `
                ($_.Response -like "4*") `
            }) `
        }
    
    # отправляем письмо админам
        $graylistingErrors | %{$_.ToDomain} | select -unique | % {
            $toDomain = $_
    
            $SMTPclient.Send( `
                $emailFrom, `
                $emailTo, `
                "[ГАРО-ITG $($myinvocation.mycommand.name)] Грейлистинг $toDomain", `
    @"
    Добрый день, postmaster@$($toDomain).
    
    Мы используем Вашу почтовую систему для работы с нашими клиентами.
    
    Обнаружили использование грейлистинга $($logDate.ToString('dd.MM.yyyy')), время - GMT+0:
    
    $($graylistingErrors | where {$_.ToDomain -eq $toDomain} | % { $_.sessionLogFormatted })
    
    Зачастую, нам необходимо доставлять почту в кратчайшие сроки.
    
    В связи с чем прошу Вас рассмотреть возможность включить наш MTA в белый список в обход грейлистинга: мы - это novgaro.ru, в dns имеется spf запись, по которой можно включить отправителя - наш MTA: mail.novgaro.ru. [213.148.164.198].
    
    P.S. Мы выполняем все требования по борьбе со спамом, репутация отправителя -
    http://www.senderbase.org/senderbase_queries/detailip?search_string=213.148.164.198
    Также наш MTA фигурирует в ряде белых списков: http://multirbl.valli.org/lookup/213.148.164.198.html
    
    С Уважением,
    postmaster@novgaro.ru.
    "@ `
        )
        }
    }
    
    # ============================================================================================================
    # ============================================================================================================
    # ============================================================================================================
    # проанализируем входящую почту. найдём все проблемы для "контрольных" доменов
    # алгоритм следующий. отказ может поступить на любом этапе сессии. поэтому сначала предлагаю осуществить
    # поиск host header через поиск всех предложений MAIL для контрольных адресов. затем снова делаем запрос
    # уже фильтруя по host header и статусам с ошибками (4XX и 5XX). таким образом мы уже и получим проблемные
    # сессии для контрольных адресов
    
    # загрузим контрольный список шаблонов адресов
    # $controlDomains = get-content -path $controlDomainsFilePath
    
    $controlDomains = ([ADSI]"LDAP://CN=Default Message Filter,CN=Message Delivery,CN=Global Settings,CN=NovGARO,CN=Microsoft Exchange,CN=Services,CN=Configuration,DC=novgaro,DC=ru").msExchTurfListNames `
        | ? {$_} | %{ switch -regex ($_) { `
        "^\*(@.*)$" { "$($matches[1])" } `
        default { "$_" } `
    } }
    
    # поиск всех ip адресов для контрольных адресов
    $controlRecords = Get-LPRecordSet (
    @"
            SELECT
                c-ip AS UserIP,
                cs-username AS Host,
                cs-method AS RequestType,
                cs-uri-query AS RequestParams
            FROM $logName
            WHERE (
                (RequestType = 'MAIL') AND ( $(($controlDomains | % {"`n`t`t`t`t(RequestParams LIKE '%$_%')"}) -join " OR ")
                )
            )
    "@
    )
    
    $inputMailErrors = Get-ProblemRecords `
        -queryWhere `
    @"
        (
            (UserIP IN ($(($controlRecords | % {"'$($_.UserIP)'"} | select -unique) -join "; ")))
            AND (
                (Status >= 400)
            )
        )
    "@ `
        -logName $logName
    
    # отправляем письмо администраторам
    if ($inputMailErrors.factsCount) {
        $SMTPclient.Send( `
            $emailFrom, `
            $emailTo, `
            "[ГАРО-ITG $($myinvocation.mycommand.name)] Обнаружены проблемы со входящей почтой контрольных контрагентов ($($inputMailErrors.factsCount))", `
    @"
    При проверке журналов SMTP сервера обнаружены потенциально проблемные SMTP сессии в части
    входящей почты от контрольных контрагентов.
    
    Проблемы обнаружены для следующих доменов и адресов:
    $($inputMailErrors.facts | ?{$_.FromDomain} | group FromDomain | sort name | %{
        "`n$($_.name) ($($_.count)):"
        $_.group | ? {$_.FromAddr} | group FromAddr | sort name | %{
            "`n`t$($_.name) ($($_.count))"
        }
    })
    
    Выдержка из журнала SMTP сервера:
    
    $($inputMailErrors.facts | group FromDomain | sort name | %{
    @"
    
    |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
    ======== Домен $($_.name) ($($_.count)) :
    |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
    "@
        $_.group | % { $_.sessionLogFormatted }
    })
    
    P.S.>
    Скрипт: $($myinvocation.mycommand.path)
    
    С Уважением,
    postmaster@novgaro.ru.
    "@ `
        )
    }
    
  2. Обнаружил проблему в журнале:

    Event Type:	Error
    Event Source:	itgCheckMailFlow
    Event Category:	None
    Event ID:	0
    Date:		23.11.2010
    Time:		14:17:06
    User:		N/A
    Description:
    При выполнении сценария itgCheckMailFlow.ps1 23.11.2010 в 14:17 возникла ошибка:
    
    At C:\temp\itgLogParser.psm1:96 char:34
    + 		$LPRecordSet = $LPQuery.Execute &lt;&lt;&lt; System.Runtime.InteropServices.COMException (0x80010105): The server threw an exception. (Exception from HRESULT: 0x80010105 (RPC_E_SERVERFAULT))
       --- End of inner exception stack trace ---
       at System.RuntimeType.InvokeDispMethod(String name, BindingFlags invokeAttr, Object target, Object[] args, Boolean[] byrefModifiers, Int32 culture, String[] namedParameters)
       at System.RuntimeType.InvokeMember(String name, BindingFlags bindingFlags, Binder binder, Object target, Object[] providedArgs, ParameterModifier[] modifiers, CultureInfo culture, String[] namedParams)
       at System.Management.Automation.ComMethod.InvokeMethod(PSMethod method, Object[] arguments)

    Как видно, даже подобное подробное сообщение не позволяет понять сути проблемы. Посему попробуем добавить в сообщение об ошибке хотя бы сам sql запрос.
    Методика этого решения описана в этой статье.

  3. А теперь ещё одна редакция сценария — уже с прогресс-барами (несколько минут молча ждать, не понимая, на каком мы этапе — не очень приятно):

    ImportSystemModules 
    Import-Module "c:\temp\itgLogParser.psm1"
    Import-Module "c:\temp\itgWrapper.psm1"
    Write-Progress -Activity "Importing modules..." -Completed -Status "All done."
    
    [string]$activity = "Анализ журналов SMTP сервера"
    [int]$step = 0
    [int]$steps = 6
    
    $ErrorActionPreference = "Stop"
    trap { Write-CurrentException($_) }
    
    Write-EventLogITG -message "Скрипт успешно приступил к работе..."
    
    [string]$fullTimeFormat = "dd.MM.yyyy HH:mm:ss"
    # диапазон времени, в пределах которого будем искать записи одной сессии
    [System.TimeSpan]$logTimeRangeForSession = New-TimeSpan -minutes 1
    
    # суффикс файла с перечнем контрольных доменов
    [string]$controlDomainsFileSuffix = " - контрольные домены.txt"
    # вычислим файл контрольных доменов
    $scriptPath = $myinvocation.mycommand.path
    if (-not $scriptPath) { $scriptPath = "c:\temp\itgCheckMailFlow.ps1" }
    [string]$controlDomainsFilePath = [System.IO.Path]::Combine( `
        [System.IO.Path]::GetDirectoryName($scriptPath), `
        [System.IO.Path]::GetFileNameWithoutExtension($scriptPath) + $controlDomainsFileSuffix `
    )
    
    # почтовый сервер, через который будем осуществлять отправку почты, адреса
    [Net.Mail.SmtpClient]$SMTPclient = new-object Net.Mail.SmtpClient("localhost")
    [string]$emailFrom = "postmaster@novgaro.ru"
    [string]$emailTo   = "postmaster@novgaro.ru"
    
    # дата, за которую будем проводить анализ
    [System.DateTime]$logDate = (Get-Date).AddDays(-1)
    
    # регулярное выражение для выделения адреса электронной почты
    $reDomain = "(?<domain>(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?\.)+(?:aero|asia|biz|cat|com|coop|edu|gov|info|int|jobs|mil|mobi|museum|name|net|org|pro|tel|travel|[a-zA-Z]{2}))"
    $reMailAddr = "(?<addr>(?<lname>[a-zA-Z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-zA-Z0-9!#$%&'*+/=?^_`{|}~-]+)*)@$($reDomain))"
    
    # получим расположение каталога журналов smtp и "вычислим" имя файла журнала
    $SMTPServiceIndex = 1
    $SMTPServerSettings = Get-WmiObject `
        -namespace "root\MicrosoftIISv2" `
        -class IIsSmtpServerSetting `
        -filter ("Name='SmtpSvc/" + $SMTPServiceIndex + "'")
        
    # период ведения журнала должен быть установлен в 1 день. именно на это мы рассчитываем.
    # поэтому если настройки будут иные - будем ругаться
    if ($SMTPServerSettings.LogFilePeriod -ne 1) {
        throw new-object System.ApplicationException("Период ведения журнала SMTP сервера должен быть установлен в 1 день!")
    }
    
    # при определении имени файла журнала нам необходима информация о типе журнала
    # я не нашёл способ пока определить тип журнала. нам необходим w3c
    [string]$logPrefix = "ex"
    [string]$logSuffix = ".log"
    
    # throw new-object System.ApplicationException("Тип журнала SMTP сервера не поддерживается!")
    
    $logName = "$($SMTPServerSettings.LogFileDirectory)\SMTPSVC${SMTPServiceIndex}\${LogPrefix}$($logDate.ToString("yyMMdd"))$LogSuffix"
    
    
    # ============================================================================================================
    # общие функции, осуществляющие поиск проблем по запросу, затем формирующие необходимый пакет информации на
    # каждую проблему для последующего анализа и формирования отчёта
    # в журналах IIS поля LogFilename, LogRow, UserIP, UserName, Date, Time, ServiceInstance, HostName, 
    # ServerIP, TimeTaken, BytesSent, BytesReceived, StatusCode, Win32StatusCode, RequestType, Target, Parameters
    # в журнале w3c (который используем) поля time c-ip cs-username cs-method cs-uri-query sc-status
    # ============================================================================================================
    
    function Get-ProblemRecords {
    	<#
    		.Synopsis
    		    Выполняем заданный запрос для поиска проблем и возвращает массив PS с записями о проблемах и 
                сопутсвтующей информацией.
    		.Description
    		    Выполняем заданный запрос (полученный с использованием $queryWhere) через LogParser для поиска
                проблем и возвращает массив PS с записями о проблемах и сопутсвтующей информацией, как то:
                    - запись из журнала, содержающая проблему
                    - выдержка из журнала по каждой проблеме
    		.Parameter queryWhere
    		    Часть запроса (Where секция) в SQL синтаксисе Log Parser для поиска проблемы
    		.Parameter logName
    		    Путь к файлу журнала, по которому будет выполнен запрос
       	#>
     
        param (
    		[Parameter(
    			Mandatory=$true,
    			Position=0,
    			ValueFromPipeline=$false,
    			HelpMessage="Запрос в SQL синтаксисе Log Parser."
    		)]
            [string]$queryWhere,
    
    		[Parameter(
    			Mandatory=$true,
    			Position=1,
    			ValueFromPipeline=$false,
    			HelpMessage="Путь к файлу журнала."
    		)]
            [string]$logName
        )
    
        write-progress `
            -id 100 `
            -parentId 10 `
            -activity "Сбор информации о сессиях, отвечающих заданным критериям" `
            -status "Анализ журнала SMTP сервера, поиск фактов" `
            -currentOperation "выполнение запроса LogParser" `
            -percentcomplete 0
    
        # запрос к журналу
        $mailErrorsRecords = Get-LPRecordSet (
    @"
            SELECT
                TO_TIMESTAMP(TIMESTAMP('$($logDate.ToString("dd.MM.yyyy"))', 'dd.MM.yyyy'), time) AS FullTime, 
                c-ip AS UserIP, 
                cs-username AS Host, 
                cs-username AS Direction, 
                cs-method AS RequestType, 
                cs-uri-query AS Response,
                cs-uri-query AS RequestParams,
                sc-status AS Status
            FROM $logName
            WHERE $queryWhere
    "@
        )
    
        [int]$mailErrorsCount = 0
        if ($mailErrorsRecords) {
            if ($mailErrorsRecords.length) {
                $mailErrorsCount = $mailErrorsRecords.length
            } else {
                $mailErrorsCount = 1
            }
        }
            
        # получен перечень проблем, теперь по каждой проблеме получим сессию
    
        $result = new-Object System.Management.Automation.PSObject
        $result | add-member `
            -memberType NoteProperty `
            -name factsCount `
            -value $mailErrorsCount
    
        if ($mailErrorsCount) { $facts = $mailErrorsRecords | foreach-object `
        -begin {
            [int]$mailErrorIndex = 0
            write-progress `
                -id 100 `
                -parentId 10 `
                -activity "Сбор информации о сессиях, отвечающих заданным критериям" `
                -status "Анализ журнала SMTP сервера, сбор сессий"
        } `
        -process {
            # начнём формирование выходного массива
            $mailErrorIndex += 1
    
           	$problemObj = new-Object System.Management.Automation.PSObject
    		$problemObj | add-member `
                -memberType NoteProperty `
                -name fact `
                -value $_
    
            if ($_.Direction -like 'OutboundConnection*') {
        		$problemObj | add-member `
                    -memberType NoteProperty `
                    -name direction `
                    -value "Outbound"
            } else {
        		$problemObj | add-member `
                    -memberType NoteProperty `
                    -name direction `
                    -value "Inbound"
            }
    
            $sessionLog = Get-LPRecordSet (
    @"
                SELECT
                    TO_TIMESTAMP(TIMESTAMP('$($logDate.ToString("dd.MM.yyyy"))', 'dd.MM.yyyy'), time) AS FullTime, 
                    c-ip AS UserIP, 
                    cs-username AS Host, 
                    cs-username AS Direction, 
                    cs-method AS RequestType, 
                    cs-uri-query AS Response,
                    cs-uri-query AS RequestParams,
                    sc-status AS Status
                FROM $logName
                WHERE
                    (
                        (FullTime > TIMESTAMP('$(($_.FullTime - $logTimeRangeForSession).ToString($fullTimeFormat))', '$fullTimeFormat'))
                        AND (FullTime < TIMESTAMP('$(($_.FullTime + $logTimeRangeForSession).ToString($fullTimeFormat))', '$fullTimeFormat'))
                    )
                    AND (UserIP LIKE '$($_.UserIP)')
    "@
            )
    
            # имеем журнал одной сессии.
            # включаем его в результирующий объект
    		$problemObj | add-member `
                -memberType NoteProperty `
                -name sessionLog `
                -value $sessionLog
                    
            # теперь также выделим адрес "виновника" контагента, его домен, сервер, с которым была попытка установить связь,
            # его ответ и IP
    		$problemObj | add-member `
                -memberType NoteProperty `
                -name UserIP `
                -value $_.UserIP
    
            switch ($problemObj.direction) {
                "Inbound"  {$headerStr = ($sessionLog | where { $_.RequestType -match "^(?:EHLO|HELO)$" } | select -first 1).RequestParams}
                "Outbound" {$headerStr = ($sessionLog | where { $_.Response -match "^220" } | select -first 1).Response}
            }
    
            $userHeader = if ( `
                $headerStr `
                -match $reDomain `
            ) { $matches[0] } 
            else { "" }
                
            $problemObj | add-member `
                -memberType NoteProperty `
                -name UserHeader `
                -value $userHeader
    
            $addrStr = ($sessionLog | where { $_.RequestType -eq "RCPT" } | select -first 1).Response
            if ($addrStr -match $reMailAddr) {
                $emailAddr = $matches["addr"] 
                $emailLName = $matches["lname"]
                $emailDomain = $matches["domain"]
            } else {
                $emailAddr = ""
                $emailLName = ""
                $emailDomain = ""
            }
    
            $problemObj | add-member -memberType NoteProperty -name ToAddr -value $emailAddr
    		$problemObj | add-member -memberType NoteProperty -name ToDomain -value $emailDomain
    		$problemObj | add-member -memberType NoteProperty -name ToLName -value $emailLName
    
            # определим адрес отправителя
            $addrStr = ($sessionLog | where { $_.RequestType -eq "MAIL" } | select -first 1).Response
            if ($addrStr -match $reMailAddr) {
                $emailAddr = $matches["addr"] 
                $emailLName = $matches["lname"]
                $emailDomain = $matches["domain"]
            } else {
                $emailAddr = ""
                $emailLName = ""
                $emailDomain = ""
            }
    
            $problemObj | add-member -memberType NoteProperty -name FromAddr -value $emailAddr
    		$problemObj | add-member -memberType NoteProperty -name FromDomain -value $emailDomain
    		$problemObj | add-member -memberType NoteProperty -name FromLName -value $emailLName
    
    
            # отформатируем фрагмент журнала так, как нам требуется, и поместим его в переменную
            [string]$logPart = `
    @"
    
    ===============================================================================
    проблема:
    "@
            $format = switch ($problemObj.direction) {
                Inbound {
                    @{Label="time"; Expression={$_.FullTime.ToString($fullTimeFormat)}; width=20}, `
                    @{Label="status"; Expression={$_.Status}; width=4}, `
                    @{Label="content"; Expression={"$($_.RequestType) $($_.Response)"}; width=80}
                }
                Outbound {
                    @{Label="time"; Expression={$_.FullTime.ToString($fullTimeFormat)}; width=20}, `
                    @{Label="content"; Expression={"$($_.RequestType) $($_.Response)"}; width=80}
                }
            }
    
            $logPart += $_ | format-table `
                -property $format `
                -wrap `
                -hideTableHeaders `
                | out-string
    
            $logPart += 
    @"
        server IP: $($problemObj.UserIP), server header: $($problemObj.UserHeader)
        To addr: $($problemObj.ToAddr), To domain: $($problemObj.ToDomain), To lname: $($problemObj.ToLName)
        From addr: $($problemObj.FromAddr), From domain: $($problemObj.FromDomain), From lname: $($problemObj.FromLName)
    
    "@
            $format = switch ($problemObj.direction) {
                Inbound {
                    @{Label="time"; Expression={$_.FullTime.ToString($fullTimeFormat)}; width=20}, `
                    @{Label="dest. IP"; Expression={$_.UserIP}; width=16}, `
                    @{Label="status"; Expression={$_.Status}; width=6}, `
                    @{Label="content"; Expression={"$($_.RequestType) $($_.Response)"}; width=70}
                }
                Outbound {
                    @{Label="time"; Expression={$_.FullTime.ToString($fullTimeFormat)}; width=20}, `
                    @{Label="dest. IP"; Expression={$_.UserIP}; width=16}, `
                    @{Label="content"; Expression={"$($_.RequestType) $($_.Response)"}; width=80}
                }
            }
            $logPart += $sessionLog | format-table `
                -property $format `
                -wrap `
                | out-string
    
            $logPart += `
    @"
    ===============================================================================
    "@
    		$problemObj | add-member `
                -memberType NoteProperty `
                -name sessionLogFormatted `
                -value $logPart
                
            write-progress `
                -id 100 `
                -parentId 10 `
                -activity "Сбор информации о сессиях, отвечающих заданным критериям" `
                -currentOperation "анализ журнала по каждому факту ($mailErrorIndex из $mailErrorsCount)" `
                -status "Анализ журнала SMTP сервера, сбор сессий" `
                -percentcomplete ($mailErrorIndex/$mailErrorsCount*100)
    
            # возвращаем сформированный объект
            $problemObj
        } `
        -end {
            write-progress `
                -id 100 `
                -parentId 10 `
                -activity "Сбор информации о сессиях, отвечающих заданным критериям" `
                -status "Анализ журнала SMTP сервера завершён" `
                -completed
        }
        }
        
        if ($facts) {
            $result | add-member `
                -memberType NoteProperty `
                -name facts `
                -value $facts
        }
            
        $result
    }
    
    # ============================================================================================================
    # ============================================================================================================
    # ============================================================================================================
    # подготовим и направим отчёт по всем проблемным исходящим сессиям проверяемого журнала
    
    write-progress `
        -id 10 `
        -activity $activity `
        -currentOperation "Готовим отчёт по всем проблемным исходящим SMTP сессиям ($step из $steps)" `
        -status "Анализ журнала SMTP сервера, сбор сессий" `
        -percentcomplete ($step/$steps*100)
    
    $outputMailErrors = Get-ProblemRecords `
        -queryWhere `
    @"
        (Direction = 'OutboundConnectionResponse') AND (
            (Response LIKE '5__%')
            OR (Response LIKE '4__%')
        )
    "@ `
        -logName $logName
    
    # отправляем письмо администраторам
    if ($outputMailErrors.factsCount) {
        $SMTPclient.Send( `
            $emailFrom, `
            $emailTo, `
            "[ГАРО-ITG $($myinvocation.mycommand.name)] Обнаружены проблемы в исходящем SMTP трафике ($($outputMailErrors.factsCount))", `
    @"
    При проверке журналов SMTP сервера обнаружены потенциально проблемные SMTP сессии.
    Проблемы обнаружены для следующих доменов и адресов:
    $($outputMailErrors.facts | ?{$_.ToDomain} | group ToDomain | sort name | %{
        "`n$($_.name) ($($_.count)):"
        $_.group | group ToAddr | sort name | %{
            "`n`t$($_.name) ($($_.count))"
        }
    })
    
    Выдержка из журнала SMTP сервера:
    
    $($outputMailErrors.facts | group ToDomain | sort name | %{
    @"
    
    |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
    ======== Домен $($_.name) ($($_.count)) :
    |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
    "@
        $_.group | % { $_.sessionLogFormatted }
    })
    
    P.S.>
    Скрипт: $($myinvocation.mycommand.path)
    
    С Уважением,
    postmaster@novgaro.ru.
    "@ `
        )
    }
    
    # ============================================================================================================
    # ============================================================================================================
    # ============================================================================================================
    # подготовим и направим отчёт по отказам на исходящие ESMTP сессии (EHLO)
    
    $step += 1;
    write-progress `
        -id 10 `
        -activity $activity `
        -currentOperation "Готовим отчёт по отказам на исходящие ESMTP сессии (EHLO) ($step из $steps)" `
        -status "Анализ журнала SMTP сервера, сбор сессий" `
        -percentcomplete ($step/$steps*100)
    
    if ($outputMailErrors.facts) {
        $ESMTPErrors = $outputMailErrors.facts | where {
            $_.fact | where { 
                ($_.Response -like "500*") `
                -or ($_.Response -like "502*")
            }
        }
    
    if ($ESMTPErrors) {
    
    # выделим найденные SMTP домены
    $newSMTPDomains = $ESMTPErrors | group ToDomain | sort name
    
    # прочитаем и разберём список доменов, не поддерживающих ESMTP
    $exchSMTPConnectorConfig = [ADSI]"LDAP://CN=SMTP\, internet\, user messages,CN=Connections,CN=Основная группа маршрутизации сообщений,CN=Routing Groups,CN=Почтовые серверы Компании ГАРО,CN=Administrative Groups,CN=NovGARO,CN=Microsoft Exchange,CN=Services,CN=Configuration,DC=novgaro,DC=ru"
    $SMTPDomains = $exchSMTPConnectorConfig.routingList `
        | ? {$_} | % {
            if ($_ -match "^SMTP:$($reDomain);(?<cost>\d+)$") {
              	$resObj = new-Object System.Management.Automation.PSObject
                $resObj | add-member -memberType NoteProperty -name Domain -value $matches["domain"]
                $resObj | add-member -memberType NoteProperty -name Cost -value $matches["cost"]
                write-output $resObj
            }
        }
    
    # проведём проверку вновь найденных доменов на соответствия шаблонам доменов из AD
    $newSMTPDomains = $newSMTPDomains | % {
        $newDomain = $_.name
        $matchedWildcard = ($SMTPDomains | ? { $newDomain -like $_.domain } | select -first 1).domain
        if ($matchedWildcard) {
            $_ | add-member -memberType NoteProperty -name matchedWildcard -value $matchedWildcard -force
        }
        $_
    }
    
    # готовим новый список
    $SMTPDomains += $newSMTPDomains | ? {-not $_.matchedWildcard} | % {
      	$resObj = new-Object System.Management.Automation.PSObject
        $resObj | add-member -memberType NoteProperty -name Domain -value $_.name
        $resObj | add-member -memberType NoteProperty -name Cost -value 10
        write-output $resObj
    }
    
    # сохраняем изменения в AD
    $exchSMTPConnectorConfig.routingList = $SMTPDomains | %{ "SMTP:$($_.domain);$($_.cost)" }
    $exchSMTPConnectorConfig.SetInfo()
    
    $SMTPclient.Send( `
        $emailFrom, `
        $emailTo, `
        "[ГАРО-ITG $($myinvocation.mycommand.name)] Обнаружены домены, не поддерживающие ESMTP", `
        @"
    При проверке журналов SMTP сервера обнаружены ESMTP сессии, в рамках которых получен отказ на EHLO.
    Данные домены должны быть включены в перечень доменов, обслуживаемых исключительно по SMTP - в SMTP connector
    "SMTP, internet, user messages" (включены сценарием автоматически).
    
    $($newSMTPDomains | group matchedWildcard | sort name | % {
        if ($_.name) {
            "`nСоответствуют уже введённому шаблону $($_.name) ($($_.count)):"
        } else {
            "`nВновь вводимые ($($_.count)):"
        }
        $_.group | % {
            "`n`t$($_.name)"
        }
    })
    
    Вновь вводимые записи включены в параметры коннектора. Новое состояние коннектора:
    $($SMTPDomains | % {
        "`n`t$($_.domain)"
    })
    
    Выдержка из журнала SMTP сервера:
    
    $($ESMTPErrors | group ToDomain | sort name | %{
    @"
    
    |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
    ======== Домен $($_.name) ($($_.count)) :
    |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
    "@
        $_.group | % { $_.sessionLogFormatted }
    })
    
    P.S.>
    Скрипт: $($myinvocation.mycommand.path)
    
    С Уважением,
    postmaster@novgaro.ru.
    "@ `
    )
    }
    }
    
    # ============================================================================================================
    # ============================================================================================================
    # ============================================================================================================
    # Станислав предложил по постоянным ошибкам 5ХХ отправлять письмо не только администратору, но и пользователю,
    # с почтой которого были проблемы
    
    $step += 1;
    write-progress `
        -id 10 `
        -activity $activity `
        -currentOperation "Готовим отчёт сотрудникам по проблемам доставки их корреспонденции (EHLO) ($step из $steps)" `
        -status "Анализ журнала SMTP сервера, сбор сессий" `
        -percentcomplete ($step/$steps*100)
    
    if ($outputMailErrors.facts) {
        $outputUsersMailErrors = $outputMailErrors.facts | where {
            $_.fact | where { 
                ($_.Response -like "5*") `
            }
        }
    
    # отправляем письмо пользователям, копию - админам
        $outputUsersMailErrors | ?{$_.FromAddr} | group FromAddr | % {
            $SMTPclient.Send( `
                $emailFrom, `
                $emailTo, `
                "[ГАРО-ITG $($myinvocation.mycommand.name)] Обнаружены проблемы при доставке Вашей почты за $($logDate.ToString('dd.MM.yyyy'))", `
    @"
    Уважаемый / уважаемая $($_.name).
    
    Данное письмо сформировано автоматически сервисом проверки почтового трафика $($myinvocation.mycommand.name).
    При проверке журналов почтового сервера за $($logDate.ToString('dd.MM.yyyy')) обнаружены возможные проблемы
    при доставке Вашей почты следующим контрагентам:
    
    $($_.group | group ToDomain | sort name | %{
        "`n$($_.name) ($($_.count)):"
        $_.group | group ToAddr | sort name | %{
            "`n`t$($_.name) ($($_.count))"
        }
    })
    
    Данная информация также передана в отдел ИТ для анализа и принятия предупреждающих мер. С целью уточнения
    факта доставки указанных почтовых сообщений следует обратиться в отдел ИТ.
    
    Выдержка из журнала SMTP сервера (время - по Гринвичу):
    
    $($_.group | group ToDomain | sort name | %{
    @"
    
    |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
    ======== Домен $($_.name) ($($_.count)) :
    |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
    "@
        $_.group | % { $_.sessionLogFormatted }
    })
    
    С Уважением,
    postmaster@novgaro.ru.
    "@ `
        )
        }
    }
    
    # ============================================================================================================
    # ============================================================================================================
    # ============================================================================================================
    # попробуем проанализировать факты грейлистинга и по ним выслать письмо администратору
    
    $step += 1;
    write-progress `
        -id 10 `
        -activity $activity `
        -currentOperation "Готовим отчёты по грейлистингу ($step из $steps)" `
        -status "Анализ журнала SMTP сервера, сбор сессий" `
        -percentcomplete ($step/$steps*100)
    
    if ($outputMailErrors.facts) {
        $graylistingErrors = $outputMailErrors.facts | where {
            ($_.ToDomain -ne "") `
            -and ($_.fact | where { `
                ($_.Response -like "4*") `
            }) `
        }
    
    # отправляем письмо админам
        $graylistingErrors | %{$_.ToDomain} | select -unique | % {
            $toDomain = $_
    
            $SMTPclient.Send( `
                $emailFrom, `
                $emailTo, `
                "[ГАРО-ITG $($myinvocation.mycommand.name)] Грейлистинг $toDomain", `
    @"
    Добрый день, postmaster@$($toDomain).
    
    Мы используем Вашу почтовую систему для работы с нашими клиентами.
    
    Обнаружили использование грейлистинга $($logDate.ToString('dd.MM.yyyy')), время - GMT+0:
    
    $($graylistingErrors | where {$_.ToDomain -eq $toDomain} | % { $_.sessionLogFormatted })
    
    Зачастую, нам необходимо доставлять почту в кратчайшие сроки.
    
    В связи с чем прошу Вас рассмотреть возможность включить наш MTA в белый список в обход грейлистинга: мы - это novgaro.ru, в dns имеется spf запись, по которой можно включить отправителя - наш MTA: mail.novgaro.ru. [213.148.164.198].
    
    P.S. Мы выполняем все требования по борьбе со спамом, репутация отправителя - 
    http://www.senderbase.org/senderbase_queries/detailip?search_string=213.148.164.198
    Также наш MTA фигурирует в ряде белых списков: http://multirbl.valli.org/lookup/213.148.164.198.html
    
    С Уважением,
    postmaster@novgaro.ru.
    "@ `
        )
        }
    }
    
    # ============================================================================================================
    # ============================================================================================================
    # ============================================================================================================
    # проанализируем входящую почту. найдём все проблемы для "контрольных" доменов
    # алгоритм следующий. отказ может поступить на любом этапе сессии. поэтому сначала предлагаю осуществить
    # поиск host header через поиск всех предложений MAIL для контрольных адресов. затем снова делаем запрос
    # уже фильтруя по host header и статусам с ошибками (4XX и 5XX). таким образом мы уже и получим проблемные
    # сессии для контрольных адресов
    
    $step += 1;
    write-progress `
        -id 10 `
        -activity $activity `
        -currentOperation "Анализ входящей почты от контрольных контрагентов ($step из $steps)" `
        -status "Анализ журнала SMTP сервера, сбор сессий" `
        -percentcomplete ($step/$steps*100)
    
    
    # загрузим контрольный список шаблонов адресов
    <#
    $exchSenderFilterConfig = [ADSI]"LDAP://CN=Default Message Filter,CN=Message Delivery,CN=Global Settings,CN=NovGARO,CN=Microsoft Exchange,CN=Services,CN=Configuration,DC=novgaro,DC=ru"
    $controlDomains = $exchSenderFilterConfig.msExchTurfListNames `
        | ? {$_} | %{ switch -regex ($_) { `
        "^\*(@.*)$" { "$($matches[1])" } `
        default { "$_" } `
    } }
    #>
    $controlDomains = get-content -path $controlDomainsFilePath
    
    # поиск всех ip адресов для контрольных адресов
    $controlRecords = Get-LPRecordSet (
    @"
    SELECT
        c-ip AS UserIP, 
        cs-username AS Host, 
        cs-method AS RequestType, 
        cs-uri-query AS RequestParams
    FROM $logName
    WHERE (
        (RequestType = 'MAIL') AND (
    $(($controlDomains | % {"        (RequestParams LIKE '%$_%')"}) -join " OR `r`n")
        )
    )
    "@
    )
    
    $inputMailErrors = Get-ProblemRecords `
        -queryWhere `
    @"
        (
            (UserIP IN ($(($controlRecords | % {"'$($_.UserIP)'"} | select -unique) -join "; ")))
            AND (
                (Status >= 400)
            )
        )
    "@ `
        -logName $logName
    
    # отправляем письмо администраторам
    if ($inputMailErrors.factsCount) {
        $SMTPclient.Send( `
            $emailFrom, `
            $emailTo, `
            "[ГАРО-ITG $($myinvocation.mycommand.name)] Обнаружены проблемы со входящей почтой контрольных контрагентов ($($inputMailErrors.factsCount))", `
    @"
    При проверке журналов SMTP сервера обнаружены потенциально проблемные SMTP сессии в части
    входящей почты от контрольных контрагентов.
    
    Проблемы обнаружены для следующих доменов и адресов:
    $($inputMailErrors.facts | ?{$_.FromDomain} | group FromDomain | sort name | %{
        "`n$($_.name) ($($_.count)):"
        $_.group | ? {$_.FromAddr} | group FromAddr | sort name | %{
            "`n`t$($_.name) ($($_.count))"
        }
    })
    
    Выдержка из журнала SMTP сервера:
    
    $($inputMailErrors.facts | group FromDomain | sort name | %{
    @"
    
    |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
    ======== Домен $($_.name) ($($_.count)) :
    |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
    "@
        $_.group | % { $_.sessionLogFormatted }
    })
    
    P.S.>
    Скрипт: $($myinvocation.mycommand.path)
    
    С Уважением,
    postmaster@novgaro.ru.
    "@ `
        )
    }
    
    # ============================================================================================================
    # ============================================================================================================
    # ============================================================================================================
    # теперь осуществим поиск всех успешных (без кодов 4ХХ и 5ХХ) попыток отправки почты с целью автоматического
    # обновления белого списка
    # предложение следующее. Осуществим поиск всех исходящих предложений RCPT TO. Далее - найдём на них полученные
    # ответы. Если ответ на RCPT TO был положительный, включаем этот адрес в кандидаты в белый список.
    # с целью подготовки информационного письма администраторам осуществим поиск всех исходящих сессий на адреса-
    # кандидаты и приведём их в письме.
    # Далее, проанализируем кандидатов на предмет соовпадения с шаблонами, уже находящимися в белом списке. Если
    # совпадение обнаружено, адрес включаем в информационное письмо, с указанием шаблона, которому он соответсвует
    # (группируем по шаблонам). В белый список его уже не добавляем.
    # И последнее. Анализируем объединение исходного и нового списков, выделяем шаблоны с общим доменом. И в
    # письме предлагаем заменить адреса с общим доменом на шаблон для домена (указывая адреса и предлагаемый шаблон)
    
    $step += 1;
    write-progress `
        -id 10 `
        -activity $activity `
        -currentOperation "Пополнение списка контрольных контрагентов ($step из $steps)" `
        -status "Анализ журнала SMTP сервера, сбор сессий" `
        -percentcomplete ($step/$steps*100)
    
    $outputMailSuccess = Get-ProblemRecords `
        -queryWhere `
    @"
        (Direction = 'OutboundConnectionCommand') AND (RequestType = 'RCPT')
    "@ `
        -logName $logName
        
    #        $addrStr = ($sessionLog | where { $_.RequestType -eq "RCPT" } | select -first 1).Response
    
    $newAddrs = $outputMailSuccess.facts | % {
        $collectResponse = $false
        $addrStr = ""
        # ещё нужно проверить, что это не DSN
        if (($_.sessionLog `
            | ? {($_.Direction -eq "OutboundConnectionCommand") -and ($_.RequestType -eq "MAIL")} `
            | select -first 1 `
        ).RequestParams -match $reMailAddr) {
            if (-not ("postmaster@novgaro.ru" -contains $matches["addr"])) {    
                $emailFromAddr = $matches["addr"] 
                $emailFromLName = $matches["lname"]
                $emailFromDomain = $matches["domain"]
                $_.sessionLog | % {
                    if (($collectResponse) -and ($_.Direction -eq "OutboundConnectionResponse") -and ($_.Response -match "^250")) {
                        if ($addrStr -match $reMailAddr) {
                            $emailAddr = $matches["addr"] 
                            $emailLName = $matches["lname"]
                            $emailDomain = $matches["domain"]
                        } else {
                            $emailAddr = ""
                            $emailLName = ""
                            $emailDomain = ""
                        }
    
                      	$resObj = new-Object System.Management.Automation.PSObject
                        $resObj | add-member -memberType NoteProperty -name FromAddr -value $emailFromAddr
                        $resObj | add-member -memberType NoteProperty -name FromLName -value $emailFromLName
                        $resObj | add-member -memberType NoteProperty -name FromDomain -value $emailFromDomain
                        $resObj | add-member -memberType NoteProperty -name ToAddr -value $emailAddr
                        $resObj | add-member -memberType NoteProperty -name ToLName -value $emailLName
                        $resObj | add-member -memberType NoteProperty -name TODomain -value $emailDomain
                        write-output $resObj
                    }
                    $collectResponse = ($_.RequestType -eq "RCPT")
                    if ($collectResponse) {
                        $addrStr = $_.RequestParams
                    } else {
                    }
                }
            }
        } else { # отправитель - <>, это - DSN
        }
    }
    
    # загрузим контрольный список шаблонов адресов
    #$exchTurfListNames = $exchSenderFilterConfig.msExchTurfListNames
    #$controlDomains = get-content -path $controlDomainsFilePath
    $exchTurfListNames = $controlDomains `
        | ? {$_} | %{ switch -regex ($_) { `
        "^(@.*)$" { "*$($matches[1])" } `
        default { "$_" } `
    } }
    
    # проведём проверку нового списка на повторения и на соответствия шаблонам адресов из AD
    $newAddrsGrouped = $newAddrs | group ToAddr | sort name | % {
        $newAddr = $_.name
        $matchedWildcard = ( $exchTurfListNames | ? { $newAddr -like $_ } | select -first 1)
        if ($matchedWildcard) {
            $_ | add-member -memberType NoteProperty -name matchedWildcard -value $matchedWildcard
        }
        $_
    }
    
    # объединяем
    $exchTurfListNames = ($exchTurfListNames + ($newAddrsGrouped | ? {-not $_.matchedWildcard} | % {$_.name})) | sort | %{$("$_")}
    
    # обработаем белый список, выделим домены
    $newTurfList = $exchTurfListNames | % {
        $test = $_ -match $reMailAddr
      	$resObj = new-Object System.Management.Automation.PSObject
        $resObj | add-member -memberType NoteProperty -name Wildcard -value $matches["addr"]
        $resObj | add-member -memberType NoteProperty -name Domain -value $matches["domain"]
        $resObj
    }
    
    $exchTurfListNames = $newTurfList | group Domain | sort name | %{
        $_.group | sort Wildcard | % {
            "$($_.wildcard)"
        }
    }
    
    # отправляем письмо администраторам
    if ($outputMailSuccess.factsCount) {
        $SMTPclient.Send( `
            $emailFrom, `
            $emailTo, `
            "[ГАРО-ITG $($myinvocation.mycommand.name)] Обновление автоматического белого списка отправителей", `
    @"
    При проверке исходящих сессий в журнале SMTP сервера обнаружены адреса контрагентов. Ниже - анализ соответствия
    существующему белому списку:
    
    $($newAddrsGrouped | group matchedWildcard | sort name | % {
        if ($_.name) {
            "`nСоответствуют шаблону $($_.name) ($($_.count)):"
        } else {
            "`nВновь вводимые (не соответствуют белому списку) ($($_.count)):"
        }
        $_.group | % {
            "`n`t$($_.name) ($($_.count))"
        }
    })
    
    Вновь вводимые записи включены в белый список. Новое состояние белого списка:
    $($exchTurfListNames | % {
        "`n`t$_"
    })
    
    Предлагаем объединить ряд записей в белом списке одним шаблоном по домену:
    
    $($newTurfList | group Domain | ?{$_.count -ge 2} | sort name | %{
        "`n- Заменить на шаблон *@$($_.name) следующие шаблоны ($($_.count)):"
        $_.group | sort Wildcard | % {
            "`n`t$($_.wildcard)"
        }
    })
    
    P.S.>
    Скрипт: $($myinvocation.mycommand.path)
    
    С Уважением,
    postmaster@novgaro.ru.
    "@ `
        )
    }
    
    # сохраняем новый белый список
    <#
    $exchSenderFilterConfig.msExchTurfListNames = $exchTurfListNames
    $exchSenderFilterConfig.SetInfo()
    #>
    $exchTurfListNames `
        | ? {$_} | %{ switch -regex ($_) { `
        "^\*(@.*)$" { "$($matches[1])" } `
        default { "$_" } `
    } } | out-file $controlDomainsFilePath -force
    
    # ============================================================================================================
    # ============================================================================================================
    # ============================================================================================================
    # Запись в журнал о завершении работы
    
    write-progress `
        -id 10 `
        -activity $activity `
        -status "Завершение работы..." `
        -completed
    
    Write-Successfull 
    
  4. Как видно из статьи, есть сложности с выделением записей лога, имеющих отношение к одной SMTP сессии. Если быть более точным — полностью «правильного» решения пока просто не нашёл, возможно — его и нет, если не использовать расширений для формирования логов SMTP. Поднял эту тему на technet, пока получил только ссылку на англоязычного коллегу, который выделяет сессию практически также.
    Так что пока поиск решения для более корректного анализа лога SMTP продолжаю. Буду рад любой «наводке» с Вашей стороны.

  5. Фисюк Денис:

    Здравствуйте, Сергей

    1. Подойдет ли, написанный вами скрипт, для анализа и мониторинга smtp логов IIS 7.0?
    Формат логов W3C Extended Log File Format.
    2. Скрипт для анализа smtp использует дополнительные модули. Можно ли получить скрипт со всеми необходимыми модулями?
    3. Решили ли вы вопрос по поводу проблем идентификации smpt-сессии?

    Хочу адаптировать данный скрипт для анализа smtp логов с нескольких серверов.
    Буду благодарен за помощь.

    • Итак, Денис. На выходных занят был, руки только сейчас дошли до ответа. Развернул svn сервер (Visual SVN Server), и опубликовал своё svn хранилище. Получить текущую версию описанного сценария со всеми его модулями можно теперь по следующему адресу: http://svn.novgaro.ru/svn/tools/Exchange/Monitoring/trunk/. Для доступа используйте учётную запись svn_sysadm, пароль — svn. Если у Вас установлен Tortoise SVN — воспользуйте следующим url: svn:http://svn.novgaro.ru/svn/tools/Exchange/Monitoring/trunk/. Запись даёт право только на чтение, в дальнейшем могу дать право и на запись, если у Вас будет желание развивать этот скрипт, я буду только благодарен сотрудничеству. Сейчас выложу ссылку на хранилище в статью.
      Касательно логов IIS7 — да, подойдёт. Для анализа использую LogParser от Microsoft, который может разобрать любой журнал любого приложения от MS.
      Касательно идентификации smtp сессии — нет, так ничего и не нашёл. Подробно изучив API SMTP Sync понял, что эта проблема (отсутствие id сессии) заложена в интерфейс, поэтому решить её вряд ли получится, потому решений и нет.
      Текущая редакция сценария без проблем обработает несколько журналов (за несколько дней).

    • Один момент — на используемые модули ссылки пока через файловое хранилище — уже относительные, утром поправлю на ссылки через http. Также опубликую хранилище и по svn протоколу.

  6. Фисюк Денис:

    Добрый день, Сергей.

    Скрипт получил. Спасибо. Приступил к отладке скрипта itgCheckMailFlow.ps1
    При отладке появляется ошибка:

    Exception calling "CreateEventSource" with "2" argument(s): "Must specify value
     for source."
    At E:\work\Monitoring\ITG.Wrapper\ITG.Wrapper.psm1:157 char:57
    +         [System.Diagnostics.EventLog]::CreateEventSource &lt;&lt;&lt;&lt; ($source, $even
    tLog)
        + CategoryInfo          : NotSpecified: (:) [], MethodInvocationException
        + FullyQualifiedErrorId : DotNetMethodException
    
    • Как Вы правильно подметили в следующем комментарии, проблема в том, что не имеет значения $global.myinvocation. Потому как запускаете из под отладчика, посему и не определено значение.
      Значение $source определяю в ITG.Wrapper сейчас так:

      [string]$source = $([System.IO.Path]::GetFileNameWithoutExtension($global:myinvocation.mycommand.name))
      

      Отсюда, как видно, и проблема.
      Могу посоветовать на время отладки заменить эту строку на что-нибудь типа

      [string]$source = 'my-test-script'
      

      Тогда и события в журнале событий будете видеть от этого источника. После завершения отладки верните исходную строку, и при запуске через командную строку у Вас всё будет ok.
      P.S. Могу сейчас новую версию ITG.Wrapper предложить, которая будет проверять $global.myinvocation, и если этот объект не даёт нам имени сценария — использовать значение переменной $global.myscriptname (её можете явно задавать в сценарии, подключающем ITG.Wrapper), а если и её значение отсутствует — тогда использовать предопределённое значение 'itg-ps-script'. Если такое решение Вас устроит для отладки — сделаю. Сообщите Ваше мнение, буду благодарен.

  7. Фисюк Денис:

    По поводу предыдущей ошибки:
    Вы писали:

    Обратил внимание на любопытный эффект. Если в командной строке:

    powershell . c:\temp\itgCheckMailFlow.ps1
    

    опустить точку, тогда $global.myinvocation не будет соответствовать области сценария, и регистрация событий не будет работать (из-за некорректного формирования source). Поэтому точка ВАЖНА!

    Использую для отладки сценариев PrimalScript 2007. Как в этом случае избежать ошибки?

    • Попробуйте PowerShell ISE — у меня из под него проблем не возникало с отладкой. PrimalScript 2007 не использовал, хотя не уверен, что проблемы будут.
      P.S. Этот «механизм» я использовал только для того, чтобы в сценариях при подключении моего ITG.Wrapper не требовалось явно указывать имя файла сценария (я его затем использую при записи событий в журнал событий). Можно, безусловно, переделать ITG.Wrapper… Рассмотрю любые Ваши предложения, каким образом в модуле ещё можно определить имя сценария для публикации событий в журнал, не используя $global.myinvocation, и при этом не усложняя существенно использование wrapper’а.

  8. Фисюк Денис:

    При запуске скрипта на сервере с iis 7 с smtp-сервисом пишет ошибку:

    PS C:\tools\monitoring> . c:\tools\monitoring\itgCheckMailFlow.ps1
    Get-WmiObject : Invalid namespace
      At C:\tools\monitoring\ITG.ParseSMTPlog\ITG.ParseSMTPlog.psm1:41 char:40
      $SMTPServerSettings = Get-WmiObject <<<<
        CategoryInfo : InvalidOperation: (:) [Get-WmiObject], ManagementException
        FullyQualifiedErrorId : GetWMIManagementException, Microsoft.PowerShell.Commands.GetWmiObjectCommand
    
    • Я использую namespace "root\MicrosoftIISv2":

          $SMTPServerSettings = Get-WmiObject `
              -namespace "root\MicrosoftIISv2" `
              -class IIsSmtpServerSetting `
              -filter ("Name='SmtpSvc/" + $SMTPServiceIndex + "'")
      

      В IIS7 он будет другим, судя по всему. Посмотрите через WMI browser, может что-либо типа «root\MicrosoftIISv7″. Буду благодарен, если сообщите namespace для IIS7. Тогда добавлю условную проверку, чтобы была поддержка и IIS6, и IIS7.

  9. Фисюк Денис:

    вопрос закрыт,  не установил  iis  wmi provider

  10. Фисюк Денис:

    wmi namespace root\WebAdministration, класс IIsSmtpServerSettings отсутствует

  11. Фисюк Денис:

    Выяснил. Т.к. smtp-сервер в ii7 не изменился и достался по наследству от iis 6, управляется также из оснастки iis 6.0 Manager, то для работы по wmi c smtp-сервером требуется wmi provider iis 6.0. Namespace wmi iis 6 остался тот же — root\MicrosoftIISv2. Wmi iis 6 можно доустановить в Windows 7/2008         IIS6 Management Compatibility -> IIS 6 WMI Compatibility

  12. Фисюк Денис:

    Добрый день, Сергей! Сценарий рабочий. Но есть вопросы:- Использую в Windows Server 2008 EN. Поэтому все сообщения на кирилице не корректно отображаются. — Возникает ошибка в сценарии при работе с файлами, имена которых состоят из кирилицы- Часть функционала достаточно специфичны и не подходит для нашей организации, к примеру сценарий что-то пишет в active directory.- Хотелось бы максимально упростить сценарий: отказаться от wmi, (дать возможность, задать путь к логам вручную и тестировать сценарий на любом компьютере.), отказаться от плагинов и работы с active directory.оставить только парсинг логов, формирование отчета, уведомление по почте администратора.К примеру в отчете хотелось бы видеть:Количество отправленных писем, количество не доставленных писем, список не правильных адресов — bad mail и другую статистику, которая была бы полезна для анализа доставки почты.Т.е.:Хотелось бы иметь для начала простой сценарий, который бы позволил администратору проанализировать лог-файл или список логов за заданный период времени.

    • Денис, спасибо, что отозвались.
      - поправить файл сценария так, чтобы путь к логам задавался руками, а не через запрос к WMI, не проблема. Так и было в первых его редакциях, переделал как раз с целью облегчения тиражирования с сервера на сервер.
      - касательно проблем с кириллицей — всё просто. Файлы — не в unicode исключительно из-за diff от svn. Вы можете либо пересохранить файлы в unicode, либо задать на машине кодовую таблицу для не unicode приложений — русскую. И всё должно работать нормально с кириллицей. У меня тоже оси англоязычные, поэтому предлагаю проверенные рецепты.
      - касательно записи в AD — текущая версия сценария ничего страшного в AD не пишет. Только то, что касается exchange organization. В частности, один из «плагинов» обнаруживает серверы, которые не принимаются от нас ESMTP (не понимают EHLO, только HELO). Этот плагин выделяет домены, при отправке на которые возникли указанные сложности, и прописывает их в параметры exchange smtp коннектора. В результате, почта, отправляемая на отобранные таким образом домены, будет отправлена через отдельный коннектор (на нём «цена» отправки на указанные домены ниже, чем на других коннекторах на *), а в этом коннекторе запрещено использовать EHLO. Как результат — сессия короче, не вижу лишних ошибок в логах (502, 503 или ещё каких). Больше ничего в AD не пишет сейчас сценарий. А этот плагин можете целиком отключить (для этого просто достаточно удалить папку с ним, и всё)
      -Добавить плагины для подсчёта отправленных писем и так далее — не проблема, у меня такой задачи не стояло…

      А пример с комментариями, как использовать плагин на журналах за несколько дней, указав при этом на каталог с журналами руками, постараюсь подготовить на этой неделе, здесь и опубликую.

  13. Фисюк Денис:

    К пример Windows 7 не поддерживает SMTP-сервер на IIS7. Поэтому протестировать сценарий с использованием всех необходимых компонентов, плагинов не получится

Опубликовать комментарий

XHTML: Вы можете использовать следующие HTML теги: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>

Tags Связь с комментариями статьи:
RSS комментарии
Обратная ссылка