Совет недели (2014-05-3): Создание уникальных подписей Outlook при помощи Windows PowerShell

Совет неделиВ предыдущем совете недели я вам рассказал о том, как можно вручную создать подпись при помощи Microsoft Outlook 2013, а также как можно централизованно распространить пользователям одинаковую подпись. Естественно, в большинстве случаев такой метод нельзя назвать идеальным, так как подписи не только должны быть в едином корпоративном стиле, но должны еще и уникально идентифицировать самого отправителя. К таким элементам подписей относятся как ФИО и занимаемая пользователем должность, так и, как вариант, различные ссылки и изображения с уникальной информацией.

Как я и обещал в конце предыдущего совета недели, в этой статье будет рассмотрен пример генерации подписей на основании заготовленного заранее docx-шаблона средствами Windows PowerShell.

Создание уникальных подписей средствами Windows PowerShell

Для того чтобы создаваемые подписи могли получать правильную информацию о пользователях, идеальным вариантом будет получение всех необходимых значений из свойств учетных записей Active Directory. Чтобы автоматически создавать подписи для пользователей, основываясь на данных из доменных служб Active Directory, прежде всего следует создать некий шаблон, на основании которого будут генерироваться все будущие подписи.

Такой шаблон создается при помощи Microsoft Outlook, как и в предыдущих случаях, однако в сгенерированный RTF-файл нужно для каждого важного элемента добавить закладки с одноименными наименованиями атрибутов Active Directory, после чего сохранить такой файл в стандартном формате MS Word, то есть шаблон сохраняется как *.docx-файл. Таким образом, по завершению редактирования ваш шаблон будет выглядеть следующим образом:

Редактирование будущего шаблона подписи

Рис. 1. Редактирование будущего шаблона подписи

Шаблон готов, можно переходить к самому скрипту.

В Интернете можно найти много различных сценариев, которые позволяют выполнять генерацию подписей различными методами. Меня заинтересовал скрипт, написанный Jan Egil Ring, позволяющий «вытягивать» различные фрагменты текста подписи из подготовленного заранее docx-файла, а также изменять их согласно атрибутам учетной записи пользователя в доменных службах Active Directory.

Этот скрипт был мною переработан под свои нужды, о чем мне сейчас и хотелось бы подробнее рассказать. В следующем примере исходный docx-файл с шаблоном подписи будет находиться в расположении \\ИМЯ_ДОМЕНА\netlogon\Signatures и будет называться CorpSignature.

Так как с документами Microsoft Office лучше всего работать в 32-разрядном Windows PowerShell, первым делом следует сразу запустить нужную версию. Для этого можно выполнить следующий код:

#Стартуем скрипт в режиме x86
if ($env:Processor_Architecture -ne "x86")
{ write-warning 'Стартуем PowerShell x86'
&"$env:windir\syswow64\windowspowershell\v1.0\powershell.exe" -NonInteractive -NoProfile -File $myinvocation.Mycommand.path -ExecutionPolicy bypass 

Далее будут объявляться основные переменные, которые включают в себя имя нашего шаблона, наименование домена, путь к нашему шаблону и прочее. В том случае, если для пользователей не были определены некоторые общие атрибуты, для них задаются значения «для всех». Здесь нужно будет изменить значения для улицы, почтового ящика, наименования города, а также номеров телефона и факса. Эта часть скрипта будет выглядеть так:

#Основные переменные 
$TemplateName = 'CorpSignature'
$DomainName = 'biopharmaceutic'
$SigSource = "\\$DomainName\netlogon\Signatures"
$DefaultAddress = "Wall Street"
$DefaultPOBox = '666'
$DefaultCity = 'Central City'
$DefaultTelephone = '911'
$DefaultFax = '912' 

Затем будет определяться путь к локальному расположению шаблонов, а также полный путь к исходному файлу с подписью:

#Переменные для локального и удаленного расположения подписи
$AppData=(Get-Item env:appdata).value 
$SigPath = '\Microsoft\Signatures' 
$LocalSignaturePath = $AppData+$SigPath 
$RemoteSignaturePathFull = $SigSource+'\'+$TemplateName+'.docx' 
$fullPath = $LocalSignaturePath+'\'+$TemplateName+'.docx'

В следующей части сценария определяются некоторые атрибуты Active Directory для учетных записей пользователей, а именно: выводимое имя, должность, улица, почтовый ящик, город, номер телефона, факс, мобильник, веб-страница, а также электронная почта самого пользователя. Как вы скорее всего заметили, в созданном ранее шаблоне нет значения для номера факса. Этот атрибут я добавил специально для того, чтобы показать, как можно избежать ошибок, если в вашем шаблоне отсутствует определенная часть, которая должна подлежать изменениям. В моем случае этот фрагмент сценария получился такой:

#Получаем информацию для текущего пользователя из Active Directory
$UserName = $env:username
$Filter = "(&(objectCategory=User)(samAccountName=$UserName))"
$Searcher = New-Object System.DirectoryServices.DirectorySearcher
$Searcher.Filter = $Filter
$ADUserPath = $Searcher.FindOne()
$ADUser = $ADUserPath.GetDirectoryEntry()
$ADDisplayName = $ADUser.DisplayName
$ADTitle = $ADUser.title
$ADCompany = $ADUser.company
$ADStreetAddress = $ADUser.streetaddress
$ADPOBox = $ADUser.postofficebox
$ADCity = $ADUser.l
$ADTelePhoneNumber = $ADUser.TelephoneNumber
$ADFax = $ADUser.facsimileTelephoneNumber
$ADMobile = $ADUser.mobile
$ADWebSite = $ADUser.wWWHomePage
$ADEmailAddress = $ADUser.mail 

Следующим этапом будет копирование исходного файла в расположение с подписями Outlook. Без нужды каждый раз этот файл копировать нет никакого смысла. Поэтому я сделал так, чтобы файл копировался только в двух случаях:

  • Файл копируется, если в конечном расположении таковой отсутствует;
  • Если же файл уже находится в целевой папке, то тогда будет высчитан хэш для обоих файлов и исходный файл будет копироваться только в том случае, если его хэш отличается от хэша конечного файла.

Выглядит это так:

#Копируем файл, если таковой отсутствует в целевой папке или если его хэш отличается от исходного
If (!(Test-Path -Path $fullPath)) {
Copy-Item $RemoteSignaturePathFull $LocalSignaturePath -Recurse -Force 
}
Else {
$Rem = Get-FileHash $RemoteSignaturePathFull -Algorithm SHA256 
$loc = Get-FileHash $fullPath -Algorithm SHA256
Write-Host $Rem.Hash
Write-Host $loc.Hash
If ($Loc.Hash -ne $Rem.Hash)
{
Copy-Item $RemoteSignaturePathFull $LocalSignaturePath -Recurse -Force 
Write-Host "КОПИРУЮ"
} Else {
Write-Host "ХЭШИ СОВПАДАЮТ"}
} 

Сейчас мы переходим к процессу изменения текста в документе. Для этого следует открыть скопированный шаблон подписи, а после этого, в уже открытом документе, изменить текст. Менять фрагменты текста можно многими способами, однако в этом сценарии будут использовать только два метода из них: при помощи метода Execute, который описан в статье «Hey, Scripting Guy! How Can I Use Windows PowerShell to Look for and Replace a Word in a Microsoft Word Document?» можно изменить определенную часть текста; также можно изменить текст, который находится в созданных нами заранее закладках. Рассмотрим оба способа.

Сначала в этом скрипте определяются параметры для метода Execute. Не буду расписывать отдельно каждый параметр, так как об их значении вы можете узнать из указанной выше статьи. В нашем случае код будет следующим:

$ReplaceAll = 2
$FindContinue = 1
$MatchCase = $False
$MatchWholeWord = $True
$MatchWildcards = $False
$MatchSoundsLike = $False
$MatchAllWordForms = $False
$Forward = $True
$Wrap = $FindContinue
$Format = $False 

Потом открывается сам документ:

#Начинаем вытягивать данные из Active Directory
[Void] [Reflection.Assembly]::LoadWithPartialName("Microsoft.Office.Interop.Word")
$WordTmpl = New-Object -comObject Word.Application
$WordTmpl.Visible = $True	
$objDoc = $WordTmpl.Documents.Open($fullPath)
$objSelection = $WordTmpl.Selection 

Изменение текста при помощи метода Execute будет рассматриваться немного ниже, а сейчас я покажу, как можно изменить текст из закладок. Для этого определяется имя закладки, затем переменной ReplaceText присваивается значение конкретной переменной $AD…, например, $ADDisplayName, а затем текст изменяется при помощи переменной $RangeNew. Выглядит это следующим образом:

#Определяем имя
$Bookmark = "displayName"  
$ReplaceText = $ADDisplayName
$RangeNew = $objDoc.Bookmarks.Item($Bookmark).Range 
$RangeNew.Text = $ReplaceText 

Теперь рассмотрим вариант изменения текста, если у учетной записи пользователя не было определено значение конкретного атрибута. Например, не была указана улица. В этом случае мы присваиваем новое значение с закладкой переменной $Bookmark, после чего проверяем, есть ли у атрибута, определенного в переменной $ADStreetAddress, значение. Если такового нет, мы при помощи переменной $ReplaceText берем текст из указанной в самом начале сценария переменной $DefaultAddress. Если же значение есть, в переменную $ReplaceText будет записываться значение из $ADStreetAddress. После этого текст изменяется средствами переменной $RangeNew. Выглядит это так:

     #Адрес пользователя с проверкой на заполнение атрибута
	$Bookmark = "streetAddress"
	If ($ADStreetAddress.ToString() -eq '') { 
		$ReplaceText = $DefaultAddress
	} Else {
       		 $ReplaceText = $ADStreetAddress.ToString()
	}
		$RangeNew = $objDoc.Bookmarks.Item($Bookmark).Range 
        		$RangeNew.Text = $ReplaceText 

Для этого метода можно выделить еще один случай: когда в самом шаблоне не было определенно требуемой закладки. В нашем примере это будет закладка для номера факса, то есть «facsimileTelephoneNumber». В этом случае начало кода будет идентично предыдущему примеру, а перед заменой текста средствами $RangeNew будет вставлена проверка наличия самой закладки. Код будет выглядеть так:

     #Номер факса с проверкой на заполнение атрибута
    $Bookmark = "facsimileTelephoneNumber"
    If ($ADFax.ToString() -eq '') {
        $ReplaceText = $DefaultFax
    } Else {
        $ReplaceText = $ADFax.ToString()
    }
    if ($objDoc.Bookmarks.Exists($Bookmark)) {
        $RangeNew = $objDoc.Bookmarks.Item($Bookmark).Range 
        $RangeNew.Text = $ReplaceText
    }	 

Пришло время рассмотреть второй метод изменения текста. В данном примере к нему придется прибегнуть в том случае, когда в самом шаблоне у нас текст из закладки будет совпадать с текстом, который не нужно менять. Это номер мобильного (закладка называется mobile, а перед номером мобильника, который будет изменяться, в документе можно найти текст mobile). В этом случае и будет использоваться метод Execute. Этот метод не позволяет изменять текст, заключенный в закладке, но вместо существующего текста «099», который можно найти в шаблоне, после выполнения сценария будет фигурировать именно тот текст, который мы получим из атрибута telephoneNumber. В сценарии это будет выглядеть так:

     #Номер мобильного, учитывая то, что в тексте 2 раза упоминается mobile
    $FindText = "099"
    $ReplaceText = $ADMobile.ToString()
    $objSelection.Find.Execute($FindText,$MatchCase, $MatchWholeWord,$MatchWildcards,$MatchSoundsLike, $MatchAllWordForms,$Forward,$Wrap,$Format, $ReplaceText,$ReplaceAll)

Все необходимые компоненты изменены – осталось сохранить подпись в форматах RTF, TXT и HTML. Это делается следующим образом:

#Сохраняем в HTML
	$saveFormat = [Enum]::Parse([Microsoft.Office.Interop.Word.WdSaveFormat], "wdFormatHTML");
	$path = $LocalSignaturePath+'\'+$TemplateName+".htm"
	$WordTmpl.ActiveDocument.saveas([ref]$path, [ref]$saveFormat) 

После этого следует закрыть активный документ, закрыть сам MS Word и выйти из 32-разрядной оболочки Windows PowerShell. Так как я скрипт разбил на фрагменты и некоторые строки кода при его разборе не написал, далее вы можете ознакомиться с полной версией скрипта:

#Стартуем скрипт в режиме x86
if ($env:Processor_Architecture -ne "x86")
{ write-warning 'Стартуем PowerShell x86'
&"$env:windir\syswow64\windowspowershell\v1.0\powershell.exe" -NonInteractive -NoProfile -File $myinvocation.Mycommand.path -ExecutionPolicy bypass

#Основные переменные 
$TemplateName = 'CorpSignature'
$DomainName = 'biopharmaceutic'
$SigSource = "\\$DomainName\netlogon\Signatures"
$DefaultAddress = "Wall Street"
$DefaultPOBox = '666'
$DefaultCity = 'Central City'
$DefaultTelephone = '911'
$DefaultFax = '912'
		 
#Переменные для локального и удаленного расположения подписи
$AppData=(Get-Item env:appdata).value 
$SigPath = '\Microsoft\Signatures' 
$LocalSignaturePath = $AppData+$SigPath 
$RemoteSignaturePathFull = $SigSource+'\'+$TemplateName+'.docx'
$fullPath = $LocalSignaturePath+'\'+$TemplateName+'.docx'
		 
#Получаем информацию для текущего пользователя из Active Directory
$UserName = $env:username
$Filter = "(&(objectCategory=User)(samAccountName=$UserName))"
$Searcher = New-Object System.DirectoryServices.DirectorySearcher
$Searcher.Filter = $Filter
$ADUserPath = $Searcher.FindOne()
$ADUser = $ADUserPath.GetDirectoryEntry()
$ADDisplayName = $ADUser.DisplayName
$ADTitle = $ADUser.title
$ADCompany = $ADUser.company
$ADStreetAddress = $ADUser.streetaddress
$ADPOBox = $ADUser.postofficebox
$ADCity = $ADUser.l
$ADTelePhoneNumber = $ADUser.TelephoneNumber
$ADFax = $ADUser.facsimileTelephoneNumber
$ADMobile = $ADUser.mobile
$ADWebSite = $ADUser.wWWHomePage
$ADEmailAddress = $ADUser.mail

#Копируем файл, если таковой отсутствует в целевой папке или если его хэш отличается от исходного
If (!(Test-Path -Path $fullPath)) {
Copy-Item $RemoteSignaturePathFull $LocalSignaturePath -Recurse -Force 
}
Else {
$Rem = Get-FileHash $RemoteSignaturePathFull -Algorithm SHA256 
$loc = Get-FileHash $fullPath -Algorithm SHA256
Write-Host $Rem.Hash
Write-Host $loc.Hash
If ($Loc.Hash -ne $Rem.Hash)
{
Copy-Item $RemoteSignaturePathFull $LocalSignaturePath -Recurse -Force 
Write-Host "КОПИРУЮ"
} Else {
Write-Host "ХЭШИ СОВПАДАЮТ"}
}

$ReplaceAll = 2
$FindContinue = 1
$MatchCase = $False
$MatchWholeWord = $True
$MatchWildcards = $False
$MatchSoundsLike = $False
$MatchAllWordForms = $False
$Forward = $True
$Wrap = $FindContinue
$Format = $False
     
#Начинаем вытягивать данные из Active Directory
[Void] [Reflection.Assembly]::LoadWithPartialName("Microsoft.Office.Interop.Word")
$WordTmpl = New-Object -comObject Word.Application
$WordTmpl.Visible = $True	
$objDoc = $WordTmpl.Documents.Open($fullPath)
$objSelection = $WordTmpl.Selection

#Определяем имя
$Bookmark = "displayName"  
$ReplaceText = $ADDisplayName
$RangeNew = $objDoc.Bookmarks.Item($Bookmark).Range 
$RangeNew.Text = $ReplaceText
    
    #Определяем должность
    $Bookmark = "title"  
    $ReplaceText = $ADTitle
    $RangeNew = $objDoc.Bookmarks.Item($Bookmark).Range 
    $RangeNew.Text = $ReplaceText 
    
    #Название компании
    $Bookmark = "company"  
    $ReplaceText = $ADCompany
    $RangeNew = $objDoc.Bookmarks.Item($Bookmark).Range 
    $RangeNew.Text = $ReplaceText 
    
    #Адрес пользователя с проверкой на заполнение атрибута
	$Bookmark = "streetAddress"
	If ($ADStreetAddress.ToString() -eq '') { 
		$ReplaceText = $DefaultAddress
	} Else {
        $ReplaceText = $ADStreetAddress.ToString()
	}
		$RangeNew = $objDoc.Bookmarks.Item($Bookmark).Range 
        $RangeNew.Text = $ReplaceText

    #Почтовый ящик пользователя с проверкой на заполнение атрибута
	$Bookmark = "postOfficeBox"
	If ($ADPOBox.ToString() -eq '') { 
		$ReplaceText = $DefaultPOBox
	} Else {
		$ReplaceText = $ADPOBox.ToString()
	}
        $RangeNew = $objDoc.Bookmarks.Item($Bookmark).Range 
        $RangeNew.Text = $ReplaceText

    #Город пользователя с проверкой на заполнение атрибута
	$Bookmark = "l"  
	If ($ADCity.ToString() -eq '') { 
		$ReplaceText = $DefaultCity
	} Else {
		$ReplaceText = $ADCity.ToString()
	}
		$RangeNew = $objDoc.Bookmarks.Item($Bookmark).Range 
        $RangeNew.Text = $ReplaceText

    #Рабочий телефон с проверкой на заполнение атрибута
	$Bookmark = "telephoneNumber"  
	If ($ADTelePhoneNumber.ToString() -eq '') { 
		$ReplaceText = $DefaultTelephone
	} Else {	
        $ReplaceText = $ADTelePhoneNumber.ToString()
	}
		$RangeNew = $objDoc.Bookmarks.Item($Bookmark).Range 
        $RangeNew.Text = $ReplaceText	

    #Номер факса с проверкой на заполнение атрибута
    $Bookmark = "facsimileTelephoneNumber"
    If ($ADFax.ToString() -eq '') {
        $ReplaceText = $DefaultFax
    } Else {
        $ReplaceText = $ADFax.ToString()
    }
    if ($objDoc.Bookmarks.Exists($Bookmark)) {
        $RangeNew = $objDoc.Bookmarks.Item($Bookmark).Range 
        $RangeNew.Text = $ReplaceText
    }	

    #Номер мобильного, учитывая то, что в тексте 2 раза упоминается mobile
    $FindText = "099"
    $ReplaceText = $ADMobile.ToString()
    $objSelection.Find.Execute($FindText,$MatchCase, $MatchWholeWord,$MatchWildcards,$MatchSoundsLike, $MatchAllWordForms,$Forward,$Wrap,$Format, $ReplaceText,$ReplaceAll)

    #Веб-сайт пользователя
    $Bookmark = "wWWHomePage"  
    $ReplaceText = $ADWebSite.ToString()
    $RangeNew = $objDoc.Bookmarks.Item($Bookmark).Range 
    $Url = $objDoc.Hyperlinks.Add($RangeNew,$ReplaceText)

    #E-mail пользователя
    $Bookmark = "mail"  
    $ReplaceText = $ADEmailAddress.ToString()
    $RangeNew = $objDoc.Bookmarks.Item($Bookmark).Range 
    $Email = $objDoc.Hyperlinks.Add($RangeNew,'mailto:'+ $ReplaceText)
	
	Write-Host 'Начинаем сохранять подписи'
			
	#Сохраняем в HTML
	$saveFormat = [Enum]::Parse([Microsoft.Office.Interop.Word.WdSaveFormat], "wdFormatHTML");
	$path = $LocalSignaturePath+'\'+$TemplateName+".htm"
	$WordTmpl.ActiveDocument.saveas([ref]$path, [ref]$saveFormat)
	
	#Сохраняем в RTF 
	$saveFormat = [Enum]::Parse([Microsoft.Office.Interop.Word.WdSaveFormat], "wdFormatRTF");
	$path = $LocalSignaturePath+'\'+$TemplateName+".rtf"
	$WordTmpl.ActiveDocument.SaveAs([ref] $path, [ref]$saveFormat)
			
	#Сохраняем в TXT    
	$saveFormat = [Enum]::Parse([Microsoft.Office.Interop.Word.WdSaveFormat], "wdFormatText");
	$path = $LocalSignaturePath+'\'+$TemplateName+".txt"
	$WordTmpl.ActiveDocument.SaveAs([ref] $path, [ref]$SaveFormat)
	$WordTmpl.ActiveDocument.Close()
	$WordTmpl.Quit() 
} 
exit

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

Как видите, скрипт позволяет настраивать пользователям уникальные корпоративные подписи с правильно заполненными данными о самих владельцах такой подписи. И, несмотря на 183 строки кода, скрипт оказался простым и гибким. А если вы предложите варианты усовершенствования кода – буду рад внести соответствующие изменения.

VN:F [1.9.22_1171]
Rating: 8.0/10 (7 votes cast)
Совет недели (2014-05-3): Создание уникальных подписей Outlook при помощи Windows PowerShell, 8.0 out of 10 based on 7 ratings

13 комментариев

  1. Странно вы берете члена объединения:
    $saveFormat = [Enum]::Parse([Microsoft.Office.Interop.Word.WdSaveFormat], «wdFormatRTF»);

    Почему не написать нормально:
    $saveFormat = [Microsoft.Office.Interop.Word.WdSaveFormat]::wdFormatHTML;?

    VA:F [1.9.22_1171]
    Rating: 0 (from 0 votes)
  2. И еще моментик:
    строку
    $fullPath = $LocalSignaturePath+’\’+$TemplateName+’.docx’
    нужно перенести перед комментарием
    #Копируем файл, если таковой отсутствует в целевой папке или если его хэш отличается от исходного
    иначе Test-Path ругнется

    VA:F [1.9.22_1171]
    Rating: 0 (from 0 votes)
    1. Спасибо, что заметили. Это я уже когда переносил в docx-файл с описанием действий не туда эту строку записал. Исправил..

      VN:F [1.9.22_1171]
      Rating: 0 (from 0 votes)
    1. Не проверял, если честно.. Данный скрипт создает только лишь саму подпись, а для импорта нужно воспользоваться тем методом, о котором я писал предыдущей статье. Там уже нужно будет для каждой версии создавать свои элементы предпочтения, т.к. разделы реестра будут отличаться. Если нужно — могу проверить совместимость с остальными версиями 🙂

      VN:F [1.9.22_1171]
      Rating: 0 (from 0 votes)
      1. Подскажите, стоит задача чтоб все ссылки (Email, www) не подчёркивались и были определённого цвета, как это реализовать?

        VA:F [1.9.22_1171]
        Rating: 0 (from 0 votes)
  3. Скажите пожалуйста а можно сделать так:
    Если поле мобильный телефон в АД не заполнено строку моб: в подписи не ставить?

    VA:F [1.9.22_1171]
    Rating: 0 (from 0 votes)
  4. Попробовал — получилось. Только возник вопрос: как сделать невидимым изменение (сохранение) подписи (пользователи зазвонят: word сам открывается-закрывается)

    VA:F [1.9.22_1171]
    Rating: 0 (from 0 votes)
    1. Тут, скорее всего, нужно будет полностью перепиливать весь скрипт.. Подумаю по свободному времени 🙂

      VN:F [1.9.22_1171]
      Rating: 0 (from 0 votes)
  5. Подскажите пожалуйста, как сделать адрес почты юзера в виде текста, но чтобы это была гиперссылка.
    В вашем скрипте гиперссылка видна и кликабельна только при нажатии на email, но самого адреса почты в подписи нет.
    Спасибо.

    VA:F [1.9.22_1171]
    Rating: 0 (from 0 votes)

Leave a Reply

Ваш e-mail не будет опубликован. Обязательные поля помечены *