POP3クライアントのモジュールを書いてみた
さて、今回はPOP3クライアントのモジュールを書いてみようと思います。前回、半泣きでテストを書いてみたので、それに合わせてつらつらと書いていけば良いはずです。
ところでPOP3では、三つの状態(AUTHORIZATION, TRANSACTION, UPDATE)が規定されています。「クライアント側だから考慮しなくていいかなぁ」と迷ったものの、そういう仕様を知ってしまった以上は実装してみたいと思います。趣味のYAGNI破りです。
では、前回ざっと決めたデザイン(オブジェクトを使う、POP3サーバーとの入出力は関数で包んじゃう、など)に沿ってコードを書いていきます。いつも通り、コンパイルエラーの山と戦うのですが、経験値が上がってきたおかげで以前ほど、苦しまなくなってきました。
で、できたのがこんなコード。
type stat =
| Authrization
| Transaction
| Update
| Close
type cmd =
| User of string
| Pass of string
| Quit
| Stat
| List of int option
| Retr of int
| Dele of int
| Noop
| Rset
| Top of int * int
type res =
| OneLine
| MultiLine
exception Error_response
type cmd_info_rec = {str:string; res:res; trans:stat option}
let cmd_info stat cmd =
let sp = Printf.sprintf in
let otos = function Some i -> sp " %d" i | None -> "" in
match stat with
| Authrization -> (
match cmd with
| User s -> {str = sp "USER %s" s; res = OneLine; trans = None}
| Pass s -> {str = sp "PASS %s" s; res = OneLine; trans = Some Transaction}
| Quit -> {str = "QUIT"; res = OneLine; trans = Some Close}
| x -> invalid_arg "stat is Authrization"
)
| Transaction -> (
match cmd with
| Stat -> {str = "STAT"; res = OneLine; trans = None}
| List o -> {str = "LIST" ^ (otos o); res = MultiLine; trans = None}
| Retr i -> {str = sp "RETR %d" i; res = MultiLine; trans = None}
| Dele i -> {str = sp "DELE %d" i; res = OneLine; trans = None}
| Noop -> {str = "NOOP"; res = OneLine; trans = None}
| Rset -> {str = "RSET"; res = MultiLine; trans = None}
| Top (i, j) -> {str = sp "TOP %d %d" i j; res = MultiLine; trans = None}
| Quit -> {str = "QUIT"; res = OneLine; trans = Some Update}
| x -> invalid_arg "stat is Transaction"
)
| x -> invalid_arg "stat is neither Authrization nor Transaction"
let recieve_response infun resmode =
let re = Str.regexp (if resmode = OneLine then "\r\n" else "\r\n.\r\n") in
let bufsize = 1024 in
let resbuf = Buffer.create bufsize in
let tmpbuf = String.create bufsize in
let rec recv () =
let rlen = infun tmpbuf 0 bufsize in (
try
let pos = Str.search_forward re tmpbuf 0 in
Buffer.add_substring resbuf tmpbuf 0 pos;
Buffer.contents resbuf
with
| Not_found ->
Buffer.add_substring resbuf tmpbuf 0 rlen; recv ()
) in
recv ()
let parse_and_check_response resbuf =
let list = Str.split (Str.regexp "\r\n") resbuf in
match list with
| [] -> failwith "invalid response";
| hd::tl -> (
let res = Str.bounded_split (Str.regexp " ") hd 2 in
match (List.hd res) with
| "+OK" -> String.concat " " (List.tl res)::tl
| "-ERR" -> raise Error_response
| _ -> failwith "invalid response"
)
class pop3_client ~infun ~outfun = object (self)
val mutable stat = Authrization
val infuc = infun
val outfun = outfun
method private execmd cmd =
let ci = cmd_info stat cmd in
let sendstr = ci.str ^ "\r\n" in
outfun sendstr 0 (String.length sendstr);
let resbuf = recieve_response infun ci.res in
let reslist = parse_and_check_response resbuf in (
match ci.trans with
| None -> ()
| Some s -> ignore (stat <- s)
);
reslist
method auth ~user ~pass =
ignore (recieve_response infun OneLine);
ignore (self#execmd (User user));
ignore (self#execmd (Pass pass));
()
method quit () = ignore (self#execmd Quit)
method stat () = self#execmd Stat
method list ?(msgno) () = self#execmd (List msgno)
method retr ~msgno = self#execmd (Retr msgno)
method dele ~msgno = ignore (self#execmd (Dele msgno))
method noop () = ignore (self#execmd Noop)
method rset () = ignore (self#execmd Rset)
method top ~msgno ~line = self#execmd (Top (msgno, line))
end
今回悩んだのは、共通処理をletによる関数定義にすべきか、privateメソッドにすべきか、というところでした。 他のライブラリのコードを適当に眺めて見たんですが、どうもポリシーが読み取れなかった。
それはさておき、改めてコードを見てみると、サービス精神のかけらも無いインターフェースにひるみますねぇ。このモジュールを使ったプログラムが、list や retr メソッドのレスポンスを涙ぐましく処理している姿が目に浮かびます… 「一つメソッドを叩けば、各メッセージについてヘッダーの連想配列と本文のタプルをリストにして返してくれる」、そんなやさしいメソッドがあってしかるべきですが、力尽きてきたのでこの辺で…
あと、本当はOCamlのシグネチャという機能を活用して、モジュールに対するアクセスを制限しなきゃならないはず。recieve_responseとかの関数は、外から呼ばれるとまずいのですから。これも力尽きたのを理由に省略…
というわけで、いくつかの手抜きをやり過ごしつつ実際に動かしてみましょう。そもそもの目的が「Gmailにアクセス」だったのでそれをやってみます。
open Unix
open Ssl
open Pop3
type conn_info = {host:string; port:int; cafile:string}
type user_info = {user:string; password:string}
let ci = {host = "pop.gmail.com"; port = 995;
cafile = "/usr/ports/security/ca-roots/files/ca-root.crt"}
let ui = {user = "hogehoge"; password = "hogepass"}
let ssl_conn info =
Ssl.init () ;
let hostent =
try gethostbyname info.host with
| Not_found -> failwith "gethostbyname" in
let sockaddr = ADDR_INET((hostent.h_addr_list.(0)), info.port) in
let ctx = Ssl.create_context Ssl.SSLv23 Ssl.Client_context in
Ssl.load_verify_locations ctx info.cafile "";
let ssl = Ssl.open_connection_with_context ctx sockaddr in
Ssl.verify ssl;
ssl
let main () =
let ssl = ssl_conn ci in
let p = print_endline in
let p_hr () = print_endline "--------------------------" in
p "connecting...";
p_hr ();
let pop3 = new pop3_client ~infun:(Ssl.read ssl) ~outfun:(Ssl.write ssl) in
pop3#auth ~user:(ui.user) ~pass:(ui.password);
p "mail list";
p_hr ();
p (String.concat "\n" (pop3#list ()));
p_hr ();
let rec loop () =
p "please enter msgno > ";
try
let msgno = read_int () in
p_hr ();
p (String.concat "\n" (pop3#retr ~msgno:msgno));
p_hr ();
loop ()
with _ -> () in
loop ();
pop3#rset ();
pop3#quit ();
Ssl.shutdown_connection ssl
;;
let () = main ()
SSL接続のところは以前のコードの流用です。で、取得した接続を、Ssl.read/writeに部分適用して入出力用の関数を作成し、pop3_clientに渡してあげています。これを以下のようにコンパイルしてみます。
ocamlbuild -ocamlc 'ocamlfind c -package str,ssl -linkpkg' mailan.byte
そして、おもむろに./mailan.byteと叩いてみると…
mitsu@garlic$ ./mailan.byte
connecting...
--------------------------
mail list
--------------------------
1 messages (1633 bytes)
1 1633
--------------------------
please enter msgno >
1
--------------------------
message follows
Delivered-To: hogehoge@gmail.com
Received: by 10.20.30.40 with SMTP id 28314098way;
Mon, 2 Jun 2008 07:30:29 -0700 (PDT)
From: hogehoge@hotmail.com
To: hogehoge@gmail.com
Subject: test mail
Date: Mon, 2 Jun 2008 14:29:12 +0000
Content-Type: text/plain; charset="iso-2022-jp"
MIME-Version: 1.0
X-OriginalArrivalTime: 02 Jun 2008 14:29:13.0438 (UTC) FILETIME=[078997E0:01C8C4BD]
hello, world
_________________________________________________________________
Live Search CO?^8!:w$GEl5~$NET?4It$,3DI=<($K!*%I%3$r$_$F$_$k!)
http://maps.live.com/
--------------------------
please enter msgno >
q
mitsu@garlic$
一応、成功です(出力内容は一部編集してます)。
メッセージの一覧が表示され、その後メッセージ番号を入力すると、当該メールのヘッダと本文が表示されました(LISTとRETRコマンドのレスポンスを表示しているだけとも言う…)。
メールに付与している広告文が文字化けしていますが、iso-2022-jp⇒UTF-8(私の開発環境)の文字コード変換が必要っぽいです。OCamlでは、camomileというライブラリをつかうのが定番のようですが、それはまた別の話…
補足: コード中に "rn" とか "n" といった文字列がありますが、CR-LF等です。エスケープシーケンスが消えちゃっているようです。
- 前のエントリー: POP3クライアントのテストを書いてみる
- 次のエントリー: makecontext()でマルチタスクっぽく
- 1人の推薦記事
- 0人がクリップ
-
ソーシャルブックマーク(-)
- トラックバック(0)
- 2件のコメント
#1 ogasawara@itpl
- 2008/06/08 16:36:13
#2 komamitsu
- 2008/06/08 21:46:14
- ホワイトペーパー
- 企画特集
次のキーワードは、SOIとITFM
ただのERPだけじゃもう足りない!
REAL IT COOL PROJECT
Green Enterprise
国内シェアNo1
情報大洪水時代を生き抜くソリューション
グローバル競争に金融不安……
IT技術者はもっと幸せになれる!
Touch Diamond VS iPhone 3G
局所冷却に注目。
NECのグリーンITの本気度「PUEは1.58」
仲間と情報を共有・公開する楽しみ方は?
グリーンデータセンターの新潮流
価格から質へと変わるアウトソーシング市場
今なら無料! IBMの「グリーンIT化診断」
- 運用管理こそ業務の背骨
- 解約率わずか1%のホスティングサービス
- なぜ、ERP 導入は敷居が高いのか?
- エンタープライズサーチ特集!
- データバックアップで事業継続力を向上!
- 市場のニーズに応えた帳票ソリューション
- 所有から利用へ。拡大するiDC市場
- MicrosoftもOracleもDWH市場に参入!
- Web2.0時代におけるセキュリティ上の課題
- ERP,CRMのレスポンスの遅さを劇的改善!
- 話題のタグ
Javaの父:「Javaはレガシーか?」「その表現は妥当ではない」と反論
マルチコア、マルチスレッド時代に果たすOSの役割
ゴスリング氏、Javaの最新動向を説明:JavaFXとWiiの連携アプリも登場
サン・マイクロシステムズ、RIA用開発プラットフォーム「JavaFX 1.0」をリリース
CSS 3のボックスレイアウトでコンテンツを画面の中央に配置する
ローカルへのデータの保存を可能にするDOM Storage
iPhoneアプリ開発苦労話ベスト3―伝説の開発者に訊く(その1)
あなたの知らないPKI(2)--PKIのコア要素/電子証明書と認証局
不況に強いITを目指す--不況時に高い需要が望める分野はこれだ
最初からXenを組み込んだPCが出荷される:Xenプロジェクト創始者の将来予測