Perl для системного администрирования

         

Информация о модулях из этой главы



Информация о модулях из этой главы

Модуль Идентификатор на CPAN Версия
Mac: :Glue CNANDOR 0.58
Win32 . : OLE (входит в состав ActiveState Perl) JDB 1.11
Mail: : Mailer (можно найти в MailTools)

GBARR 1.13
Text::Wrap (можно найти в Text-Tabs+Wrap, также распространяется с Perl) MUIR 98.112902
10: '.Socket (можно найти в 10, кроме того, распространяется с Perl) GBARR 1.20
Mail : : Internet (можно найти в MailTools) GBARR 1.13
Mail: : Heade r (можно найти в MailTools) GBARR 1.13
Mail: : Folder: :Mbox (можно найти в Mail: : Folder) KJOHNSON 0.07
Socket (распространяется с Perl)
BerkeleyDB PMQS 0.10
Net: :Tclnet JROGERS 3.01
DB_File (распространяется с Perl) PMQS 1.72


Использование IPC специфичных для операционной системы



Использование IPC, специфичных для операционной системы

В MacOS или Windows NT можно управлять почтовым клиентом, используя IPC (Interprocess Communication, межпроцессные взаимодействия).

Я не знаю о существовании версий sendmail для MacOS, но в нем для управления почтовым клиентом можно применять AppleScript:

$to="someone\@example.com"; $from="me\@example.com"; $subject="Hi there"; $body="message body\n";

MacPerl: :DoAppleScnpt(«EOC); tell application "Eudora"

make message at end of mailbox "out"

-- 0 is the current message

set field \"from\" of message 0 to \"$frorn\"

set field \"to\" of message 0 to \"$to\"

set field \"subject\" of message 0 to \"$subject\"

set body of message 0 to \"$body\"

queue message 0

connect with sending without checking

quit

end tell EOC В этом примере запускается очень простая программа AppleScript, которая общается с почтовым клиентом Eudora. Сценарий создает новое сообщение, помещает его в очередь для отправки, а затем отдает инструкции почтовому клиенту об отправке сообщения из очереди перед выходом.

Еще один, более эффективный способ написать подобный сценарий состоит в том, чтобы использовать модуль Мае: : G1 ие, уже рассмотренный в главе 2 «Файловые системы».

use Mac::Glue ':glue ;

$e=new Mac::Glue 'Eudora';

$to="someone\@example.com"; $f rom="me\(<}>exainple. com";

$subject="Hi there"; $body="<riessage body";

$e->make(

new => 'message',

at => location(end => $e->obj(mailbox => 'Out')) );

$e->set($e->obj(field => from => message => 0), to =4 $froin):

$e->set($e->obj(field => to => message => 0), to => $to);

$e->set($e->ob](field => subject => message => 0), to => Ssubject);

$e->set($e->prop(body => message => 0), to => $body);

$e->queue($e->obj(message => 0)); $е->соппесц sending => 1, checking => 0);

$e->quit;

В NT можно обратиться к библиотеке Collaborative Data Objects Library (раньше она называлась Active Messaging), простой в использовании надстройке на архитектуре MAPI (интерфейс прикладного программирования систем передачи сообщений). Вызвать эту библиотеку для управления таким почтовым клиентом, как Outlook можно, применив модуль Win32: :OLE следующим образом:

$to="me\(Sexample.com"; $subject="Hi there";

$body="message body\n";

use Win32::OLE;

# инициализируем OLE и COINIT_OLEINITIALIZE,

необходимые при и использовании объектов MAPI.Session

Win32: :OLE->Initialize(Win32: :OLE: :COINIT__OLEINITIALI7E): die Win32: :OLE->LastEr'-or(), "\n"

if Win32: :OLE->LastErrc-( V

создаем объект сессии, который вызовет logoff при уничтожени

my Ssession = Win32::OLE->new('MAPI.Session','Logoff);

die Win32::OLE->LastError();"\n" if Win32::OLE->LastError();

№ регистрируемся в этой сессии, используя OL98 Internet Profile по

$session->Logon('Microsoft Outlook Internet Settings').

die Win32: :OLE->LastError(),"\n" if Win32::OLE->LastError();

создаем обьект message

my Smessage = $session->Outbox->Messages->Add:

die Win32:'OLE->LastError(),"\n" if Win32::OLE->LastError():

Я создаем обьект recipient

my Srecipient = $message->Recipients->Add;

die Win32::OLE->LastError(),"\n" if Win32::OLE->LastError();

заполняем данными объект recipient

$recipient->{Name} = $to;

$recipient->{Type} = 1; n 1 = "To:", 2 = "Cc:", 3 = "Вес:"

все адреса должны быть расшифрованы по справочнику

# (в этом случае, скорее всего, по вашей адресной книге)

Полные адреса расшифровываются сами в себя, так что эта

строка в большинстве случаев не изменит обьект recipient

$ recipient->Resolve();

die Win32: :OLE->LastError(). "\r> if Win32: :OLE->LastError();

tt заполняем строку Subject: и тело сообщения

$message->{Subject} = Ssubject; $message->{Text} = Sbody;

tt помещаем сообщение в очередь для отправки

1-й аргумент = сохранить копию сообщения

2-й аргумент = позволить пользователю изменить сообщение

tt перед отправкой в диалоговом окне

№ 3-й аргумент = родительское окно диалога, если 2-й аргумент True

$message->Send(0, О, О):

die Win32::OLE->LastError(),"\n" if Win32::OLE->LastError();

tt явно уничтожить объект Ssession, вызвав

$session->Logoff undef Ssession,

В отличие от предыдущего примера, программа всего лишь помещает письмо в очередь. Это уже дело почтового клиента (такого как Outlook) или транспортного агента (например Exchange) периодически инициировать отправку почты. Существует CDO/AM 1.1 - метод для объекта Session под названием DeiiverNow(), обращающийся к MAPI с заданием очистить все очереди входящих и исходящих сообщений. К сожалению, в некоторых ситуациях он недоступен или не работает, поэтому его нет и в предыдущем примере. В упомянутом примере управление MAPI производится «вручную» при помощи вызовов OLE. Если вы хотите использовать MAPI, «не пачкая рук», можно применить модуль Win32: :МАР1, который берет на себя все функции (модуль находится на http://www.generation.net/ -aminer/Perl/ ).

Программы, полагающиеся на AppleScript/Apple Events или MAPI, так же непереносимы, как и вызов программы sendmall. Они берут на себя часть работы, но относительно неэффективны. К этим методам нужно прибегать в последнюю очередь.



Электронная почта



Электронная почта

Отправка почты Распространенные ошибки при отправке почты. Получение почты Информация о модулях из этой главы Рекомендуемая дополнительная литература

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

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

Также мы рассмотрим, применение Perl для обработки входящей почты, повышая ее эффективность. Perl поможет бороться со спамом и разбираться с вопросами пользователей.

Будем впредь считать, что у вас уже есть надежная, работоспособная почтовая система. Кроме того, будем исходить из того, что в ней применяются протоколы, соответствующие спецификации IETF для отправки и получения почты. В примерах этой главы используется протокол SMTP (простой протокол передачи почты, RFC821), а сообщения соответствуют RFC822. Но всему свое время.



Контроль над частотой отправки почты



Контроль над частотой отправки почты

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

$last_sent = time;

Если программа запускается один раз в N минут или часов через сгоп в Unix или механизмы планирования задач NT, эту информацию можно переписать в файл, состоящий из одной строки, и считывать его при следующем запуске программы. В подобном случае обязательно обратите внимание на меры предосторожности, перечисленные в главе 1 «Введение».

В зависимости от ситуации можно поэкспериментировать с временем задержки. В этом примере показана экспоненциальная задержка (exponential backoff):

$max = 24*60*60; и максимальная задержка в секундах (1 день)

Sunit = 60;

увеличиваем задержку относительно этого значения (1минута)

# интервал времени, прошедший с момента отправки предыдущего

# сообщения и последняя степень 2, которая использовалась для

# расчета интервала задержки. Созданная нами подпрограмма

# возвращает ссылку на анонимный массив с этой информацией

sub time_closure {

my($stored_sent,$stored_power)=(0,-1); return sub {

(($stored_sent,$stored_power) = @_) if @_; [$stored_sent,$stored_power]; > };

$last_data=&time_closure; # создаем замыкание

ft возвращаем значение "истина" при первом вызове и затем после

# задержки

sub expbackoff {

my($last_sent,$last_power) = @{&$last_data};

# возвращаем true, если это первое наше обращение или если

# текущая задержка истекла с тех пор, как мы спрашивали

последний раз. Если мы возвращаем значение true, мы

запоминаем время последнего утвердительного ответа и

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

if (!$last_sent or ($last_sent +

(($unit -.$last_power >= $max) 9

$max : $unit * 2**$last_power) <= time())){

&$last_data(time().++$last„power); return 1;

}

else {

return 0; } >

Подпрограмма expbackoffQ возвращает значение true (1), если нужно отправить сообщение, и false (0), если нет. При первом вызове она возвращает true, а затем быстро увеличивает время задержки до тех пор, пока значение t rue не станет появляться лишь раз в день.

Чтобы сделать программу более интересной, я применил необычную конструкцию под названием замыкание (closure) для хранения времени последней отправки сообщения и последней степени двойки, используемой для расчета задержки. Замыкание используется как способ скрытия важных переменных от остальной программы. В данной маленькой программе это было сделано из любопытства, но польза от такой технологии очень быстро становится очевидной в программах большего размера, где более вероятно, что другой код может случайно перезаписать значения этих переменных. Вот, вкратце, как работают замыкания.

Подпрограмма &time_closure() возвращает ссылку на анонимную подпрограмму, по существу, на небольшой отрывок кода без имени. Позже данная ссылка будет вызывать этот код, используя стандартный синтаксис символических ссылок: &$last_data. Код из анонимной подпрограммы возвращает ссылку на массив, поэтому и используется такая масса знаков пунктуации, чтобы получить доступ к возвращаемым данным:

my($last_sent,$last_power) = @{&$last_data};

Вот и вся тайна, которая скрывается за замыканиями: поскольку ссылка создается в том же блоке, что и переменные $stored_seri: и $sto-red_power (посредством my()), то они схватываются в уникальном контексте. Переменные $stored_sent и $stored_power можно прочитать и изменить только при выполнении кода из этой ссылки. Кроме того, они сохраняют свои значения между вызовами. Например:

создаем замыкание $last_data=&time._closure:

вызываем подпрограмму, устанавливающую значения переменных

&$last_data(1,1);

и пытаемся изменить их за пределами подпрел раммы

$stored__sent - $stored_power = 2:

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

print "@{&$last_data}\n":

Результатом выполнения этого кода будет "1 1", хотя и создается впечатление, что в третьей строке были изменены значения переменных $stored_sent и $stored_power. Да, значения глобальных переменных с теми же именами были изменены, но невозможно затронуть копии, защищенные замыканиями.

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

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

$тах = 60*60»24;

максимальная задержка в секундах (1 день)

$min = 60*5; tt минимальная задержка в секундах (5 минут)

$unit = 60; tt уменьшаем задержку относительно этого значения (1 минута)

$start_power = int log($max/$unit)/log(2): # ищем ближайшую степень двойки

sub time_closure {

my($last_sent,$last_power)=(0,$start_power+l); return sub {

(($last_sent, $last_p<wer) = @_) if ё>_; n keep exponent positive

$last_power = ($last_power > 0) 9

$last:_power : 0; [Siast^sent,$last_power]; } };

$last_data=&t ime_clusiire;

n создаем замыкание

возвращаем ггче при первом вызове и затем после роста

it экспоненты sub exprampup {

my($last_sent,$last_power) = @{&$last_data}.

возвращаем true, если это первое обращение или если

текущая задержка истекла с момента последнего обиащен/я.

Если сообщение отправляется, то мш запоминаем время

последнего ответа и увеличиваем

$min : $unit * 2**$last_power) <= time())){

&$last_data(time(),++$last_power1): return 1;

}

else

{

return 0; } }

В обоих примерах вызывалась дополнительная подпрограмма (&$last_data), которая позволяла выяснить, когда было отправлено последнее сообщение и как вычислялась задержка. Позже, при необходимости изменить программу, такое деление позволит изменить способ хранения состояния. Например, если переписать программу так, чтобы она выполнялась периодически, а не постоянно, то замыкание совсем нетрудно заменить обычной подпрограммой, сохраняющей нужные данные в текстовом файле и потом считывающей их оттуда.



Контролируем количество сообщений



Контролируем количество сообщений

Другая разновидность синдрома «чрезмерной отправки почты» - это проблема «каждый в сети за себя». Если все машины из сети решат послать вам чуточку почты, вы вполне можете пропустить что-то действительно важное в этом потоке сообщений. Было бы лучше, если бы все сообщения отправлялись в центральный репозиторий. А затем в собранном виде почта поступала бы в одном сообщении.

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

имя-узла

удача-или-неудача

количество-завершенных-вычислений

Программа, проверяющая эту информацию и отсылающая результаты, может выглядеть так:

use Mail::Mailer; use Text::Wrap;

tf список машин, отправляющих сообщения

$repolist = "/project/machinelist";

ft каталог, куда они записывают файлы

Srepodir = "/project/reportddir";

it

разделитель файловой системы, используется для переносимости.

Можно было бы использовать модуль File::Spec

$separator= "/";

# отправляем почту "с" этого адреса

$reportfromaddr = "project\@example. corn";

# отправляем почту на этот адрес Sreporttoaddr = "project\@example.com";

tf считываем список машин в хэш. Позже будем вынимать из этого

# хэша по мере доклада машин, оставив только те машины, которые

не принимали участие в действии

open(LIST,$repolist) or die "Невозможно открыть список

$repolist:$!\n"; while(<LIST>){

chomp;

$missing{$_}=1;

$machines++; }

# считываем все файлы из центрального каталога

и замечание: этот каталог должен автоматически очищаться другим

# сценарием

opendir(REPO,Srepodir) or die "Невозможно открыть каталог $repodir:$!\n";

while(defined($statfile=readdir(REPO))){

next unless -f Srepodir.$separator.$statfile;

# открываем каждый файл и считываем информацию о состоянии

open(STAT,$repodir.$separator.$statfile) or die "Невозможно открыть Sstatfile:$!\n";

chornp($report = <STAT>);

($hostname.$result,$details)=spiit(' ',$report,3);

warn "В файле Sstatfile утверждается, что он был сгенерирован машиной

Shostname1 \n" if($hostname ne Sstatfile);

имя узла больше не считается пропущенным

delete $missing{$nostname}; # заполняем значениями хэши

(Sresult eq "success")}

$success{$nostname}=$details;

$succeeded++: } else {

$fail{$hostname}=$details:

$failed++; }

close(STAT); } closedir(REPO);

# создаем информативный заголовок для сообщения if (Ssucceeded == $machines){

Ssubject = "[report] Success: $machines"; }

elsif ($failed == Smachines or scalar keys %missing >= Smachines) {

Ssubject = "[report] Fail: Smachines"; } else

{ ;

Ssubject = "[report] Partial: Ssucceeded ACK, Sfailed NACK".

((%missing) ? ", ".(scalar keys %missing)1." MIA" : ""); }

# создаем объект mailer и заполняем заголовки $type="sendmail";

my Smaller = Mail::Mailer->new($type) or die "Невозможно создать новый объект:$!\п";

$mailer->open({From=>$reportf romaddr,

To=>$reporttoaddr. Subject=>$subject})! or die "Невозможно заполнить объект mailer:$!\n";

» создаем тело сообщения !

print $mailer "Run report from $0 on " . scalar localtime(tine) . "\n":

if (keys %success){

print Smaller "\n==Succeeded==\n";

foreach $hostname (sort keys %success){

print Smaller "$hostname: $success{$hostname}\n":

} } 308

if (keys %fail){

print Smaller "\n==Failed==\n";

foreach Snostname (sort, keys %fail){

print Smaller "Shostname: $fail{$hostname}\n"

} }

if (keys %missing){

print Smaller "\n==Missing==\n";

print Smaller wrap("","".join(" ".sort keys %missing)),"\n"; }

# отправляем сообщение $mailer->close;

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

Получается такой отчет:

Date: Wed, 14 Apr 1999 13:06:09 -0400 (EOT)

Message-Id: <199904141706.NAA08780@example.com> Subject: [report]

Partial: 3 ACK, 4 NACK, 1 MIA To: project(s>example. con From: project@example.com

Run report from reportscript on Wed Apr 14 13:06:08 1999

==Succeeded==

barney: computed 23123 oogatrons betty: computed 6745634

oogatrons fred: computed 56344 oogatrons

==Failed==

bambam: computed 0 oogatrons dino: computed 0

oogatrons pebbles: computed 0

oogatrons wilma: computed 0 oogatrons

==Missing== mrslate

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

use 10::Socket;

use Text::Wrap: ft используется для создания аккуратного вывода

список машин, посылающих отчеты Srepolist = "/project/machinelist":

номер порта для соединения с клиентами Sserverport = '9967' ;

Sloadmachines: # загружаем список ма^ин

# настраиваем нашу сторону сокета

Sreserver = 10::Socket::INET->new(LocalPort => Sserverport,

Proto => "tcp",

Type => SOCK_STREAM,

Listen => 5,

Reuse => 1) or die "Невозможно настроить сокет на нашей стороне: $!\п";

и начинаем слушать порт в ожидании соединений while(

($connectsock,Sconnectaddr) = $reserver->accept()){

# имя подсоединившегося клиента

Sconnectname = gethostbyaddr((sockaddr_in($connectadar))[1],AF_INЈT):

chomp($report=$connectsock->getline);

($hostname,$result,$details)=split(' ',$report,3);

в если нужно сбросить информацию, выводим готовое к

# отправке сообщение и заново инициализируем все

хэши/счетчики

if (Shostname eq "DUMPNOW'H

&printmail($connectsock);

close($connectsock):

undef %success;

undef %fail:

Ssucceeded = Sfailed = 0:

&loadmachines;

next: }

warn "$connectname говорит, что был сгенерирован $nostnarce' \r-."

if($hostname ne Sconnectnaiie): delete $n;issiP.g{Shostna"rie}:

($result eq "success")!

$success{Shostnare}=$deraiIs:

$succeeded++: / else 1

$fail{$hostrame}=$dera;ls:

$fai!ed++. }

close($connectsock); } close($reserver):

# загружаем список машин из заданного файла sub loadmachines

undef %missing;

undef Smachines;

open(LIST,$repolist) or die "Невозможно открыть список Srepolist:$!\n";

while(<LIST>){

chomp;

$missing{$_}=1;

$machines++; } }

выводим готовое к отправке сообщение. Первая строка - тема,

# последующие строки - тело сообщения sub printmail<

(Ssocket) = $_[0];

if (Ssucceded == $machines){

Ssubject = "[report] Success: Smachines"; }

elsif ($failed == Smachines or scalar keys %missing >= Smachines) {

Ssubject = "[report] Fail: Smachines"; } else {

Ssubject = "[report] Partial: Ssucceeded ACK, Sfailed NACK".

((%missing) ? ", ".(scalar keys %missing)1." MIA" : ""); }

print Ssocket "$subject\n":

print Ssocket "Run report from $0 on ".scalar localtime(time)."\n";

if (keys %success){

print Ssocket "\n==Succeeded==\n";

foreach Shostname (sort keys %success){ print

Ssocket "Shostname: $success{$hostname}\n":

}

}

if (keys %fail){ print Ssocket "\n==Failed==\n":

foreach Shostname (sort keys %fail)< print $socket "Shostname: $fail{$hostname}\n"; } }

if (keys %nissing){ print Ssocket " \n==Missing==\n":

print $socket wrapC"1."" join(" ".sort keys Vrissi^g) ).'"

}

Кроме переноса части кода в отдельные подпрограммы, главное изменение заключается в том, что добавлен код для работы с сетью. Модуль 10: : Socket позволяет без труда открывать и использовать сокеты, которые можно сравнить с телефоном. Сначала нужно установить свою сторону сокета (10: :Socket->new()), как бы включая свой телефон, а затем ждать «звонка» от клиента (10: :Socket»accept()). Программа приостановлена (или «заблокирована») до тех пор, пока не установлено соединение. Когда соединение установлено, запоминается имя подсоединившегося клиента. Затем из сокета считывается строка ввода.

Мы предполагаем, что строка ввода выглядит точно так же, как и строки из отдельных файлов в предыдущем примере. Единственное различие - это загадочное имя узла DUMPNOW. Если это имя встречается, подсоединившемуся клиенту выводится тема и тело готового к отправке сообщения, при этом сбрасываются все счетчики и хэш-таб-лицы. За отправку сообщения, полученного от сервера, ответственен клиент. Теперь посмотрим на пример клиента и узнаем, что он может сделать с этим сообщением:

use 10::Socket;

номер порта для соединения с клиентом

Sserverport = "9967";

и имя сервера

$servername = "reportserver";

ft преобразуем имя в IP-адрес

Sserveraddr = inet_ntoa(scalar gethostbyname($servername));

Sreporttoaddr = "project\@example.com";

Sreportf romaddr = "project\(g>example.com";

Sreserver = 10::Socket::INET->new(PeerAddr => Jserveraddr.

PeerPort => $serverport Proto => "tcp". Type => SCCK^STREAM) or ale

"Невозможно создать сокет -а нашей стороне:: $!\п":

if ($ARGV;;0] re "-ni"){

print Sreserver $ARGV[0]: v else {

use Mail::Mailer;

print Sreserver "DUMPNOW\T;

chomp($subject = <$reserver.>) $body = join("",<$reserver>);

$type="send!rmil";

my Smaller = Mail::Mailer->new($type) or die

"Невозможно создать новый обьект niailer-$' \n";

$mailer->open({

From => $reportfromaddr To => $reporttoaddr, Subject => Ssubject » or die

"Невозможно заполнить объект mailer:$!\n";

print Smaller $body; $mailer->close; }

close($reserver);

Эта программа проще. Сначала открывается сокет с сервером. В большинстве случаев ему передается информация о состоянии (полученная в командной строке как $ARGV[0]) и соединение закрывается. При желании создать клиент-серверную систему регистрации, подобную этой, вероятно, нам пришлось бы перенести данный клиентский код в подпрограмму и вызывать ее из другой, гораздо более крупной.

Если передать сценарию ключ -т, он отправит серверу «DUMPNOW» и прочитает полученную от него строку темы сообщения и тело сообщения. Затем этот вывод передается модулю Mail: : Mailer и отправляется в виде почтового сообщения при помощи той же программы, которую мы видели раньше.

Для ограничения размера примера и для того, чтобы не уходить в сторону от дискуссии, здесь представлен лишь костяк кода для клиента и сервера. В нем нет ни проверки ошибок или ввода, ни управления доступом, ни авторизации (в сети любой, получивший доступ к серверу, может взять с него данные), ни постоянного хранилища данных (а что, если машина не работает?), ни даже мало-мальских мер предосторожности. Мало того, в каждый момент времени можно обрабатывать только один запрос. Если клиент остановится в середине транзакции, мы «влипли». Более изощренные примеры можно найти в книгах «Advanced Perl Programming» (Углубленное программирование на Perl) Шрирама Шринивасана (Sriram Srinivasan) и «Perl Cookbook» («Perl: Библиотека программиста») Тома Кристиансена (Tom Christiansen) и Натана Торкингтона (Nathan Torkington), обе выпущены издательством O'Reilly. Модуль Net:: Daemon Джошена Вьедмана (Jochen Wi-edmann) также поможет создавать более сложные программы-демоны.

Однако пора вернуться к рассмотрению других ошибок, допускаемых в программах для системного администрирования, отправляющих почтовые сообщения.



Общение напрямую по почтовым протоколам



Общение напрямую по почтовым протоколам

Последний выбор - написать программу, общающуюся с почтовым сервером на его родном языке. Большая часть этого языка документирована в RFC821. Вот как выглядит основной обмен данными по SMTP (Simple Mail Transfer Protocol, простой протокол передачи почты). Данные, которые мы посылаем, выделены жирным шрифтом:

%

telnet example.com 25 -соединяемся с SMTP-портом на

example.com

Trying 192.168.1.10 ... Connected to example.com. Escape character is '"]'.

220 mailhub.example.com ESMTP Sendmail 8.9.1a/8.9.1: Sun, 11 Apr 1999 15:32:16 -0400 (EOT)

HELD client.example.com -идентифицируеммашину,с которой мы.

пришли (можно использовать EHLO)

250 mailhub.example.com Hello dnb@client.example.com [192.168.1.11]. pienSt;' to meet you

MAIL FROM: «jnb@example.com> - определяем отправителя 250 <dnb@exarnple. com>... Sender ok

RCPT TO: <dnb@example.com> - определяем получателя

250 <dnb@example.com>... Recipient ok

DATA - начинаем отправлять данные, не забывая о некоторых ключевых заголовках

354 Enter mail, end with "." on a line by itself From:

David N. Blank-Edelman (David N. Blank-Edelman) To: dnbeexample.com Subject: SMTP - хороший протокол

Просто хочу напомнить себе о том, насколько я люблю SMTP. С миром, dNb

- завершаем сообщение

250 РАА266Р4 Message accepted for delivery QUIT - конец сессии

221 mailhuD.example.com closing connection Cor.rectici closed by foreign host.

Несложно записать в сценарий подобную беседу. Можно было бы использовать модуль Socket или что-нибудь вроде Net: : Telnet, как в главе 6 «Службы каталогов». Но существует несколько хороших модулей для отправки почты, которые упрощают эту задачу. Среди них модуль Женды Крыницки (Jenda Krynicky) Mail; :Se'-oer, Mai 1: :Sendmail Мили-вожа Ивковича (Milivoj Ivkovic) и Mail::Ma:Ier из пакета MailTools Грэхема Бара (Graham Barr). Все эти модули не зависят от операционной системы и будут работать практически везде, где доступен современный дистрибутив Perl. Мы рассмотрим Mail: :Mailer, поскольку он предлагает единый интерфейс к двум способам отправки почты, которые обсуждались до сих пор. Как и в случае с большинством модулей, написанных в объектно-ориентированном стиле, первый шаг заключается в создании экземпляра нового объекта:

use Mail::Mailer;

$f rom="me\@example. coin"; $to="you\@example.com";

$subject="Hi there"; $body="message body\n";

$type="srnto"; $server="mail.example,com";

my Smaller = Mail::Mailer->new($type, Server -> $server) or die

"Невозможно создать новый объект mailer:$'\n".

Переменная $type позволяет выбрать один из следующих типов поведения:

smtp

Посылает почту, обращаясь к модулю Net: :SMTP (часть пакета lib-net), доступному и для большинства не-Unix версий Perl. Если используется MailTools версии 1.13 или выше, можно задать имя SMTP-сервера, применяя приведенную выше символику =>. В противном случае, придется устанавливать имя сервера во время процедуры установки libnet.

mail

Отправка почты при помощи почтового агента mail (или любого другого, который задан вторым необязательным аргументом). Это напоминает недавнее использование AppleScript и MAPI.

sendmail

Отправка почты с помощью программы sendmail, как и в первом случае из данного раздела.

Кроме того, можно установить переменную окружения PtRL_MAILERS, чтобы изменить путь, установленный по умолчанию для поиска программ (например, sendmail) в системе.

Вызов метода ореп() для нашего объекта Mail:: Mailer заставляет последний выполнять роль дескриптора для исходящего сообщения. В этом вызове передаются заголовки сообщения ссылке на анонимный хэш:

$mailer->open({From => $from, То => $to.

Subject => Ssubject}) or die "Невозможно заполнить объект mailer

Тело сообщения выводится в этот псевдодескриптор, который потом закрывается для отправки сообщения:

print Smailer $body: $mailer->close;

Этого вполне достаточно, чтобы отправка почты из Perl не зависела от системы.

В зависимости от того, какой тип поведения $type был выбран при работе с модулем, могут оказаться скрытыми (а могут и не оказаться) более сложные вопросы, относящиеся к МТА, о которых уже говорилось. В предыдущем примере использовалось поведение smtp, а это означает, что программа должна быть достаточно умна, чтобы обрабатывать такие сбои как недоступность сервера. Приведенный пример не настолько «сообразителен». Обязательно позаботьтесь о таких моментах, когда будете писать программы.



Отправка почты Начнем с рассмотрения


Если вы работаете на Win32, то вам повезло, т. к. я знаю по крайней мере о трех версиях sendmail, перенесенных под Win32:

трех версиях sendmail, перенесенных под Win32: Перенесенная версия sendmail от Cygwin (http://dome/weeg.ui-owa.edu/pub/domestic/sos/ports) Коммерческая версия sendmail от Mercury Systems (http://www.de-mobuilder.com/sendmail.htm) Коммерческая версия Sendmail for NT от Sendmail, Inc. (http: www.sendmail.com)

Тем, кому нужно что-то менее тяжеловесное и кто хочет внести некоторые изменения в программу на Perl, чтобы поддержать различные аргументы командной строки, возможно, помогут достичь цели другие программы для Win32:

blat (http://www.interlog.com/~tcharron/blat.html) netmall95 (http://www.geocitles.com/SlliconValley/Lakes/2382/net mail.html) wmailto (http://www.impaqcomp.com/jgaa/wmailto.html)

Преимущества такого подхода состоят в том, что можно выбросить из сценария все сложности отправки почты. Хороший агент передачи почты (МТА) пытается повторно соединиться с почтовым сервером, если тот в данный момент недоступен, выбирает нужный целевой сервер (ищет записи Mail eXchanger в DNS и осуществляет переходы между ними), при необходимости переписывает заголовки, справляется с внезапными коллизиями и т. д. Если можно избежать необходимости заботиться обо всем этом в Perl, то это просто замечательно.



Отслеживание спама Теперь когда


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

Можно использовать такой черный список, чтобы разобраться, проходило ли сообщение через узлы, известные своим спамерством. Ясно, что в черном списке нет сервера, передавшего нам почту (иначе с ним просто не было бы установлено соединение), но любой из остальных почтовых серверов, указанный в заголовках Received:, вполне может там быть.

Не существует способа написать одну программу, проверяющую все возможные черные списки агентов передачи почты, поскольку разные агенты хранят эту информацию в различных форматах. Большая часть узлов в Интернете в настоящее время применяет в качестве агента передачи почты sendmail, так что в нашем примере будет применяться его формат черного списка. В новых версиях sendmail черный спи- сок хранится в базе данных при помощи библиотек Berkeley DB 2.X, доступных на http://www.sleepycat.com.

Поль Маркес (Paul Marquess) написал модуль BerkeleyDB, специально предназначенный для работы с библиотеками Berkeley 2.x/3.x. Это может сбить с толку, поскольку в документации по DB_File, еще одному известному модулю Маркеса, входящему в состав дистрибутива Perl, также рекомендуется применять библиотеки 2.х/З.х. DB_File использует библиотеки Berkeley DB 2.х/З.х в «режиме совместимости» (в частности, библиотека собирается с ключом --enable-compat185, так что доступен API версии 1.x API). Модуль BerkeleyDB позволяет программисту на Perl применять расширенные возможности из API версии 2.х/З.х.

Агент передачи sendmail использует формат BerkeleyDB 2.х/З.х, так что нужно включить модуль BerkeleyDB. Вот пример, который выводит содержимое локального черного списка:

Sblacklist = "/etc/mail/blacklist.db"; use BerkeleyDB;

Ясвяжем хэш %blist с черным списком, используя Berkeley DB

# для получения значений

tie %blist, 'BerkeleyDB::Hash', -Filename => Sblacklist or die

"Невозможно открыть файл $filenane: $! SBerkeleyDB::Error\n" ;

# обходим в цикле каждый ключ и значение из этого файла, и

выводя только записи REJECT while(($key,$value) = each %blist){

в списке также могут быть записи "OK", "RELAY" и др. next

if ($value ne "REJECT");

print "$key\n": }

Принимая за основу этот код, можно написать подпрограмму, проверяющую, находится ли данный узел или домен (содержащий этот узел) в черном списке. Если нужно узнать об узле mallserver.spam-mer.com, следует обойти в цикле все записи из черного списка (в котором могут находиться mailserver.spammer.com, spammer.com или даже просто spammer), чтобы проверить, содержатся ли в имени узла какие-либо записи из него.

Существует много способов написать на Perl программу, сравнивающую список значений с какими-либо данными. Но для того чтобы программа была эффективной и интересной, мы будем использовать две продвинутые технологии. Они созданы для уменьшения числа компиляций регулярных выражений, которые применяются в ходе выполнения программы. Каждый раз, когда программа использует «новое» регулярное выражение, Perl должен компилировать его заново. Например, в этом отрывке кода Perl вынужден обрабатывать новое значение на каждой итерации:

вообразите себе внешний цикл, в котором этот код вызывается

множество раз

foreach Smatch (qw(alewife davis porter harvard central кепааН park))

{

Sstation =" /Smatch/ and print "found our station stop'": }

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

Вот пример первой технологии, созданной для решения указанной проблемы:

use BerkeleyDB;

Sblacklist = "/etc/mail/blacklist.db";

&loadblist;

и принимаем имя узла в качестве аргумента командной строки и

сообщаем, если оно есть в черном списке

if (defined &checkblist($ARGV[0])){

print "*** $found найден в черном списке \п"; }

И

загружаем черный список в массив анонимных подпрограмм sub loadblist{

tie %blist, 'BerkeleyDB::Hash', -Filename => Sblacklist or die

"Невозможно открыть Sfilename:

$! $BerkeleyDB::ErrorXn" ;

while(my($key,$value) = each %blist){

# в черном списке могут быть "OK", "RELAY" и пр. next if ($value ne "REJECT");

push(@blisttests, eval 'sub {$_[0] =~ \Q$key/o and $key}'); } }

sub checkblist{

my($line) = shift:

foreach Ssubref (@blisttests){

return Sfound if (Sfound = &$subref($line)); }

return undef: } В этом примере используются анонимные подпрограммы - технология, продемонстрированная в книге Джозефа Хола (Joseph Hall) «Effective Perl Programming» (Эффективное программирование на Perl) (Addison Wesley). Для каждой записи из черного списка создается анонимная подпрограмма. Каждая подпрограмма сверяет переданные ей данные с одним из элементов черного списка. Если они совпадают, такая запись возвращается. Ссылки на эти подпрограммы хранятся в списке. Вот строка, в которой создается подпрограмма и ссылка на нее добавляется к списку:

push(@blisttests, eval 'sub <$_[0] =" /\0$key/o and $key}');

Так что, если в черном списке есть запись spammer, ссылка на код, добавленная в массив, будет указывать на подпрограмму, по сути эквивалентную следующей:

sub {

$_[0] =" /\Qspammer/o and "spammer"; }

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

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

return $found if (Sfound = &$subref($line));

Компиляция регулярного выражения, которая нас так беспокоит, происходит всего один раз - при создании ссылки на подпрограмму. Можно вызывать каждую подпрограмму столько раз, сколько надо, не теряя время на компиляцию регулярного выражения.

Существует и другой, чуть менее продвинутый подход, который можно применять, если у вас Perl версии 5.005 или выше. В Perl 5.005 была введена новая синтаксическая конструкция, названная «прекомпи-лируемым регулярным выражением», которая делает подобную задачу несколько проще. Переписать код, используя эту новую конструкцию, можно было бы примерно так:

sub loadblist{

tie %blist, 'BerkeleyDB::Hash', -Filename => $blacklist or die

"Невозможно открыть файл $Шегше:

$BerkeleyDB: :Error\n" ;

while(my($key,$vaiue) = eac^- %blist){

# в черном списке могут бьть запис/ "OK". "RELAY" и пр. next

(Svalue ne "PEJECT") push('SDlisttests, [qr/\Q$i<ey/. $кеу]): }

sub checkblisu

my($iine) = shift;

foreach my Stest (§blisttests){

my($re,$kr;v) = a{$test}

return $key i* ($line =" /$re/): }

return undef; }

На этот раз ссылка была перенесена на анонимный массив в@blist test. Первый элемент этого массива - скомпилированное регулярное выражение, созданное с применением нового синтаксиса qr/Y. Это позволяет сохранить регулярное выражение после его компиляции. Такая форма обработки значительно увеличивает скорость выполнения программы при дальнейшем поиске соответствия. Второй элемент анонимного массива - сама запись из черного списка, которая будет возвращена при найденном соответствии скомпилированному регулярному выражению.



Поиск в черном списке для всего Интернета



Поиск в черном списке для всего Интернета

В последнем примере программы на вопрос «Спамер ли это?» мы отвечали, руководствуясь собственным мнением об узле или домене, не принимая во внимание опыт остальных пользователей Интернета. Существуют несколько спорные службы, предлагающие простой доступ к глобальным черным спискам спамеров или известных узлов, открыто допускающих ретрансляцию почты. Две хорошо известные службы такого типа - Realtime Blackhole List (RBL) от Mail Abuse Prevention System и Open Relay Behaviour-modification System (ORBS). Для получения доступа к этим спискам:

Измените на обратный порядок следования элементов проверяемого IP-адреса. Например, 192.168.1. 34 станет 34.1.168.192. Добавьте специальное имя домена к полученному числу. Для проверки адреса в RBL необходимо использовать 34.1.168.192. rbl. Выполните запрос к DNS-серверу для данного адреса. Если вы получите положительный ответ (т. е. запись о ресурсах А), это означает, что данный IP-адрес находится в черном списке.

Несколько менее спорным является список Dial-up User List, также поддерживаемый силами специалистов из Mail Abuse Prevention System. Это список диапазонов IP-адресов, динамически присваиваемых модемным пулам. Теоретически, SMTP-соединения не должны исходить от какого-либо из этих узлов. Почта с таких узлов должна отправляться через почтовый сервер провайдера (которого нет в этом списке).

Вот один из способов проверить, находится ли IP-адрес в каком-либо из этих списков:

sub checkaddr{

my($ip,Sdomain) = @_;

return undef unless (defined Sip);

my $lookupip = join('.',reverse split(/\./,$ip));

if (gethostbyname($lookupip.$domain)){

return Sip; } else {

return undef; } }

Очень скоро эта подпрограмма будет добавлена в предпоследний пример этого раздела. А пока, располагая существенно большим объемом информации о каждом из заголовков Received:, попробуем вычислить человека или людей, ответственных за администрирование каждой машины из списка. Модуль Net: ;Whois, уже рассмотренный в главе 6, вероятно, первым будет использоваться для решения этой проблемы.

К сожалению, этот модуль специализируется только на получении информации о связи имен и доменов (name-to-domain information). Кроме того, он «предполагает», что информация будет представлена в виде, используемом InterNIC. Нам могут понадобиться сведения о связи IP-адресов и доменов (IP address-to-domain information) от WHOIS-серверов на http://whois.arin.net (American Registry for Internet Numbers), http://whois.ripe.net (European IP Address Allocations) и http:// whois.apnic.net (Asia Pacific Address Allocations). Отсутствие соответствующего модуля - первое препятствие, которое необходимо преодолеть.

Но даже если бы мы знали, как соединиться со всеми этими реестрами и обработать их различные форматы вывода, было бы неясно, к какому из них нужно обращаться для поиска информации о данном IP-адресе. Нам необходимо определить, к какому серверу нужно обратиться, и это второе препятствие. К счастью, если обратиться к ARIN с запросом по адресу, не принадлежащему его базе данных, он направит нас на нужный реестр. Так что, если мы спросим ARIN об адресе из Японии, он отправит нас на APNIC.

Для преодоления первого препятствия можно использовать модуль общего назначения, подобный Net: :Telnet из главы 6. Другой путь - уже рассмотренный модуль 10: :Socket. Что выбрать- дело личных предпочтений, ну и, конечно, необходима возможность доступа к нему с вашей платформы.

Служба WHOIS работает на порту 43 TCP, хотя ее имя будет использоваться только в целях предосторожности. С WHOIS-сервером очень просто общаться. Необходимо соединиться, выполнить запрос (в нашем случае это IP-адрес) и получить ответ. Программа, запрашивающая произвольный WHOIS-сервер, очень проста:

sub getwhois{

my($ip) = shift; my(Sinfo);

$cn = new Net::Telnet(Host => Swhoishost, Port => 'wliois',

Errmode => "return", Timeout => 30)

or die "Невозможно установить -соединении с

Swhoishost connection:$!\n";

unless ($cn->print($ip."\n")){

$cn->close;

die "Невозможно послать

$ip на Swhoishost: ".$cn->errmsg."Vv }

while ($ret = $cn->get){

Sinfo ,=$ret; };

$cn->close;

return $info; 1

Для преодоления второго препятствия, состоящего в выборе нужного реестра, есть, по крайней мере, две возможности. Можно послать запрос к http://whois.arin.net и проанализировать ответ. Например, вот запись диалога с ARIN по поводу IP-адреса японской машины. Жирный шрифт используется для выделения текста, введенного человеком:

X telnet whois.arin.net 43

Trying 192.149.252.22 ..

Connected to whois. arm. not Escape character is '"] 210.161.92.226

Asia Pacific Network Information Center (NETBLK-APNIC-CIDR-BLK)

Level 1 - 33 Park Roan1 Milton, 4064 AU

Netname: APNIC-CIDR-BLK2

Netblock: 210.0.0.0 - 211.255.255.0

Coordinator;

Administrator, System (SA90-ARIN)

sysadn®APNIC,NET +61-7-3367-0490

Domain System inverse mapping provided by:

SVC01.APNIC.NET 202.12.28.131

NS.TELSTRA.NET 203.50.0.137

NS.KRNIC.NET 202.30.64.21

NS.RIPE.NET 193.0.0.193

-** please refer to whois.apnic.net for more information *«*

*«* before contacting APNIC

*-« use whois -h whois.apnic.net <object> *»*

Record last updated on 04-Mar-99.

Database last updated on 19-Apr-99 16:14:16 EOT.

Подобные результаты означают, что запрос нужно послать к http:// whois.apnic.net.

Другой способ - послать запрос к «умному» WHOIS-серверу, который сделает всю работу сам. Из них мне больше всего нравится сервер -http://whois.geektools.com. «Умник» проанализирует ваш запрос, отправит его на нужный WHOIS-сервер и вернет результаты. Тому, кто пользуется этой службой, не нужно беспокоиться о том, на каком именно сервере хранится информация.

Чтобы код программы сильно не разрастался, а мы не отвлекались от обсуждаемой темы, будем пользоваться вторым (более простым) способом.

Поместим все эти маленькие запросы в одну большую программу и запустим ее. Итак, если нужно выполнить все приведенные выше подпрограммы с нашим примером сообщения:

use Mail:'Header: use Socket; use BerkeleyDB;

use Net::Telnet;

$header = new Mail-Header \*STDIN;

Sheader ->unfold('Received'); @received = $header->get('Received'):

Srbldomain = ".rbl.maps.vix.com" Sorbsdomain = ".relays.orbs.org";

Sduldomain = ".dul.maps.vix.com": Sblacklist = "/etc/mail/blacklist.db":

Swhoishost = "whois.geektools.com";

&loadblist;

for (reverse @received){ chomp;

parseline($_);

if (!defined $ehelo and !defined Svalidname and !defined $validip){ print "$Дп";

>

else {

Sflags = (&checkaddr($validip:$rbldomain) ? "R" : ""):

# в RBI/' $flags .= (&checkaddr($validip,$orbsdomairi) ? "0" : "");

в ORBS9 Sflags ,= (&checkaddr($validip. Sduldomain) ? "D......);

$flags .= (&checkblist($_) 9 "B" : ""): # в нашем спи

$flags .= (&checkrev($validip,$validname) ? "L" "");

(I rev-.;o.;k: push(@iplist,Svalidip);

write; } }

for (@iplist){

print "\nWHOIS info for $_:\n"; print &getwhois($_); >

format STDOUT =

@««««««««<«« @<«««««<'<««««««<«««««

Sehelo.Svalidname.Svalidip.Sflags

то будут получены такие результаты (слегка сокращенные):

login_0246.whynot.net mx.whynot.net 206.212.231 88 ;

extreme host-209-214-9-150 :r.a 209.214.9.150 Or

isiteinc.cori www.isiteinc.com 206,136 243 2

WHOIS info for 206,212.231.88:

WHOIS info for 209.214.9.150;

BellSouth.net Inc. (NETBLK-BELLSNET-BLK4)

1100 Ashwood Parkway

Atlanta. GA 30338

Netname: BELLSNET-BLK4

Netblock: 209.214.0.0 - 209.215.255.255

Maintainer: BELL

Coordinator-...

WHOIS info for 206.136.243.2:

Brainsell Incorporated (NET-ISITEINC)

4105-R Laguna St.

Coral Gaoles, FL 33146

US

Netname: ISITEINC Netnumber: 206.136.243.0

Coordinator:...

Гораздо лучше! Теперь нам известно:

Спамер дал неверные ответы HELO/EHLO. Первый узел, по всей вероятности, фальшивый (не удалась попытка разыменования, и по нему нет информации WHOIS). Сообщение, скорее всего, попало в сеть через соединение по телефонным линиям. Два из этих адресов уже находятся в нашем черном списке. ORBS они тоже не нравятся. Кроме того, нам известна контактная информация для связи с провайдером.

Perl помог разобраться с непрошеной коммерческой почтой.

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



Получение почты



Получение почты

Обсуждая в этом разделе получение почты, мы не будем говорить о ее сборе (fetching). Передача почты с одной машины на другую не представляет особого интереса. Модули Mail: :POP3Client Сина Дауда (Sean Dowd) и Mail: :Cclient Малколма Битти (Malcolm Beattie) легко могут передать почту по протоколам POP (Post Office Protocol) или ШАР (Internet Message Access Protocol). Гораздо интереснее посмотреть, что с этой почтой делать после ее получения, и именно на этом мы и остановимся.

Начать следует с основ, поэтому рассмотрим инструменты, позволяющие разбить как отдельные сообщения, так и почтовые ящики. Чтобы рассмотреть первое, вновь обратимся к пакету MailTools Грэма Бара (Graham Barr), на этот раз прибегнем к модулям Mail : :Interne: и Mail : :Header.

Разбиение отдельных сообщений

Модули Mail: :Internet и Mail: :Header предлагают удобный способ разбить заголовки почтового сообщения, соответствующего RFC822. RFC822 определяет формат почтового сообщения, включая имена допустимых заголовков и их форматов.

Модулю Mail: : Internet необходимо передать либо файловый дескриптор файла с сообщением, либо ссылку на массив, содержащий его строки:

use Hail::Internet: $messagefile = "mail";

open(MESSAGE,"Smessagefile") or die "Невозможно открыть $messagefile:$!\n";

Smessage = new Mail::Internet VMESSAGE;

close(MESSAGE);

Если мы хотим анализировать сообщение, поступающее в стандартный поток ввода (т. е. переданное через конвейер на стандартный ввод), можно поступить так:

use Mail::Internet;

Smessage = new Mail: :Internet VSTDIN;

Mail: : Internet возвращает экземпляр объекта сообщения. Чаще всего с этим экземпляром объекта будет применяться один из двух методов: body() и head(). Метод body() возвращает ссылку на анонимный массив, содержащий строки тела сообщения. head() более интересен и предлагает плавное продолжение модуля Mail: : Header.

При загрузке Mail::Internet неявно загружается Mail::Header. Если вызвать метод head() модуля Mail: :Internet, он вернет экземпляр объекта заголовка Mail: :Header. Это будет тот же экземпляр объекта, который можно получить, если использовать не Mail: :Internet, а напрямую Mail: :Header:

use Mail::Header; Smessagefile = "mail";

open(MESSAGE,"Smessagefile") or die "Невозможно открыть $messagefile:$!\n"

$header = new Mail:: Header VMESSAGE;

close(MESSAGE):

Объект $header содержит заголовки сообщения и предлагает несколько удобных способов для получения данных. Например, чтобы вывести отсортированный список встречающихся имен заголовков (которые в модуле называются «тегами»), можно добавить такую строчку в конец предыдущего примера:

print join("\n",sort $header->tags);

В зависимости от сообщения можно увидеть что-то подобное нижеследующему:

Сс

Date

From

Message-Id

Organization

Received

Reply-To

Sender

Subject

To

Необходимо получить все заголовки Received: из сообщения. Вот как это можно сделать:

received = $header->get("Received");

Часто методы Mail: :Header используются вместе с объектом Mail: : Internet. Если применять Mail: : Internet для возвращения объекта, содержащего и тело и заголовки сообщения, можно сцепить вместе некоторые методы из этих модулей:

©received = $message->head->get("Received"):

Обратите внимание, что get()вызывается в списочном контексте. В скалярном контексте метод вернул бы только первое вхождение этого тега, в случае, если бы мы не задали вторым аргументом порядковый номер тега. Например, get("Received", 2) вернет вторую строку Receivec: из сообщения. Модуль Mail: : Header предоставляет и другие методы для удаления и добавления тегов в заголовки; подробную информацию можно найти в документации.



Пропуск темы сообщения Строка



Недостаточная информация в теле сообщения

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

Кто?

Какой сценарий сообщает об ошибке? Добавьте содержимое переменной $0 (если не устанавливали ее явно), чтобы показать полный путь к текущему сценарию. Сообщите о версии сценария, если таковая у него имеется.

Где?

Сообщите что-то о том месте в сценарии, где возникает проблема. Функция caller () из Perl возвращает всю нужную для этого информацию:

замечание: то, что возвращает са!1ег(). может зависеть от

версии Perl, так что обязательно загляните в документацию по perlfunc

(Spackage, $filename, $line, $subroutine, Shasargs,

$wantarray, Sevaltext, $is_require) = caller($frames);

Где $f rames - это количество нужных фреймов на стеке (если вызывались подпрограммы из подпрограмм). Чаще всего вы будете устанавливать $f rames в 1. Вот пример списка, возвращаемого функцией caller() в середине кода для сервера из последнего полного примера:

('main','repserver1,32,'main:iprintmail1,1,undef)

Подобная запись указывает, что сценарий, запущенный из файла repserver в строке 32, находился в пакете main. В этот момент выполнялся код из подпрограммы main: .printmail (у нее есть аргументы, кроме того, она не вызывается в списочном контексте).

Если вы не хотите вручную применять caller(), можете воспользоваться отчетом о проблемах, предоставляемым модулем Carp.

Когда?

Опишите состояние программы в момент возникновения ошибки. К примеру, какой была последняя строка прочтенных данных?

Почему?

Если сумете, ответьте на незаданный читателем вопрос: «Зачем беспокоить меня этим почтовым сообщением?» Ответ может быть очень простым, например: «данные об учетных записях не были полностью обработаны», «DNS-сервер сейчас недоступен» или «в серверной пожар». Это даст читающему представление о предмете разговора (и, возможно, побудит к изучению).

Что?

Ну и наконец, надо сказать о том, что же пошло не так раньше всего. Вот небольшой пример на Perl, который охватывает все эти пункты:

use ext::Wrap;

sub problemreport {

9 $shortcontext - описание проблемы в одной строке

# Susercontext - подробное описание проблемы Получение почты 315

Snextstep - лучшее предположение о том, что делать, чтобы исправить

проблему

my($shortcontext,Susercontext,Snextstep) = @_: my

($filename, $line, Ssubroutine) = (caller(1) )[1,2,3]:

push(@return,"Проблема с Sfilename $shortcortext\n '):

push(@return,"** Сообщение о проблеме с Sfilename ***\n\n"):

push(@return,fill("","","- Проблема: Susercontext") "\r\n")

push(@return,"- Место: строка Sline файла Sfilename в

$subroutine\n\n"); push(@return,"- Произошла: ".scalar localtime(time)."\n\n");

push(@return,"- Дальнейшие действия: $nextstep\n"):

\@return; }

sub fireperson {

Sreport = &problemreport("компьютер горит ", «EOR, «EON);

При составлении отчета загорелась задняя часть компьютера.

Случилось это сразу же после обработки пенсионного плана для ORA. EOR

Пожалуйста, потушите пожар, а потом продолжайте работу. EON

print @{$report}; } &fireperson;

Обращение к &problemreport выведет, начиная с темы сообщения, отчет о проблеме, согласующийся с Mail: : Mailer, как и в предыдущих примерах.

Теперь, разобравшись с отправкой почты, перейдем к другой стороне медали.



Распространенные ошибки при отправке


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



Разбиение почтового ящика



Разбиение почтового ящика

Перейти на следующий уровень, где мы разобьем на части почтовые ящики, довольно просто. Если почта хранится в формате «классического mbox» или qmail (еще один агент передачи почты, подобный sendma.il), можно использовать модуль Mail : : Folder Кевина Джонсона (Kevin Johnson). Многие из распространенных почтовых агентов (не в Unix), таких как Eudora, тоже хранят почту в классическом формате Unix mbox, так что этот модуль может быть полезным на многих платформах. Что-то подобное мы уже видели:

use Mail': Folder: :Mbox: для классическое фермата Unix mbox

Sfolder = new Mail::Folder('mbox',"filename1 )

Конструктор new() принимает тип формата почтового ящика и имя файла для анализа. Он возвращает экземпляр объекта folder, через который можно запрашивать, добавлять, удалять и изменять сообщения. Чтобы получить шестое сообщение, нужно применить следующее:

Smessage = $folder->get_message(6);

Теперь Smessage содержит экземпляр объекта Mail: : Internet. С этим экземпляром объекта можно применять все только что обсужденные методы. Если вам нужен только заголовок сообщения, можно использовать:

$header = $folder->get_header(6);

Здесь нет никаких сюрпризов; возвращается ссылка на экземпляр объекта Mail: .'Header. Чтобы узнать о других доступных методах, загляните в документацию по Mail:: Folder.



Рекомендуемая дополнительная литература



Рекомендуемая дополнительная литература

«Advanced Perl Programming»,

Sriram Srinivasan (O'Reilly, 1997)- в книге есть хороший раздел о программировании серверов.

«Effective Perl Programming»,

Joseph Hall, Randal Schwartz (Addison Wesley, 1998) - полезная книга, в которой можно найти множество идиом Perl.

http://www.cauce.org/ -

сайт от Coalition Against Unsolicited Email (коалиция против непрошеной почты). Существует много сайтов, посвященных борьбе со спамом; этот сайт неплох для начала. Здесь можно найти ссылки на множество других ресурсов, включая те, на которых описан подробный аналиа почтовых заголовков.

http://www.eudora.com/developers/scripting.html -

содержит информацию по Eudora и ссылки на другие источники по AppleScript.

http://www.microsoft.com

и http://msdn.microsoft.com - содержат информацию по «MAPI», «active messaging» и «CDO». Названия этой технологии уже менялись дважды, так что я не решаюсь привести точную ссылку. На сайтах Microsoft содержится масса полезной информации (особенно в разделе про библиотеки MSDN) по этим темам, но она постоянно перемещается с места на место.

«Perl Cookbook»,

Tom Christiansen, Nathan Torkington (O'Reilly, 1998). В этой книге также рассматриваются вопросы программирования серверов.

«RFC821 .-Simple Mail Transfer Protocol»,

J. Postel, 1982.

«RFC822:Standard for the format of ARPA Internet text messages»,

D. Crocker, 1982.

«RFC954.-NICNAME/WHOIS»,

K. Harrenstien, M. Stahl, E. Feinler, 1985.

Увеличение почты в службу поддержки



Увеличение почты в службу поддержки

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

Все можно хранить и отслеживать, в отличие от разговоров в коридорах. Электронная почта асинхронна; системный администратор может читать почту и отвечать на нее в более спокойные вечерние часы. По желанию, это может быть индивидуальная, групповая или широковещательная рассылка. Если 14 человек пишут об одном и том же (скажем, об одних и тех же сообщениях спамеров), есть возможность ответить им всем одновременно, когда проблема будет решена. Почту легко переслать тому, кто разбирается в обсуждаемом деле или ответственен за конкретные службы. Все это веские причины использовать электронную почту для связи по вопросам поддержки. Однако есть у электронной почты и недостатки: Если проблема касается самой почтовой системы или у пользователя что-то не ладится с электронной почтой, то для связи нужно применять что-то другое. Пользователи могут и будут писать в сообщениях все что угодно. Нет никакой гарантии, что в письме будет информация, которая пригодится для решения проблемы или помощи пользователю. Возможно, вы даже не поймете, зачем нужно было такое сообщение. Это приводит к головоломке, о которой стоит поговорить в данном разделе.

Мое любимое письмо в службу поддержки приведу точно в таком виде, в каком оно было получено, за исключением имени автора:

Date: Sat, 28 Sep 1996 12:27:35 -0400 (EOT) From:

Special User <user@example.com> To:

systems@example.com Subject: [Req. 89531] printer help

something is wrong and I

have know idea what (что-то случилось, и я не имею понятия, что именно)

Если бы пользователь не упомянул слово «принтер» в теме сообщения, не было бы никаких указаний на то, с чего начать, и нам, вероятно, пришлось бы думать, что и впрямь случилось нечто ужасное. Конечно, это самый крайний случай. Чаще вы будете получать примерно такую почту:

From: Another user <user2®examplecom>

Subject: [Req 814563] broken macine

To. systems§exarriple com

Date: Wed, 11 Mar 1998 10:59:42 -0500 (EST)

С малиной krakatoa. example, com

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

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

Для пользователя вопрос «С какой машиной проблемы?» кажется странным. Они говорят об одном компьютере, о том, на котором работают сейчас. Неужели это не очевидно? Системному администратору столь же странной кажется просьба «помогите с принтером»; в конце концов, он следит за многими принтерами.

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

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

Что ж, если применить Perl, может быть и нет. Perl наверняка поможет увеличить количество нормальной почты и поучаствовать в процессе поддержки. Один из первых шагов системного администратора выяснить местоположение: «Где случилась проблема? С каким принтером? С каким компьютером?». И так далее.

Вот костяк программы, которую я назвал suss, представляющая собой «скелет» для сбора информации. Программа изучает сообщение и пытается выяснить, с какой машиной оно связано. В результате часто можно определить имя узла для писем из категории «С моим компьютером проблемы», не вступая ради этого в дальнейшую беседу с рассеянным пользователем. Имя узла - хорошая отправная точка в процессе поиска возникших проблем.

Программа suss применяет очень простой алгоритм для разгадывания имени узла (обычно, поиск в хэше для каждого слова из сообщения). Сначала изучается тема сообщения, затем его тело и, наконец, выполняется поиск по заголовкам Received:. Вот упрощенная версия, считывающая файл /etc/hosts, чтобы определить имена узлов:

use Mail::Internet; $localdomain = ".example.com";

ft считываем файл /etc/hosts

open(HOSTS,"/etc/hosts") or die "He могу открыть файл узлов\п";

while(defined($_ = <HOSTS>)){

next if /"ft/; ft пропускаем комментарии

next if /"$/; ft пропускаем пустые строки

next if /monitor/i; ft пример вводящего в заблуждение узла

ft выделяем первое имя узла и переводим его в нижний регистр

Smachine = lc((split)[1]);

$machine =~ s/\Q$localdomain\E$//oi;

t удаляем имя домена

$macriines{$machine}++ unless $macnines{$machine}; }

ft анализируем сообщение

$message = new Mail: :Internet VSTDIN;

$message->head->unfold();

ft проверяем тему сообщения

my Ssubject = $message->head->get('Subject');

Ssubject =' s/[.,;?]//g;

for (split(/\s+/,Ssubject)) {

if (exists $machines{lc $_}) {

print "subject: $_\n";

$found++; } } exit if $found;

ft проверяем тело сообщения

chomp(my @body = @{$message->body()}):

my Sbody = join(" ",@body);

$body =" s/t"\w\s]/ /g: и удаляем знак

@body{split( ', lc $bcdy)i = ().

for (keys %body) {

if (exists $machines!ic $_}) {

print "body: $_\n";

$*ound+-i-; } }

exit if $found,

# последняя надежда: проверяем последнюю строку

Received: Sreceived -(reverse $message->head->get('Received'))[0];

Sreceived =" s/\0$localdomain\E//g; for (split(/\s+/,Sreceived)) {

if (exists $machines{lc $_}) { print "received: $_\n";

}

Несколько комментариев к программе:

Простота проверки становится проблемой, когда мы сталкиваемся с вполне приемлемыми именами узлов, подобных monitor. Если имена узлов, являющиеся обычными словами, могут появиться в сообщениях, вам придется либо специально их обработать, как было сделано с next if /monitor/i, либо придумать более сложную схему анализа, что предпочтительнее. Мы используем срез хэша (§body{...}), чтобы ускорить поиск по телу сообщения. За один шаг из сообщения выделяются все уникальные слова. Чтобы разобраться с этой конструкцией, можно прочитать ее изнутри. Во-первых, split() возвращает из сообщения список всех «слов» (в нижнем регистре). Эти слова используются как ключи для хэша %body. Поскольку имена ключей в хэше повторяться не могут, он будет содержать только уникальные слова из тела сообщения. Именно подобные возможности делают программирование на Perl приятным.

Теперь применим эту программу. Вот два настоящих сообщения в службу поддержки:

Received: from strontium.example.com

(strontium.example.com [192.168.1.114])

by mailhub.example.com (8.8.4/8.7.3) with ESMTP id RAA27043

for <systems>; Thu, 27 Mar 1997 17:07:44 -0500 (EST)

From: User Person <user@example.com>

Received: (user@localhost)

by strontium.example.com (8.8.4/8.6.4) id RAA10500 for systems;

Thu, 27 Mar 1997 17:07:41 -0500 (EST)

Message-Id: <199703272207.RAA10500@strontium.example.com

Subject: [Req #11509] Monitor

To: systems@example.com

Date: Thu, 27 Mar 1997 17:07:40 -0500 (EST)

Hi,

My monitor is flickering a little bit and it is tiresome

when working with it to much.

Is it possible to fix it or changing the monitor? Thanks. User,

Received: from example.com (user2@example.com [192.168.1.7])

by mailhost.example.com (8.8.4/8.7.3) with SMTP id SAA00732

for <systems@example.com>: Thu. 27 Mar 1997 18:34:54 -0500 (EST)

Date: Thu, 27 Mar 1997 18:34:54 -0500 (EST)

From: Another User <user2@example.com>

To: systems@example.com

Subject: [Req 811510] problems with two computers

Message-Id: <Pine.SUN.3.95.970327183117,23440A-100000ffiexamole.com>

In Jenolen (in room 292), there is a piece of a disk stuck in it.

In intrepid, there is a disk with no cover

(or whatever you call that silver thing) stuck in it.

We tried to turn off intrepid, but it wouldn't work.

Wo (the proctor on duty and I) tried to get the disk piece out, but it didn't. work.

The proctor in charge decided to put signs on them saying 'out of order'

AnotherUser После запуска программы для этих двух сообщений мы получили:

received: strontium и:

body: jenolen body: intrepid

Оба узла были найдены верно и для этого понадобился лишь небольшой отрывок простого кода. Шагнем дальше и предположим, что поступило такое письмо:

Received: from [192.168.1.118]

(buggypeak.example.com [192.168.1.118])

by mailhost.example.com (8.8.6/8.8.6)

with SMTP id JAA16638 for <systems>;

Tue, 4 Aug 1998 09:07:15 -0400 (EOT)

Message-Id: <v02130502b1ecb78576a9@[192.168.1.118]>

Date: Tue, 4 Aug 1998 09:07:16 -0400

To: systems@example.com

From: user@example.com (Nice User)

Subject: [Req «15746] printer

Could someone please persuade my printer to behave and print like

printer should9 Thanks much :)

-Nice User.

Пользователь, должно быть, не знает, что вы «пасете стадо» из 30 принтеров. Но можно применить Perl и чуть-чуть наблюдательности, чтобы сделать умные догадки. Пользователи стараются печатать на принтерах, расположенных ближе всего к тому компьютеру, за которым в данный момент работают. Если бы можно было определить машину, с которой отправлена почта, вероятно, удалось бы вычислить и принтер. Существует много способов получить информацию о связи компьютер-принтер, например, из отдельного файла, из поля в базе данных узлов, о которой упоминалось в главе 5, или даже из службы каталогов LDAP. Вот простой пример, в котором используется простая база данных компьютеров и связанных с ними принтеров:

use Mail: .'Internet; use DB_File;

Slocaldomain = ".example.com";

# printdb - это файл Berkeley DB.

Ключи - имена узлов, значения - принтеры Sprintdb = "printdb";

п анализируем сообщение

Smessage = new Mail::Internet \*STDIN;

$message->head->unfold();

# проверяем тему сообщения

my Ssubject = $<nessage->head->get('Subject');

if (Ssubject =" /print(er|ing)?/i){

ff ищем машину-отправителя (ситаем, что используется формат заголовков Sendmail)

$received = (reverse $rr,essage->head->get( 'Received' ))[0];

($host) =

$received =" /"from \S+ \((?:\S+@)?(\S+)\Q$localdomain\E \[/; }

tie %printdb, "DB_File",Sprintdb or die "Невозможно подключиться к базе данных

Sprintdb:$!\n";

print "Проблема на машине Shost может быть связана с принтером " .

$pnntdb{$host} . "Дп";

untie %printdb;

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

Проблема на машине buggypea* может быть связана с niroshige. Потратив время на изучение структуры своего окружения, вы найдете разные способы получать больше пользы от почты, доставленной в службу поддержки. Приведенные в этом разделе примеры невелики и созданы для того, чтобы заставить вас задуматься о возможностях. Как еще могут помочь программы, читающие почту (возможно, это почта, отправленная другими программами)? Perl предоставляет много способов проанализировать почту, рассмотреть ее в широком контексте и затем использовать найденную информацию.