|
Какие из ежедневных операций по отправке исходящей и разбору входящей почты можно переложить на плечи программного обеспечения?
Сегодня электронная почта — пожалуй, самый распространенный интернет-сервис. Он активно используется во всем мире как при организации бизнес-процессов, так и в персональном применении. Объем пересылаемой информации огромен, и зачастую возникает необходимость автоматизации отправки и разбора писем.
Предположим, нам нужно решить такие задачи:
- отправить письмо с темой
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
|
|