今回のお題
先日引き受けた相談事は「メールを使ってデータベースにレコードを登録したい」とのこと。レコードを本文としたメールを投げて、そいつがデータベースに格納されるカラクリが欲しいってんですね。例えば売上データやらアンケート結果やら、あちこちで収集したデータをメールでかき集めるのが目的なのでしょう。「そんなもん、どこぞにWeb-serviceをホストしておいてHTTPで投げればいぃんじゃね?」と答えたところ、できるだけチープに実現したく、サーバ立てるのは避けたいそうな。
なるほど、そう言うことなら納得です。テキトーなメールアカウントを1つ用意し、みんなが寄ってたかってそいつめがけてメールを投げつけ、小さなアプリケーションがメールを読んでデータベースに流し込むってスンポーですか。「登録したいレコードって定型なの? どれも同じフォーマットなら実装楽なんだけど」と訊くと依頼主曰く「いや、ゆくゆくはいろんな目的に利用することになりそうなので、フォーマットはできるだけ潰しの利くものにしてほしい」と。「んー…それじゃXMLでやっちゃいましょうか」ってことになり、「POP3からメールを取り出し、そこに書かれたXMLを解釈してデータベースに流し込む」小さなお試しアプリケーションを作ることになりました。
「で、〆切はいつ?」「できれば明日までに」うひー…
POP3からメールを取り出す
まずはPOP3サーバーからメールを取り出すところから始めます。デキトーなメールアカウントに以下のようなメールを投げておきます。
しばらく待って届いた頃を見計らってtelnetでPOP3に繋いで読み込んでみます。
+OK Qpopper (version 4.0.9) at localhost starting. <9999.1345119383@localhost> USER episteme ■ +OK Password required for episteme. PASS abracadaburachichinnpuipui ■ +OK episteme has 3 visible messages (0 hidden) in 6481 octets. LIST ■ +OK 3 visible messages (6481 octets) 1 1451 2 2436 3 2594 . RETR 3 ■ +OK 2594 octets Return-Path: <episteme@cppll.jp> ...省略... X-UIDL: Do6"!I)=!!+0A"!0`P"! <?xml version='1.0' encoding='iso-2022-jp' ?> <words> <word en='dog' ja='B$$$LB' /> <word en='cat' ja='B$M$3B' /> </words> . DELE 3 ■ +OK Message 3 has been deleted. QUIT ■ +OK Pop server at localhost signing off.
"■"のついた行は僕が入力した部分です。POP3に繋がったら:
- USER <ユーザ名>とPASS <パスワード>を送信
- LISTで得られた各メールIDに対し
- RETR <メールID>でナカミを取り出し
- 必要ならDELE <メールID>で削除して
- QUITで抜ける
この手順を生socketでちまちま書いてもさほどの手間はかからんけども、いかんせん〆切が迫っているのでBoost.Asioで済ませます。必要なのはXMLだけですから、RETRで得られる文字列のうち"<?xml..."から"."直前までを切り出します。
#include <iostream> #include <sstream> #include <string> #include <vector> #include <iterator> #ifndef _WIN32_WINNT #define _WIN32_WINNT 0x0501 #endif // Boost #include <boost/asio.hpp> using namespace std; using namespace boost::asio; /* * モロモロの定数値 */ // POP3 サーバのアドレスとポート const string pop3_address = "mail.dokkanoserver.com"; const string pop3_port = "110"; // POP3 アカウントとパスワード const string mail_user = "episteme"; const string mail_pass = "abracadabra"; inline bool starts_with(const string& str, const string& pattern) { return str.find(pattern) == 0; } template<typename OutputIterator> OutputIterator get_xml_from_pop3(const string& address, const string& port, const string& user, const string& pass, OutputIterator out) { string line; io_service io; ip::tcp::iostream pop3(address,port); vector<int> mail_id; getline(pop3, line); // USER pop3 << "USER " << user << endl; getline(pop3, line); if ( !starts_with(line,"+OK") ) goto quit; // PASS pop3 << "PASS " << pass << endl; getline(pop3, line); if ( !starts_with(line,"+OK") ) goto quit; // LIST pop3 << "LIST" << endl; getline(pop3,line); if ( !starts_with(line,"+OK") ) goto quit; // メールIDを読み取る for (;;) { getline(pop3,line); if ( !line.empty() && line[0] == '.' ) break; istringstream stream(line); int id; stream >> id; mail_id.push_back(id); } // RETR <メールID> でメールを読む for ( vector<int>::size_type i = 0; i < mail_id.size(); ++i ) { pop3 << "RETR " << mail_id[i] << endl; bool has_xml = false; ostringstream stream; // "<?xml..." から "."までを抽出 for (;;) { getline(pop3, line); if ( !line.empty() && line[0] == '.' ) break; if ( line.find("<?xml") == 0 ) { has_xml = true; } if ( has_xml ) stream << line << endl; } if ( has_xml ) { *out++ = stream.str(); // DELE <メールID> で削除 pop3 << "DELE " << mail_id[i] << endl; getline(pop3,line); } } quit: // QUIT pop3 << "QUIT" << endl; return out; } int main() { vector<string> msgs; get_xml_from_pop3(pop3_address, pop3_port, mail_user, mail_pass, back_inserter(msgs)); wcout << L"----------- I've got these XMLs ------------------" << endl; for_each(begin(msgs), end(msgs), [](const string& body) { cout << body << endl; }); }