Автоматизация контроля 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)
RSS комментарии
Обратная ссылка
Переписал формирование писем с использованием командлета Group-Object. Письма стали читабельнее при большом количестве доменов:
Обнаружил проблему в журнале:
Как видно, даже подобное подробное сообщение не позволяет понять сути проблемы. Посему попробуем добавить в сообщение об ошибке хотя бы сам sql запрос.
Методика этого решения описана в этой статье.
А теперь ещё одна редакция сценария — уже с прогресс-барами (несколько минут молча ждать, не понимая, на каком мы этапе — не очень приятно):
Как видно из статьи, есть сложности с выделением записей лога, имеющих отношение к одной SMTP сессии. Если быть более точным — полностью «правильного» решения пока просто не нашёл, возможно — его и нет, если не использовать расширений для формирования логов SMTP. Поднял эту тему на technet, пока получил только ссылку на англоязычного коллегу, который выделяет сессию практически также.
Так что пока поиск решения для более корректного анализа лога SMTP продолжаю. Буду рад любой «наводке» с Вашей стороны.
Здравствуйте, Сергей
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 протоколу.Добрый день, Сергей.
Скрипт получил. Спасибо. Приступил к отладке скрипта itgCheckMailFlow.ps1
При отладке появляется ошибка:
Как Вы правильно подметили в следующем комментарии, проблема в том, что не имеет значения
$global.myinvocation
. Потому как запускаете из под отладчика, посему и не определено значение.Значение
$source
определяю в ITG.Wrapper сейчас так:Отсюда, как видно, и проблема.
Могу посоветовать на время отладки заменить эту строку на что-нибудь типа
Тогда и события в журнале событий будете видеть от этого источника. После завершения отладки верните исходную строку, и при запуске через командную строку у Вас всё будет ok.
P.S. Могу сейчас новую версию ITG.Wrapper предложить, которая будет проверять
$global.myinvocation
, и если этот объект не даёт нам имени сценария — использовать значение переменной$global.myscriptname
(её можете явно задавать в сценарии, подключающем ITG.Wrapper), а если и её значение отсутствует — тогда использовать предопределённое значение'itg-ps-script'
. Если такое решение Вас устроит для отладки — сделаю. Сообщите Ваше мнение, буду благодарен.По поводу предыдущей ошибки:
Вы писали:
Использую для отладки сценариев PrimalScript 2007. Как в этом случае избежать ошибки?
Попробуйте PowerShell ISE — у меня из под него проблем не возникало с отладкой. PrimalScript 2007 не использовал, хотя не уверен, что проблемы будут.
P.S. Этот «механизм» я использовал только для того, чтобы в сценариях при подключении моего ITG.Wrapper не требовалось явно указывать имя файла сценария (я его затем использую при записи событий в журнал событий). Можно, безусловно, переделать ITG.Wrapper… Рассмотрю любые Ваши предложения, каким образом в модуле ещё можно определить имя сценария для публикации событий в журнал, не используя
$global.myinvocation
, и при этом не усложняя существенно использование wrapper’а.При запуске скрипта на сервере с iis 7 с smtp-сервисом пишет ошибку:
Я использую namespace
"root\MicrosoftIISv2"
:В IIS7 он будет другим, судя по всему. Посмотрите через WMI browser, может что-либо типа «root\MicrosoftIISv7″. Буду благодарен, если сообщите namespace для IIS7. Тогда добавлю условную проверку, чтобы была поддержка и IIS6, и IIS7.
вопрос закрыт, не установил iis wmi provider
Бывает :-). namespace тот же?
P.S. эти запросы использую только для определения местоположения логов. Я понимаю, что в IIS7 можно и родными командлетами для этих целей воспользоваться, но такой подход универсален и для IIS6, и для IIS7.
wmi namespace root\WebAdministration, класс IIsSmtpServerSettings отсутствует
Выяснил. Т.к. 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
Спасибо за информацию. Самому скоро переходить на 2008ую ОС, так что Ваша информацию как нельзя кстати.
P.S. После установки IIS6 WMI Compatiblity сценарии работают успешно?
Фисюк Денис » Денис, у Вас всё получилось? Сценарии работают?
Добрый день, Сергей! Сценарий рабочий. Но есть вопросы:- Использую в 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 не пишет сейчас сценарий. А этот плагин можете целиком отключить (для этого просто достаточно удалить папку с ним, и всё)
-Добавить плагины для подсчёта отправленных писем и так далее — не проблема, у меня такой задачи не стояло…
А пример с комментариями, как использовать плагин на журналах за несколько дней, указав при этом на каталог с журналами руками, постараюсь подготовить на этой неделе, здесь и опубликую.
К пример Windows 7 не поддерживает SMTP-сервер на IIS7. Поэтому протестировать сценарий с использованием всех необходимых компонентов, плагинов не получится
Так его тестировать без exchange смысла никакого нет. Если опасаетесь подвоха — запустите его из под ограниченной учётки, которая сможет только прочитать логи и AD, но не изменить.