Правильный email-клиент
Учимся работать с электрической почтой без использования плодов чужого труда Игорь «Spider_NET» Антон
🕛 28.08.2008, 14:02
Учимся работать с электрической почтой без использования плодов чужого трудаEmail-клиент - программа, которой мы пользуемся регулярно. Количество «мыльниц», предлагаемых разработчиками, растет не по дням, а по часам. Выбор велик, но зачастую все эти многофункциональные монстры снабжены теми фишками, которые среднестатистическому пользователю могут вообще никогда не потребоваться, но за которые все же приходится платить. Что, тебя тоже не устраивает эта ситуация? Тогда мы покажем тебе, как написать свой собственный email-клиент, который будет уметь отправлять и принимать почту и тем самым, возможно, поможет тебе не только сэкономить бабло, но и заработать. Мы понимаем, что закодить такую программку, используя компоненты, как-то не по-хакерски, поэтому мы усложним задачу и рассмотрим почтовик на WinSock API. Полученные знания пригодятся тебе при создании как почтовиков, так и других полезных взломщику сетевых тулз.
Разбираемся с протоколами
Прежде чем приступить к практике и терзанию клавиатуры, давай разберемся с теорией приема и передачи почтовых сообщений. Ты наверняка знаешь (а если не знаешь, то стыд тебе и позор), что для передачи электронных писем существует старый проверенный протокол SMTP (Simple Mail Transfer Protocol - простой протокол передачи электронной почты), а для приема наибольшей популярностью пользуется POP3. Спецификация этих протоколов описана в RFC 2821 и RFC 1225. Я рекомендую тебе сразу же скачать эти доки, поскольку в них описано много интересных вещей, о которых в силу небольшого объема статьи я рассказать не смогу.
SMTP
После того как ты в своем почтовом клиенте состряпал письмо и нажал кнопочку «Отправить», твоя «мыльница» соединяется с указанным в настройках SMTP-сервером для передачи специальных команд, посредством которых и будет отправлено твое сообщение. Давай рассмотрим этот процесс на примере ручной отправки письма. За конвертом и марками можешь не бежать, все, что нам потребуется для этого, - telnet.exe, который входит в поставку Windows. Запускай cmd.exe и набирай команду «telnet your smtp server порт». Порт SMTP-сервера по умолчанию 25, но некоторые админы его изменяют. У меня есть почтовый ящик в домене inbox.ru, поэтому я вводил «telnet smtp.inbox.ru 25». После установки соединения с удаленным сервером мы получим приветствие.
Сервер сказал нам «Здорова», а значит, нам необходимо проявить вежливость и ответить тем же. Для этого отправь команду «HELO ИмяСвоегоКомпа»:
HELO SPIDER
В случае успешного выполнения команды сервер вернет нам код 250, что будет означать, что «все тип-топ», можно продолжать дальше. Сообщим серверу отправителя письма:
MAIL FROM:<spider_net@inbox.ru>
В ответ на это мы снова получим сообщение с кодом 250, свидетельствующее об успешном выполнении команды. Обрати внимание, что в MAIL FROM нужно указывать свой ящик на этом сервере, обычно он выступает гарантом того, что ты свой человек, а не мерзкий спамер, которому не разрешается пользоваться сервером. Хотя тебе ничто не мешает указать ящик другого пользователя на этом же сервере :).
Ниже я приведу остальную часть «диалога» с SMTP-сервером, снабдив все это дело необходимыми комментариями. Символ «>» в начале строчки указывает на то, что эту команду посылает клиент, а символом «<» помечаются ответы сервера.
> RCPT TO:<antonov.igor.khv@gmail.com> //Получатель/получатели сообщения
< 250 Accepted
> DATA //После этой команды отправляется заголовок и тело письма
//Сервер разрешает нам ввод текста нашего сообщения. По завершению набора нужно отправить символ точки на новой строке.
< 354 Enter message. Ending with "." on a line by itself
> From: <spider_net@inbox.ru> //От кого
> To: <antonov.igor.khv@gmail.com> //Кому
//Кодировка письма
> Mime-Version: 1.0
> Content-Type: text/plain; charset="windows-1251"
//Текст сообщения
> Это текст сообщения
//Даем понять серверу, что наше сообщение сформировано
>.
< 250 OK id = 1bKd33kk33
//Завершаем соединение с сервером
> QUIT
< 221 smtp.inbox.ru closing connection
Вот таким образом TheBat! и прочие Outlook'и передают нашу корреспонденцию. Вроде бы ничего сложного, а некоторые программисты на этом неплохо зарабатывают.
Коды ответов SMTP-сервера
Ты заметил, что почти на каждую нашу команду сервер отвечает соответствующим цифровым кодом? Благодаря этим ответам мы можем анализировать происходящую ситуацию. Например, если мы отправим некорректную команду, то сервер вернет нам код 500, который означает «unrecognized command» («команда не распознана»). Чтобы все письма передавались без сучка и задоринки, в программе-клиенте надо реализовать проверку возвращаемых сервером команд, а для этого в свою очередь необходимо знать их значения. Все возможные коды (на основании RFC 2821) я привел в соответствующей табличке.
POP3
Общение клиента с POP3-сервером схоже с SMTP. В этом протоколе также определен набор команд, посредством которого и происходит обмен информацией. Для лучшего понимания опять же приведу пример типичной сессии соединения через Telnet с POP3-сервером моего провайдера.
//Соединение с POP3-сервером установлено
<+OK
//Отправляем свой логин
> USER spider_net@inbox.ru
//Все пучком, такой пользователь есть
< +OK Password required for user spider_net@inbox.ru
//Говорим свой пароль
> PASS 1234
//Залогинились успешно, в ящике семь писем
< +OK spider_net@inbox.ru maildrop has 7 messages (45056 octets)
//Запрашиваем список писем
> LIST
//Всего семь писем общим размером 45056
< +OK 7 messages (45056)
//Далее идет список писем в формате: «номер_письма» «размер»
< 1 2204
< 2 2304
….
.
//Запрашиваем письмо с идентификатором 1
> RETR 1
//Текст письма
.
//Помечаем на удаление письмо с идентификатором 1
> DELE 1
< +OK message 1 deleted
//Нет, лучше вернем его обратно. Снятие пометки удаления
> RSET
< +OK maildrop has 7 messages
//Закрываем соединение с POP3-сервером
< QUIT
> OK POP3 server at inbox.ru signing off
В отличие от SMTP, в протоколе POP3 не реализованы коды ошибок, вместо них предусмотрены служебные +OK (команда успешно выполнилась) и -ERR (описание ошибки).
Обрати внимание на команду DELE. После ее выполнения физическое удаление письма не произойдет до тех пор, пока POP3-сервер не перейдет в состояние UPDATE. Переход в это состояние начинается после отправки команды QUIT. До начала стадии UPDATE ты в любое время имеешь полное право отказаться от удаления выбранного письма, точнее, снять пометку на удаление с письма.
Вездесущий WinSock API
Выше мы с тобой договорились, что для решения поставленной задачи мы не станем пользоваться готовыми компонентами, а реализуем все по-мужски - с использованием лишь функций, предоставленных сетевой моделью Windows. Хорошо разобравшись с функциями, входящими в WinSock API, ты сможешь написать любое сетевое приложение. Кроме того, используя функции лишь из первой версии сетевой библиотеки, ты имеешь возможность портировать приложения в никс-системы. Итак, давай разберем функции, которые нам потребуются.
function WSAStartup (wVersionRequested:word; var WSAData:TWSAData):integer; stdcall;
Это функция, с вызова которой нужно начинать программирование любого сетевого приложения. Она предназначена для инициализации сетевой библиотеки Windows. Функции нужно заслать два параметра:
1. wVersionRequested - версия инициализируемой библиотеки. Их всего две - 1.1 и 2.0. Например, для первой версии пишем makeword(1,1). 2. Указатель на структуру WSAData. После выполнения функции в эту структуру запишется информация о сетевой библиотеке.
При успешном выполнении функция вернет 0. Для получения кодов ошибок в WinSock API служит функция WSAGetLastError(). Ей не нужно передавать какие-либо параметры, после вызова она возвращает код последней возникшей ошибки при работе с сетевыми функциями.
function socket (af:integer; type:integer; protocol:integer):TSocket, stdcall;
Перед тем как соединиться с удаленным узлом, нужно создать «розетку» - socket. Как раз за его создание и отвечает одноименная функция socket. Входных параметров здесь три:
1. af - семейство протоколов. Нам потребуется лишь TCP, поэтому будем указывать AF_INET. 2. type - тип создаваемого сокета. Может быть Sock_stream (для протокола TCP/IP) и sock_dgram (UDP). 3. protocol - протокол. Для TCP нужно указать IPPROTO_TCP.
Результатом выполнения будет новый сокет. Создав сокет, можно пробовать подключиться. Для этого в библиотеке реализована функция Connect.
function Connect (S:TSocket; var name:TSockAddr; namelen:integer):Integer:stdcall;
Параметрами для функции служат:
1. s - socket, созданный функцией socket. 2. name - структура SockAddr, содержащая данные, необходимые для подключения (протокол, адрес удаленного компьютера, порт). 3. namelen - размер структуры типа TSockAddr.
Успешно выполнившись, а значит, и установив соединение, функция вернет 0 или ошибку, которую можно получить с помощью WSAGetLastError().
Структура TSockAddr выглядит так:
TSockAddrIN = sockaddr_in;
SockAddr_in = record
sin_family: u_short; //Семейство протоколов
sin_port: u_short; //Порт, с которым нужно будет установить соединение
sin_addr: TInAddr; //Структура, в которой записана информация об адресе удаленного компьютера
sin_zero: array[0..7] of Char; //Совмещение по длине структуры sockaddr_in с sockaddr и наоборот
emd;
Чтение и отправка данных удаленной стороне осуществляется с помощью функций send и recv. Они описаны следующим образом:
function send (s:TSocket, var Buf; len:integer; flags:integer):Integer;stdcall;
function recv (s:TSocket, var Buf; len:integer; flags:integer):Integer;stdcall;
Параметры для этих функций одинаковы:
1. s - сокет, на который нужно отправить (или принять) данные. 2. buf - буфер с данными для отправки (приема). 3. len - размер передаваемых (принимаемых) данных. 4. flags - флаги, отвечающие за метод отправки (приема).
Выполнившись, функция вернет фактическое количество отправленных/принятых байт.
function CloseSocket(s:TSocket):integer;stdcall;
Эта функция служит для закрытия сокета, переданного в качестве одного-единственного параметра.
Отливаем мыльницу
Вот мы с тобой и добрались до самого интересного - до практической части. От теории уже плавится мозг, нужно срочно упорядочивать полученные знания. Не вопрос! Запускай Delphi, создавай новый проект и срисовывай мою форму.
На форме у меня расположен компонент PageControl, благодаря которому созданы закладки. Первая закладка посвящена отправке писем, вторая - чтению, третья - настройкам (адреса SMTP- и POP-серверов и т.д.), в четвертой ведутся логи происходящего. Каждая закладка забита полями ввода (TEdit), TMemo и т.д.
Шкодинг
Как я уже говорил, первое, с чего необходимо начинать программирование любого сетевого приложения, - это инициализация сетевой библиотеки. Код, отвечающий за эту функцию, я повесил на событие OnCreate() формы:
if WSAStartup(makeword(1,1), _wData) <> 0 then
begin
ShowMessage('Ошибка при инициализации WinSock. Продолжение невозможно');
Application.Terminate;
end;
В коде я проверяю результат выполнения функции. Если он не равен 0, значит возникла ошибка и нет смысла продолжать работу программы, поэтому показываем сообщение и прерываем работу приложения.
По нажатию кнопки «Отправить» на закладке «SMTP-клиент» напиши код из соответствующей врезки.
Листинг отправки письма получился довольно-таки большим. Рассмотрим его внутренности. Первое, что я делаю, - это создаю новый сокет. Причем создаю сокет не функцией socket, о которой рассказывал выше, а еще неизвестной тебе CreateSocket(). Эту функцию я описал самостоятельно и реализовал в ней создание нового сокета и заполнение структуры TSockAddrIN. Результатом выполнения функции будет новый сокет и заполненная структура _server_addr (объявлена в глобальных переменных).
Сокет создан, а это означает, что можно начинать попытки соединения с удаленным сервером. Вызываю функцию Connect(), сразу же проверяя результат ее выполнения. Если он равен значению константы SOCKET_ERROR, то значит возникла ошибка и нужно узнать, какая именно. Для получения этой информации у меня определена процедура SocketsErrors(), а уже из этой процедуры происходит занесение информации в лог.
В случае успешного завершения работы функции я делаю небольшую задержку (в одну секунду). Задержка нужна для того, чтобы сервер успел среагировать и отправить нам свой ответ. Полученный ответ сохраняем в лог с помощью процедуры AddToLog(), в которой в качестве параметра передаем результат выполнения функции ReadFromSocket() - еще одной самописной функции, введение которой спасет код от большего числа одинаковых конструкций. Код функции ReadFromSocket() ты можешь увидеть в могучей врезке «Удобная функция для облегчения приема данных».
Вызвав функцию ReadFromSocket, мы прочитали первую порцию данных от SMTP-сервера. Ты внимательно изучил теорию и теперь знаешь, что самыми первыми данными, которые отправляет удаленный сервер, является приветствие. Получив его, можно начинать «диалог» с SMTP-сервером. Все остальное, что происходит в коде, тебе должно быть уже знакомо по рассмотренному нами выше примеру сессии общения с SMTP-сервером. Давай лучше поглядим поближе на ReadFromSocket() - процедуру, через которую мы получаем все данные, пришедшие нам от сервера. В самом начале процедуры я очищаю переменную, в которую буду принимать данные от сервера. Для этого я полностью забиваю ее нулями. Затем я вызываю функцию приема данных recv(). О входных параметрах для этой функции я уже рассказывал, поэтому сейчас мы их пропустим. Приняв данные, можно начать ими распоряжаться. Я подготавливаю их к форматированию и последующему выводу на экран пользователя. Под форматированием я подразумеваю нормальный построчный вывод полученных данных в компонент Memo. Для отправки данных серверу я создал функцию - SendToSocket. В качестве параметров ей нужно передать лишь сокет и данные, которые будут отправлены. Весь остальной код, отвечающий за подключение к POP3-серверу, я приводить не буду, постольку он полностью аналогичен коду на врезке, за исключением лишь самих команд, посылаемых серверу. Ты же всегда можешь посмотреть прокомментированный исходник на нашем диске. На этом я хочу откланяться и пожелать тебе удачи в нелегком кодерском труде.
ЧАВО при использовании WinSock API
Q: Я запустил исходник примера, заполнил настройки, нажал «Отправить» и программа стала подвисать. Почему?
A: При использовании WinSock API при вызове какой-либо функции возникает «застывание» окна приложения. Для решения этой проблемы можно вынести код с обращением к WinSock API в отдельный поток или использовать для получения данных сокетов событийную модель Windows.
Q: Я набрал весь код из журнала, но моя программа не хочет компилироваться, ругается на на использование неизвестных функций. В чем проблема?
A: Скорей всего, ты забыл добавить в uses ссылку на модуль winsock с описанием сетевых функций.
Q: Модуль подключил, помогло частично, но на вызове некоторых функций все равно возникает ошибка: «Неизвестная функция».
A: Вероятнее всего, ты подключил старый модуль. Тот модуль, который идет в поставке с Delphi, содержит описание функций лишь первой версии. Чтобы получить возможность заюзать новые функции, нужно скачать заголовочный файл. Найти его можно на нашем диске.
Удобная функция для облегчения приема данных
function TForm1.ReadFromSocket(socket: TSocket): String;
var
_buff: array [0..255] of Char;
_Str:AnsiString;
_ret:integer;
begin
fillchar(_buff, sizeof(_buff), 0);
Result:='';
_ret := recv(socket, _buff, 1024, 0);
if _ret = -1 then
begin
Result:='';
Exit;
end;
_Str := _buff;
while pos(#13, _str)>0 do
begin
Result := Result+Copy(_str, 1, pos(#13, _str));
Delete(_str, 1, pos(#13, _Str)+1);
end;
end;
Код по нажатию кнопки «Отправить»
var
_Socket:TSocket;
_str:string;
I,J:integer;
begin
PageControl1.ActivePageIndex:=3;
AddToLog('Подготовка сокета');
//Создаем сокет для подлючения к smtp серверу
_Socket := CreateSocket(smtpServerEdit.Text, StrToInt(SmtpPortEdit.Text));
//Пробуем подсоединиться к SMTP-серверу
if (Connect(_Socket, _server_addr, sizeOf(_server_addr)) = SOCKET_ERROR) then
begin
SocketsErrors();
Exit;
end;
sleep(1000);
//Прочитаем приветствие сервера
AddToLog(ReadFromSocket(_Socket));
SendToSocket(_socket, 'HELO '+GetLocalHost);
sleep(100);
AddToLog(ReadFromSocket(_Socket));
SendToSocket(_socket, 'MAIL FROM:<'+FromEdit.Text+'>');
sleep(100);
AddToLog(ReadFromSocket(_socket));
SendToSocket(_socket, 'RCPT TO:<'+ToEdit.Text+'>');
sleep(100);
AddToLog(ReadFromSocket(_socket));
//Заполняем заголовок письма
SendToSocket(_socket, 'DATA');
sleep(100);
AddToLog(ReadFromSocket(_socket));
//От кого
SendToSocket(_socket, 'From:<'+FromEdit.Text+'>');
//Кому
SendToSocket(_socket, 'To:<'+ToEdit.Text+'>');
//Тема письма
SendToSocket(_socket, 'Subject: '+SubjectEdit.Text);
SendToSocket(_socket, 'Mime-Version: 1.0'+#13+#10+'Content-Type: text/plain; charset="windows-1251"');
//Программа-отправитель
SendToSocket(_socket, 'X-Mailer: MyMailProgram');
//Текст письма
For I:=0 to TextMemo.Lines.Count-1 do
begin
_str:=TextMemo.Lines.Strings;
while _str<>'' do
begin
j:=SendToSocket(_socket, _str);
if j=SOCKET_ERROR then
break;
Delete(_str, 1, j);
end;
end;
sendToSocket(_socket,#13+#10+'.');
AddToLog(ReadFromSocket(_socket));
sendToSocket(_socket, 'QUIT');
AddToLog(ReadFromSocket(_socket));
CloseSocket(_socket);
end;