image

1. Пара слов от автора


Статья обновлена. Скрипты изменены. Добавлен скрипт для обновления одного списка рассылки.

В комментариях к прошлой статье мне задали интересный вопрос об автоматическом формировании списков рассылки на основе групп безопасности AD. Есть задача – есть решение. Итак, поехали.

2. Исходные данные


ОС сервера: CentOS 7

По поводу ОС
На самом деле разница между CentOS7 и любой другой системой будет заключаться исключительно в командах серверу на установку пакетов, и, возможно, расположении некоторых файлов. Работа ведется в основном с командлетами Zimbra, так что отличия настройки будут минимальны.

Домен Zimbra: zimbramail.home.local
Путь монтирования шары на хосте Zimbra: /mnt/ZM/

3. Настройка


  1. Монтируем шару Windows к нашему Linux серверу. Это нужно для упрощения и автоматизации передачи данных из Windows PowerShell в Linux Bash. Процедура монтирования была описана в предыдущей статье. Не буду повторяться.
  2. Создаем в AD отдельное OU, в котором создаем группы, на основе которых будут созданы списки рассылки в Zimbra. Имя группы = имя списка рассылки.
  3. Добавляем в группы, созданные в новом OU, пользователей или группы безопасности, на основе которых будут наполняться списки рассылки в Zimbra. Скрипт отрабатывает рекурсивно, что значит, что он соберет все данные о пользователях, состоящих в группах, которые добавлены в группы в целевом OU. Подробнее о выводе команды Get-ADGroupMember.
  4. Создаем скрипт сбора данных из Active Directory.
  5. Создаем скрипт добавления листов рассылки и их наполнения пользователями на основе полученных данных в предыдущем скрипте.
  6. Наслаждаемся.


3.1. По поводу OU


Я создал OU “ZimbraDL” в корне домена и запретил ему наследование групповых политик, чтобы эти группы оставались обособленными. Они не будут участвовать в жизни домена никак, помимо формирования Distribution Lists в Zimbra Collaboration OSE.

4. Скрипт на PowerShell для сбора данных из AD


Скрипт PowerShell
$Path = "C:\ZM\ZimbraDL"
$enc = [system.text.encoding]
function ReCode ( $f, $t, $line ) 
{
    $cp1          = $enc::getencoding( $f )
    $cp2          = $enc::getencoding( $t )
    $inputbytes   = $enc::convert( $cp1, $cp2, $cp2.getbytes( $line ))
    $outputstring = $cp2.getstring( $inputbytes )
    $outputstring | add-content $OutputFile
}

#Очистка каталога
if(test-path $Path)
{ 
    Remove-Item $Path -Recurse -Force 
}

#Создание рабочих директорий
if(!(Test-Path $Path))
{
    New-Item -ItemType Directory -Force -Path $Path
}
if(!(Test-Path $Path\Groups))
{
   New-Item -ItemType Directory -Force -Path $Path\Groups
}
if(!(Test-Path $Path\Users))
{
   New-Item -ItemType Directory -Force -Path $Path\Users
}
if(!(Test-Path $Path\UsersTemp))
{
    New-Item -ItemType Directory -Force -Path $Path\UsersTemp
}

#Создание списка групп
Import-Module ActiveDirectory
Get-AdGroup -filter * -SearchBase "OU=ZimbraDL,DC=home,DC=local" | select samaccountname | Out-File $Path\Groups\GetGroupsAD.txt

#Форматирование списка групп
(Get-Content "$Path\Groups\GetGroupsAD.txt") -notmatch "samaccountname" | where {$_ -ne ""} | where {$_ -ne "--"} | Where-Object {$_ -notmatch '-'} | out-file "$Path\Groups\GetGroupsAD.txt"
$File = @(Get-Content $Path\Groups\GetGroupsAD.txt)
foreach ($File1 in $File) 
{
    $string=$File1.TrimStart('"')
    $string=$string.TrimEnd('"')
    $string=$string.TrimStart(' ')
    $string=$string.TrimEnd(' ') | Out-File $Path\Groups\GroupsListTemp.txt -Append
}

#Удаление временного файла
Remove-Item $Path\Groups\GetGroupsAD.txt -Force

$InputFile = gc $Path\Groups\GroupsListTemp.txt
$OutputFile = "$Path\Groups\GroupsList.txt"

#Исправление кодировки
foreach ($line in $InputFile) 
{
    ReCode -f "windows-1251" -t "utf-8" $line
}

#Удаление временного файла
Remove-Item $Path\Groups\GroupsListTemp.txt -Force
 
#Создание файлов со списком пользователей для каждой группы
$GroupName = @(Get-Content $Path\Groups\GroupsList.txt)
Foreach ($Group in $GroupName)
{
    Get-ADGroupMember $Group -recursive | ft SamAccountName | out-file "$Path\UsersTemp\$Group.txt" -Append
    (get-content "$Path\UsersTemp\$Group.txt")  -notmatch "Name" | where {$_ -ne ""} | where {$_ -ne "--"} | Where-Object {$_ -notmatch '-'} | out-file "$Path\UsersTemp\$Group.txt"
    $File=@(Get-Content $Path\UsersTemp\$Group.txt)
    foreach ($File1 in $File) 
    {
        $string=$File1.TrimStart('"')
        $string=$string.TrimEnd('"')
        $string=$string.TrimStart(' ')
        $string=$String.TrimEnd(' ') | Out-File "$Path\UsersTemp\$Group" -Append
    }
    $InputFile = gc $Path\UsersTemp\$Group
    $OutputFile = "$Path\Users\$Group"

    #Исправление кодировки
    foreach ($line in $InputFile) 
    {
        ReCode -f "windows-1251" -t "utf-8" $line
    }
}

#Очистка лишних временных директорий
Remove-Item "$Path\UsersTemp\" -Recurse -Force
Remove-Item "$Path\Groups" -Recurse -Force


4.1. Как работает скрипт


  1. Сначала происходит проверка существования и удаление рабочей директории, если она существует. Это нужно, чтобы в процессе работы данные не задвоились.
  2. PoSh смотрит в указанное OU, считывает группы пользователей, находящиеся в нем и записывает их в файл GetGroupsAD.txt
  3. Отбрасывает из полученного файла все лишнее (PoSh пишет в файл весь свой вывод, так что в изначальном выводе команды первой строкой идет Name, второй строкой разделитель "----", и только после этого перечисляются группы по одной на строку), меняет кодировку с «windows-1251» на utf-8, результатом чего является другой файл GroupsList.txt
  4. Далее на основе полученного файла считывается информация о пользовтелях групп, содержащихся в файле. Файлы, содержащие в себе имена пользователей (samAccountName)
    помещаются в директорию \Users и называются по именам групп


4.2. Скрипт для считывания информации с отдельно взятой группы


Скрипт с возможностью считать данные только одной группы безопасности мало чем отличается от предыдущего, в основм тем, что в не�� есть блок с предложением пользователю ввести имя группы, на основе которой нужно будет обновить список рассылки.

Скрипт PowerShell для запуска руками с возможностью считать данные только одной группы
$Path = "C:\ZM\ZimbraDL"
$enc = [system.text.encoding]
function ReCode ( $f, $t, $line ) 
{
    $cp1          = $enc::getencoding( $f )
    $cp2          = $enc::getencoding( $t )
    $inputbytes   = $enc::convert( $cp1, $cp2, $cp2.getbytes( $line ))
    $outputstring = $cp2.getstring( $inputbytes )
    $outputstring | add-content $OutputFile
}

#Очистка каталога
if(test-path $Path)
{ 
    Remove-Item $Path -Recurse -Force 
}

#Создание рабочих директорий
if(!(test-path $Path))
{
    New-Item -ItemType Directory -Force -Path $Path
}
if(!(Test-Path $Path\Groups))
{
   New-Item -ItemType Directory -Force -Path $Path\Groups
}
if(!(Test-Path $Path\Users))
{
   New-Item -ItemType Directory -Force -Path $Path\Users
}
if(!(Test-Path $Path\UsersTemp))
{
    New-Item -ItemType Directory -Force -Path $Path\UsersTemp
}

#Создание списка групп
Import-Module ActiveDirectory
$Groupname = Read-Host 'Введите имя группы, которую нужно обновить, или ALL для обновления всех списков рассылки'
If ($Groupname -eq "ALL")
{
    Get-AdGroup -filter * -SearchBase "OU=ZimbraDL,DC=home,DC=local" | select samaccountname | Out-File $path\Groups\GetGroupsAD.txt
}
Else
{
    $Groupname > "$Path\Groups\GetGroupsAD.txt"
}    

#Форматирование списка групп
(Get-Content "$Path\Groups\GetGroupsAD.txt") -notmatch "samaccountname" | where {$_ -ne ""} | where {$_ -ne "--"} | Where-Object {$_ -notmatch '-'} | out-file "$Path\Groups\GetGroupsAD.txt"
$File = @(Get-Content $Path\Groups\GetGroupsAD.txt)
foreach ($File1 in $File) 
{
    $string=$File1.TrimStart('"')
    $string=$string.TrimEnd('"')
    $string=$string.TrimStart(' ')
    $string=$string.TrimEnd(' ') | Out-File $Path\Groups\GroupsListTemp.txt -Append
}
Remove-Item $Path\Groups\GetGroupsAD.txt -Force
$InputFile = gc $Path\Groups\GroupsListTemp.txt
$OutputFile = "$Path\Groups\GroupsList.txt"
foreach ($line in $InputFile) 
{
    ReCode -f "windows-1251" -t "utf-8" $line
}
Remove-Item $Path\Groups\GroupsListTemp.txt -Force
 
#Создание файлов со списком пользователей для каждой группы
$GroupName = @(Get-Content $Path\Groups\GroupsList.txt)
Foreach ($Group in $GroupName)
{
    Get-ADGroupMember $Group -recursive | ft SamAccountName | out-file "$Path\UsersTemp\$Group.txt" -Append
    (get-content "$Path\UsersTemp\$Group.txt")  -notmatch "Name" | where {$_ -ne ""} | where {$_ -ne "--"} | Where-Object {$_ -notmatch '-'} | out-file "$Path\UsersTemp\$Group.txt"
    $File=@(Get-Content $Path\UsersTemp\$Group.txt)
    foreach ($File1 in $File) 
    {
        $string=$File1.TrimStart('"')
        $string=$string.TrimEnd('"')
        $string=$string.TrimStart(' ')
        $string=$String.TrimEnd(' ') | Out-File "$Path\UsersTemp\$Group" -Append
    }
    $InputFile = gc $Path\UsersTemp\$Group
    $OutputFile = "$Path\Users\$Group"
    foreach ($line in $InputFile) 
    {
        ReCode -f "windows-1251" -t "utf-8" $line
    }
}
Remove-Item "$Path\UsersTemp\" -Recurse -Force
Remove-Item "$Path\Groups" -Recurse -Force



5. Скрипт на Bash для создания списков рассылки


Оговорюсь по поводу копирования файлов-скриптов, созданных под Windows
В прошлой статье был описан метод форматирования файлов с помощью команды cat, которая, будучи запущенная с определенным ключом, убирает все лишние нечитаемые символы. Ссылка на статью в конце статьи.

Скрипт Bash
#!/bin/bash
#Определение переменных
#Путь к рабочему каталогу
Path="/mnt/ZM/ZimbraDL"
#имя домена Zimbra
Domain="zimbramail.home.local"
#путь к командлету zmprov
zmprov="/opt/zimbra/bin/zmprov"
#путь к лог-файлу
log="/mnt/ZM/DLlog.txt"
#путь ко временному файлу со списком групп
DLnames="/mnt/ZM/DLnames"
#путь ко временному файлу со списком пользователей
UserNames="/mnt/ZM/Usernames"
#конец блока переменных

echo "Запись списка групп..."
ls $Path/Users > $DLnames
if [ $? -eq 0 ]; then
	echo -n "$(tput hpa $(tput cols))$(tput cub 6) [OK]"
	echo
	echo -en "ls directory for Groups correct $(date +%T)\n" >> $log
else
	echo -n "$(tput hpa $(tput cols))$(tput cub 6) [FAIL]"
	echo
	echo -en "ls directory for Groups INcorrect $(date +%T)\n" >> $log
fi
#Удаление списков рассылки
echo "Удаление обновляемых списков рассылки"
for DLname in $( cat $DLnames); do
	#Проверка существования списка рассылки
	echo "Проверка существования списка рассылки $DLname..."
	Result=$($zmprov gdl $DLname@$Domain)
	if [ $? -eq 0 ]; then
		echo -n "$(tput hpa $(tput cols))$(tput cub 6)[OK]"
		echo
		echo -en "DL $DLname exist $(date +%T)\n" >> $log
		#Удаление списка рассылки
		echo -en "Start deleting DL for group $DLname $(date +%T)\n" >> $log
		echo "Удаление списка рассылки $DLname..."
		$zmprov ddl $DLname@$Domain
		if [ $? -eq 0 ]; then
			echo -n "$(tput hpa $(tput cols))$(tput cub 6) [OK]"
			echo
			echo -en "DL for group $DLname is deleted in $(date +%T)\n" >> $log
		else
			echo -n "$(tput hpa $(tput cols))$(tput cub 6) [FAIL]"
			echo
			echo -en "DL for group $DLname is NOT deleted in $(date +%T)\n" >> $log
		fi
	else
		echo -n "$(tput hpa $(tput cols))$(tput cub 6)[FAIL]"
		echo
		echo -en "DL $DLname not exist! $(date +%T)\n" >> $log
	fi	
done
for DLname in $( cat $DLnames); do
	#Создание списка рассылки
	echo -en "Start create DL for group $DLname $(date +%T)\n" >> $log
	echo "Создание списка рассылки для группы AD $DLname..."
	$zmprov cdl $DLname@$Domain
	if [ $? -eq 0 ]; then
		echo -n "$(tput hpa $(tput cols))$(tput cub 6) [OK]"
		echo
		echo -en "DL for group $DLname is created in $(date +%T)\n" >> $log
	else
		echo -n "$(tput hpa $(tput cols))$(tput cub 6) [FAIL]"
		echo
		echo -en "DL for group $DLname is NOT created in $(date +%T)\n" >> $log
	fi
	#Наполнение списка рассылки
	echo "Наполнение списка рассылки"
	for UserName in $( cat $Path/Users/$DLname); do
		echo "Проверка существования ящика для пользователя $UserName..."
		Result=$($zmprov gmi $UserName@$Domain)
		if [ $? -eq 0 ]; then
			echo -n "$(tput hpa $(tput cols))$(tput cub 6) [OK]"
			echo
			echo -en "MilBox for user $UserName exist $(date +%T)\n" >> $log
			echo "Добавление пользователя $UserName в список рассылки $DLname@$Domain..."
			$zmprov adlm $DLname@$Domain $UserName@$Domain
			if [ $? -eq 0 ]; then
				echo -n "$(tput hpa $(tput cols))$(tput cub 6) [OK]"
				echo
				echo -en "User $UserName added in $DLname@$Domain correctly in $(date +%T)\n" >> $log
			else
				echo -n "$(tput hpa $(tput cols))$(tput cub 6) [FAIL]"
				echo
				echo -en "DL for group $DLname is NOT created in $(date +%T)\n" >> $log
			fi
		else
			echo -n "$(tput hpa $(tput cols))$(tput cub 6) [FAIL]"
			echo
			echo -en "MilBox for user $UserName is NOT exist $(date +%T)\n" >> $log
		fi
	done
done
#Очистка временных файлов
echo "Очистка временных файлов"
echo "Очистка файла со списком групп..."
echo -n > $DLnames
if [ $? -eq 0 ]; then
	echo -n "$(tput hpa $(tput cols))$(tput cub 6)[OK]"
	echo
	echo -en "File $DLnames was successfull cleared in $(date +%T)\n" >> $log
else
	echo -n "$(tput hpa $(tput cols))$(tput cub 6) [FAIL]"
	echo
	echo -en "File $DLnames was NOT cleared in $(date +%T)\n" >> $log
fi
#Удаление директорий, содержащих структуру групп и пользователей
echo "Удаление рабочего каталога $Path для очистки свободного пространства..."
rm -rf $Path
if [ $? -eq 0 ]; then
	echo -n "$(tput hpa $(tput cols))$(tput cub 6)[OK]"
	echo
	echo -en "Directory $Path was seccessfull deleted in $(date +%T)\n" >> $log
else
	echo -n "$(tput hpa $(tput cols))$(tput cub 6) [FAIL]"
	echo
	echo -en "Directory $Path was NOT deleted in $(date +%T)\n" >> $log
fi
#запись в лог-файл времени окончания обновления списков рассылки
echo -en "Job complete in $(date +%T)\n" >> $log
echo -en "____________________________________\n" >> $log


5.1. Как работает скрипт


  1. Записать в файл список групп
  2. Проверить существование списка рассылки в Zimbra, при существовании – удалить его
  3. Поочередно создать списки рассылки на основе списка групп, наполняя каждый из них пользователями (на экран выводится ID списка рассылки, ��то стандартный вывод командлета zmprov при создании DL). При этом проверяется существование почтовых ящиков пользователей в Zimbra, и если ящика не существует, то пользователь не будет добавлен в список рассылки. Можно, конечно, создать пользователю новый почтовый ящик и добавить его в список рассылки, но я исходит из того, что Zimbra autoprov работает в режиме Eager, и если пользователь не создался автоматически, то ему и нечего делать в системе
  4. Очистить временные файлы
  5. Удалить рабочую директорию

6. Заключение


В целом, задача не сложная, проблема была лишь в передаче данных из PowerShell в Bash. Я довольно долго пытался найти инструмент для рекодирования текстовых файлов с выводом PoSh в удобоваримый для Bash вид. Результатом многодневных поисков стала функция:

Функция рекодирования
$InputFile = gc File1.txt
$OutputFile = "File2.txt"
$enc = [system.text.encoding]
function ReCode ( $f, $t, $line ) 
{
    $cp1          = $enc::getencoding( $f )
    $cp2          = $enc::getencoding( $t )
    $inputbytes   = $enc::convert( $cp1, $cp2, $cp2.getbytes( $line ))
    $outputstring = $cp2.getstring( $inputbytes )
    $outputstring | add-content $OutputFile
}
foreach ($line in $InputFile) 
{
    ReCode -f "windows-1251" -t "utf-8" $line
}


Может, кому-то будет полезно.

7. P.S.:


Это третья статья из серии «как я «Zimbra» внедрял». Первая, про внедрение, LDAP-авторизацию и автоматическое создание ящиков для пользователей AD, вот тут. Вторая, про настройку резервного копирования и восстановления Zimbra целиком и отдельными ящиками – тут.

8. P.S.S.:


Есть более простой способ, описанный тут товарищем kiby75