n e t
3 t o n
Death to Humans! (c) Bender
.main
.txt
.prj
.img
.inf
.lnk
.Article

Автоматизация электронной почты

Какие из ежедневных операций по отправке исходящей и разбору входящей почты можно переложить на плечи программного обеспечения?

Сегодня электронная почта — пожалуй, самый распространенный интернет-сервис. Он активно используется во всем мире как при организации бизнес-процессов, так и в персональном применении. Объем пересылаемой информации огромен, и зачастую возникает необходимость автоматизации отправки и разбора писем.

Предположим, нам нужно решить такие задачи:

  • отправить письмо с темой Test, текстом Test message и вложенным файлом C:\out\Test.doc по адресу fio@firma.com;
  • найти в папке Входящие письмо с темой Test, полученное от адресата fio@firma.com, выделить тело письма, а вложенный файл сохранить в папке C:\in.

Эти задачи решаются тремя способами: с использованием механизма OLE, с помощью интерфейса MAPI(SMAPI) и путем прямого обращения к серверам SMTP/POP3. Для этого нам потребуется написать небольшие программы. Мы воспользуемся MS Visual C 6.0.

Почта через OLE

OLE (Objects Linked and Embedded) — это предложенная Microsoft технология, позволяющая получить доступ из одного приложения к объектам других приложений, называемых серверами автоматизации. Воспользуемся почтовым клиентом, который поддерживает данную технологию — MS Outlook из пакета MS Office. Кстати, все приложения этого пакета являются серверами автоматизации и предоставляют возможности по управлению ими.

Для взаимодействия с этими приложениями существуют специальные библиотеки типов — файлы с расширением *.olb, содержащие определения всех доступных объектов приложения. В частности, для MS Outlook 2000 это файл MSOutl9.olb, расположенный в папке MS Office.

Нам необходимо импортировать нужные объекты в разрабатываемое приложение. Для этого в MS Visual C надо вызвать команду ClassWizard>AddClassрFrom a type library и выбрать файл MSOutl9.olb (для MS Office 2000) в окне выбора файлов. Затем остается выделить объекты для импортирования. Нам потребуются следующие: _Application, _NameSpace, _Folders, MAPIFolder, _Items, _MailItem, Attachments. Отношения между ними приведены на рисунке. (Более подробную схему можно найти в MSDN).

Отношения между объектами MS Outlook (фрагмент структурной схемы).

В результате будут сгенерированы файлы с расширениями .h и .cpp, которые будут содержать «оболочечные» классы указанных объектов. Перед началом работы необходимо инициализировать OLE функцией AfxOleInit:

if(!AfxOleInit()) return FALSE;

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

_Application appOutlook;
_NameSpace   myNameSpace;
_Folders     myFolders;
MAPIFolder   myFolder;
_Items       myItems;
_MailItem    myItem;
Attachments  myAttachments;
// Создаем главный объект - Application
if (!appOutlook.CreateDispatch("Outlook.Application")) return FALSE;
myNameSpace = appOutlook.GetNamespace("MAPI");
// Получаем коллекцию папок
myFolders = myNameSpace.GetFolders();
// Переходим к [Outbox]
myFolder = myNameSpace.GetDefaultFolder(4 /*olFolderOutbox*/);
// Получаем коллекцию сообщений в папке
myItems = myFolder.GetItems();
// Создаем новое сообщение
myItem = myItems.Add(COleVariant((long)0) /*olMailItem*/);
// Заполняем поля сообщения
myItem.SetSubject("Test");
myItem.SetTo("fio@firma.com");
myItem.SetBody("Test message");
// Прикрепляем файл
myAttachments = myItem.GetAttachments();
myAttachments.Add(COleVariant("c:\\out\\Test.doc"),
                  COleVariant((long)1) /*olByValue*/,
                  COleVariant((long)15000),
                  COleVariant("Test.doc"));
// Отсылаем
myItem.Send();

А вот код для разбора входящей почты — нашей второй задачи:

Attachment myAttachment;
if (!appOutlook.CreateDispatch("Outlook.Application")) return FALSE;
myNameSpace = appOutlook.GetNamespace("MAPI");
myFolders = myNameSpace.GetFolders();
myFolder = myNameSpace.GetDefaultFolder(6 /*olFolderInbox*/);
myItems = myFolder.GetItems();
// Переходим к первому сообщению
myItem = myItems.GetFirst();
while (myItem)
{  
   // Ищем нужное нам сообщение
   if ( !(myItem.GetSubject().Compare("Test")) && 
        !(myItem.GetSenderName().Compare("fio@firma.com")) )
   {
      // Получаем коллекцию прикрепленных файлов
      myAttachments = myItem.GetAttachments();
      // Получаем первый файл
      myAttachment = myAttachments.Item(COleVariant((long)(1)));
      // Сохраняем файл
      myAttachment.SaveAsFile("c:\\in\\Test.doc");
   }
   // Переходим к следующему сообщению
   myItem = myItems.GetNext();
}

Как видно из листингов, OLE — весьма понятный и гибкий интерфейс. С его помощью легко управлять почтой, адресными книгами, настройками и др. Напомним также, что аналогично можно управлять и другими приложениями MS Office.

Передача почты через интерфейс MAPI (SMAPI)

Интерфейс MAPI (Message API) был разработан для обеспечения взаимодействия клиентских приложений с различными службами почтовой системы. Он предоставляет собой обширный набор функций и структур для работы с адресной книгой, хранилищами сообщений, службой транспорта и др. При регистрации в системе почтового клиента по умолчанию он прописывает в системе свою реализацию MAPI (копирует файл mapi32.dll в системную папку). Заметим, что иногда работа некоторых функций разных реализаций MAPI может отличаться от спецификации.

Для упрощения освоения и использования в MAPI был выделен подинтерфейс SMAPI (Simple MAPI), имеющий более простую организацию. Он предоставляет основные возможности, такие как отправка и разбор входящей почты, а также работа с адресной книгой. При разборе входящей почты имеется доступ только к стандартной папке Входящие (Inbox). Впрочем, этого зачастую вполне достаточно, так что более громоздкий MAPI не нужен.

SMAPI предоставляет в распоряжение разработчика всего 12 функций, расположенных в файле mapi32.dll и объявленных в mapi.h. Для того, чтобы ими можно было воспользоваться, их необходимо импортировать из dll-библиотеки. Нелишним также будет убедиться, установлен ли MAPI вообще. Учтя это, импортировать, к примеру, функцию MAPILogon можно так:

// Проверяем установлен ли MAPI
bool isMInst = (GetProfileInt("MAIL","MAPI",0) != 0) &&
               (SearchPath(0,"MAPI32.DLL",0,0,0,0) != 0);
if (!isMInst) return FALSE;
// Импортируем MAPILogon
LPMAPILOGON mlpMAPILogon;
HINSTANCE Mlib = LoadLibrary("mapi32.dll");
if (Mlib)
{
   mlpMAPILogon = (LPMAPILOGON)GetProcAddress(Mlib,"MAPILogon");
}

При работе с интерфейсом SMAPI прежде всего надо открыть сессию работы с почтовой системой функцией MAPILogon, а после завершения работы закрыть ее функцией MAPILogoff. Отправить почтовое сообщение можно с помощью функции MAPISendMail, которой надо передать указатель на структуру MapiMessage. Эта структура содержит полную информацию о сообщении, в том числе указатели на массивы структур MapiFileDesc и MapiRecipDesc, где хранится информация о вложенных файлах и получателях.

// Открываем сессию
LHANDLE pSessiya;
mlpMAPILogon(0, 0, 0, MAPI_LOGON_UI, 0, &pSessiya);
// Указываем получателя
MapiRecipDesc Rec;
ZeroMemory(&Rec, sizeof(MapiRecipDesc));
Rec.ulRecipClass = MAPI_TO;
Rec.lpszName     = "fio";
Rec.lpszAddress  = "fio@firma.com";
// Указываем вложенный файл
MapiFileDesc Attach;
ZeroMemory(&Attach, sizeof(MapiFileDesc));
Attach.nPosition    = (ULONG)-1;
Attach.lpszPathName = "c:\\out\\Test.doc";
Attach.lpszFileName = "Test.doc";
// Формируем сообщение
MapiMessage Mes;
ZeroMemory(&Mes, sizeof(MapiMessage));
Mes.lpszSubject  = "Test";
Mes.lpszNoteText = "Test message";
Mes.nRecipCount  = 1;
Mes.lpRecips     = &Rec;
Mes.nFileCount   = 1;
Mes.lpFiles      = &Attach;
// Отправляем сообщение
mlpMAPISendMail(pSessiya, 0, &Mes, 0, 0);
// Закрываем сессию
mlpMAPILogoff(pSessiya, 0, 0, 0);

Получить сообщение из стандартной папки Входящие (Inbox) можно с помощью функции MAPIReadMail. Ей следует передать указатель на идентификатор сообщения и необходимые флаги. С помощью флагов можно указать, следует ли читать только заголовок сообщения (MapiMessage) или подгружать вложенные файлы, а также нужно ли пометить сообщение как прочитанное.

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

Зная это, решим вторую поставленную задачу:

char pMsgID[512];
MapiMessage *lpMes;
FLAGS flFindM = MAPI_LONG_MSGID;
// Ищем первое сообщение
ULONG FinRes = mlpMAPIFindNext(pSessiya,0,0,0,flFindM,0,pMsgID);
// Читаем найденное сообщение
mlpMAPIReadMail(pSessiya,0,pMsgID,MAPI_PEEK,0,&lpMes);
while (1)
{
   // Ищем нужное сообщение
   if ( !lstrcmp(lpMes->lpszSubject,"Test") &&
        !lstrcmp(lpMes->lpOriginator->lpszAddress,"fio@firma.com") )
   {
      // Копируем первый прикрепленный файл
      CopyFile(lpMes->lpFiles[0].lpszPathName, "c:\\in\\Test.doc", 0);
   }
   // Ищем очередное сообщение
   FinRes = mlpMAPIFindNext(pSessiya,0,0,pMsgID,flFindM,0,pMsgID);
   If (FinRes != SUCCESS_SUCCESS) break;
   // Читаем очередное сообщение
   mlpMAPIReadMail(pSessiya,0,pMsgID,MAPI_PEEK,0,&lpMes);
}

При чтении сообщения функцией MAPIReadMail (если не указан флаг чтения только заголовка), прикрепленные файлы копируются во временную папку, а пути к ним записываются в соответствующие поля структур MapiFileDesc (напомню, что в MapiMessage есть указатель на массив этих структур). Удалить же сообщение по идентификатору можно с помощью функции MAPIDeleteMail.

Более подробную информацию о функциях и структурах можно найти в MSDN.

Как уже было сказано раньше, реализация MAPI у каждого почтового клиента своя и встречаются некоторые «отклонения» от стандартов. Так, для The Bat! не удается открыть сессию, если он не запущен. На мой взгляд, самое строгое соответствие спецификации у MS Outlook (а могло быть иначе? :)).

Прямое общение с серверами SMTP и POP3

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

Для отправки почты используется сервер SMTP, обмен данными с которым осуществляется по протоколу SMTP (Simple Mail Transfer Protocol), обычно через порт 25. Взаимодействие в рамках SMTP строится по принципу двусторонней связи, которая устанавливается между отправителем и получателем почтового сообщения (в роли получателя выступает сервер). Типичный обмен данными между получателем <SMTP-Serv> и отправителем <USER> выглядит следующим образом:

SMTP-Serv:	 220 mailserv.com Simple Mail Transfer Service Ready
USER:	         HELO MyComp 
SMTP-Serv:	 250 mailserv.com
USER:	         MAIL FROM: <fio1@somewhere.com> 
SMTP-Serv:	 250 OK
USER:	         RCPT TO: <fio2@mailserv.com> 
SMTP-Serv:	 250 OK
USER:	         DATA 
SMTP-Serv:	 354 Start mail input; end with <CRLF>.<CRLF> 
USER:	         Mail text... 
USER:	         ...etc. etc. etc. 
USER:	         . 
SMTP-Serv:	 250 OK 
USER:	         QUIT 
SMTP-Serv:	 221 mailserv.com Service closing transmission channel

Сразу после подключения сервер сообщает, что готов к принятию запросов. Первой командой отправителя должна быть HELO, аргументом служит имя его машины. В ответ сервер посылает свое имя. Вообще, каждый ответ сервера состоит из кода и краткого пояснения. Далее отправитель, с помощью команды MAIL FROM, должен начать почтовую транзакцию и указать, от кого отправляется сообщение. Некоторые серверы SMTP проверяют, зарегистрирован ли у них указанный пользователь, и если нет, — завершают почтовую транзакцию. Командой RCPT TO отправитель указывает получателя сообщения, после чего командой DATA сообщает серверу, что далее он будет передавать тело почтового сообщения. Тело должно заканчиваться последовательностью <CRLF><точка><CRLF> (<CRLF> — переход на следующую строку). После того, как сообщение было отправлено, необходимо завершить почтовую транзакцию командой QUIT. После ответа сервер закрывает соединение.

Следует отметить, что каждая строка запроса должна заканчиваться символами <CRLF>, а следующий запрос — посылаться только после ответа сервера на предыдущий.

Особенность использования SMTP заключается в том, что сервер воспринимает только ограниченное количество символов. В его символьную таблицу входят большие и маленькие латинские буквы, цифры и символы «+» и «/». Остальные символы сервером игнорируются. Поэтому при отправке простого текстового сообщения в латинице проблем не возникает, а вот о вложенных файлах и сообщениях с другим набором символов этого не скажешь. Для их отправки необходимо перекодировать тело сообщения в привычный для SMTP вид. Существует несколько общепринятых кодировок, в том числе base64, quoted-printable, 8bit.

Но как же передавать сложные сообщения с вложенными файлами и картинками? Где указывать используемую кодировку, тему сообщения? Когда возникла такая необходимость, было введено специальное расширение — MIME. При этом сообщение приобрело структурированную форму, с условным разделением на две части — заголовок и тело. Заголовок состоит из полей, которые, в свою очередь состоят, из имени поля и значения, разделенных символом «:». Тело же сообщения состоит из нескольких частей с разделителями между ними. Каждая часть представляет собой отдельное текстовое сообщение или вложенный файл. К примеру, сообщение в формате HTML с вложенной картинкой выглядит так:

FROM: SidorovSA <fio1@somewhere.com>
TO: PetrovDN <fio2@mailserv.com>
SUBJECT: Hello.
MIME-Version: 1.0
Content-Type: multipart/mixed; boundary="simple boundary"

--simple boundary
Content-Type: text/html; charset=iso-8859-1
Content-Transfer-Encoding: quoted-printable

<h3>Hello!</h3><img src="pic.jpg">
--simple boundary
Content-Type: application/octet-stream; name=pic.jpg
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename="pic.jpg"

PEhUTUw+D ... E1MPg=

--simple boundary--

Теперь перейдем к получению сообщений. Для этой цели используется сервер POP3, обмен данными с которым осуществляется по протоколу POP3 (Post Office Protocol). Обычно порт сервера POP3 — 110. Многие концепции и принципы протокола POP3 выглядят и функционируют подобно SMTP. При работе с POP3 тоже устанавливается соединение между сервером и получателем, только на этот раз в роли получателя выступает пользователь. В первую очередь он передает свое имя и пароль. После того как сервер их подтвердит, открывается почтовая транзакция:

USER:      USER fio
Pop3-Serv: +ОК
USER:      PASS password
Pop3-Serv: +ОК fio's maildrop has 2 messages (320 octets)

Когда почтовая транзакция открыта, пользователь может приступать к работе с сообщениями в своем почтовом ящике. Для этого у сервера POP3 есть чуть больше десятка команд. В ответ на них он может дать только два ответа: +OK (положительный) и -ERR (отрицательный). Аналогично SMTP, сервер POP3 передает также некоторое разъяснение. Почтовая транзакция закрывается передачей пользователем команды QUIT:

USER:      QUIT
Pop3-Serv: +OK dewey POP3 server signing off

С помощью команды STAT пользователь может узнать количество сообщений и их общий размер:

USER:      STAT
Pop3-Serv: +OK 2 320

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

USER:      DELE 1
Pop3-Serv: +OK message 1 deleted

Командой TOP пользователь может, не загружая всего сообщения, получить его заголовок и указанное количество строк тела. К примеру, для получения 10 строк тела первого сообщения используется такой код:

USER:      TOP 1 10
Pop3-Serv: +ОК
Pop3-Serv: ...

Наконец, с помощью команды RETR сообщение передается пользователю полностью:

USER:      RETR 2
Pop3-Serv: +OK 120 octets
Pop3-Serv: <the POPS server sends the entire message here>
Pop3-Serv: . . .

За дополнительной информацией можно обратиться к следующим документам: SMTP — rfc0821, rfc1123, POP3 — rfc1225, rfc1734, MIME — rfc1521, rfc1522 (www.ietf.org/rfc).

Для подключения к серверу можно воспользоваться стандартным механизмом сокетов. Например, подключиться к серверу SMTP можно так:

SOCKADDR_IN sockin;
// Настройки подключения
sockin.sin_family = AF_INET;
sockin.sin_port = htons(25); // 25 порт
sockin.sin_addr.s_addr = inet_addr("192.25.176.1");
// Создаем сокет
SOCKET sockt = socket(AF_INET,SOCK_STREAM,0);
// Соединяемся
connect(sockt,(LPSOCKADDR)&sockin,sizeof(sockin));
//... Работаем ...
// Закрываем соединение
closesocket(sockt);

Чтение из сокета осуществляется по команде recv, в которой указывается буфер и его размер, запись — по команду send с аналогичными аргументами, например:

char lpRes[128] = {0};
// Читаем с сокета
recv(sockt,lpRes,128,0);
// Пишем в него же то, что считали
DWORD lenRes = strlen(lpRes);
send(sockt,lpRes,lenRes,0);

Заключение

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

10.2005

Файлы примеров. (318 Kb)

Александр Харьков aka 3ton or net3ton

Copyright © 2006. 3ton aka Kharkov A S
Используются технологии uCoz