CodeZine(コードジン)

特集ページ一覧

Rubyで簡易POP3サーバを作る

RubyによるネットワークプログラミングとUNIXシステムプログラミングの基礎

  • LINEで送る
  • このエントリーをはてなブックマークに追加
2006/11/10 00:00
目次

POP3コマンドの実装

 この節では、POP3の各コマンドの実装について解説します。

メインループ

 もう一度POPd#_sessionメソッドから話を始めます。

POPd#_session(3回目)
def _session
  stamp = apop_stamp()
  print_line "+OK popd #{stamp}"
  mailbox = auth(stamp)
  while cmd = read_cmd()
    mid = "cmd_#{cmd.name}"
    unless respond_to?(mid, true)
      print_line '-ERR unknown command'
      next
    end
    __send__(mid, mailbox, cmd.args)
  end
end

 authメソッド呼び出しの行までは既に説明したので、その次から見ていきましょう。auth_APOPメソッドにもあったような、POPコマンドを読み込むためのwhileループがあります。このwhile文の本体では、コマンドの種類に応じたメソッドを呼び出しています。このとき、普通のメソッド呼び出しではなく、Rubyのリフレクション機能を使っている点に注意が必要です。

 まず、このwhile文の本体で実行したいことは、次のようなことです。

case cmd.name
when 'STAT' then cmd_STAT(mailbox, cmd.args)
when 'LIST' then cmd_LIST(mailbox, cmd.args)
when 'RETR' then cmd_RETR(mailbox, cmd.args)
when 'QUIT' then cmd_QUIT(mailbox, cmd.args)
end

 しかし、このコードをそのまま書くのではあまりに芸がなさすぎます。実装するコマンドの種類が増えたらこのcase節も書き換えなければいけませんし、同じ形のコードがズラズラ並ぶのも嬉しくありません。

 そこでRubyのリフレクション機能を利用します。POPdクラスにcmd_XXXXXXXXはPOP3のコマンド名)というメソッドが定義されていたら、そのメソッドをXXXXコマンドの実装とみなして呼び出すようにします。

 この仕掛けのために、次の2つのメソッドを使います。

  1. Object#respond_to?
  2. Object#__send__

 1つめのObject#respond_to?は、そのオブジェクトにメソッドが定義されているか調べるメソッドです。例えば「"str".respond_to?(:size)」は、文字列"str"sizeというメソッドが定義されているかどうかを調べます。文字列(Stringオブジェクト)にはsizeメソッドが定義されているので、この式の返り値は当然trueです。

 また、respond_to?の第2引数がtrueだと、privateメソッドも調べます。POPdでこれから呼ぼうとしているcmd_XXXXメソッドはすべてprivateメソッドなので、第2引数にtrueを渡す必要があります。

 2つめのObject#__send__は、「メソッドを呼び出すためのメソッド」です。例えば「"str".__send__(:size)」は文字列"str"に対してsizeメソッドを呼び出します。この例では何も嬉しくないわけですが、呼び出すメソッド名(:size)が変数になると話が違ってきます。実行時にメソッド名を作って、そのメソッドを呼び出せるからです。

 また、__send__の第2引数以降は、__send__が呼び出すメソッドにそのまま引き渡されます。例えば「"str".__send__(:sub, /s/, 'p')」は「"str".sub(/s/, 'p')」と同じ意味です。

 以上の2つのメソッドの意味が分かれば、POPd#_sessionメソッドのコードは問題なく読めるでしょう。クライアントから送られたコマンド名に対応するcmd_XXXXメソッドが定義されているかどうかをrespond_to?(mid, true)で調べて、cmd_XXXXメソッドが定義されているならそれを__send__で呼び出します。定義されていなかったら、そのPOP3コマンドは実装されていないということですから、クライアントに「+ERR」を返して次のPOP3コマンドを読み込みます。

メタ情報の取得 ~STATコマンド~

 ではここからは各POP3コマンドの実装であるcmd_XXXXメソッドを見ていきましょう。まずはSTATコマンドの実装であるPOPd#cmd_STATメソッドを読みます。

POPd#cmd_STAT
def cmd_STAT(mailbox, args)
  return unless check_args(0, args)
  print_line "+OK #{mailbox.n_messages} #{mailbox.total_octets}"
end

 check_argsメソッドでコマンドの引数の数をチェックします。check_argsメソッドの内容は以下のとおりです。

POPd#check_args
def check_args(expect, args)
  unless args.size == expect
    print_line '-ERR syntax error'
    return false
  end
  true
end

 このように、引数の数が予想した数(第1引数expect)と違ったら、「-ERR」をクライアントに送ってfalseを返します。

 チェックが済んだら、cmd_STATメソッドの2行目(最後の行)でSTATコマンドに対する反応をクライアントに返します。STATコマンドの返答は、1つめの数値がメールボックスにあるメールの数、2つめの数値がメールの合計バイト数です。

 ちなみに、メールの合計バイト数を得る式はmailbox.total_octetsとなっていますね。このメソッド名にある「octets」というのは、「8ビット」を表す単語です。

 現在は1バイトと言えば普通8ビットですが、実は1バイトは8ビットとは限りません。「バイト」はそのコンピュータでの基本的なビット数を表す単位なので、コンピュータによっては、1バイトが9ビットだったり13ビットだったりします。そこで、確実に8ビットを表せる単語として、ネットワークプログラミングでは「octet(オクテット)」を使うのです。

メールのリスト ~LISTコマンド~

 次に、メールの一覧を得るLISTコマンドの実装であるcmd_LISTメソッドを確認しましょう。

POPd#cmd_LIST
def cmd_LIST(mailbox, args)
  case args.size
  when 0
    print_line '+OK'
    mailbox.each_message do |m|
      print_line "#{m.seq_number} #{m.octets}" unless m.deleted?
    end
    print_line '.'
  when 1
    ent = fetch_entry_n(mailbox, args.first) or next
    print_line "+OK #{ent.seq_number} #{ent.octets}"
  else
    print_line '-ERR syntax error'
  end
end

 実はLISTコマンドには引数が0個の場合と1つの場合があります。しかし、普通は引数が0個のLISTコマンドがあれば十分なので、0個の場合のコードだけ見ましょう。「when 0」の節を見てください。

 この節のコードは非常に直感的だと思います。まず最初の「+OK」を出力し、続いてmailbox.each_messageでメールボックス内のすべてのメールに対して繰り返して、それぞれのメール番号(m.seq_number)とオクテット数(m.octets)を出力します。最後に「.」だけの行を出力して終わりです。

メールの取得 ~RETRコマンド~

 次は、メールをサーバから取得するRETRコマンドの実装、cmd_RETRメソッドを確認しましょう。POPd#cmd_RETRメソッドのコードを以下に示します。

POPd#cmd_RETR
def cmd_RETR(mailbox, args)
  return unless check_args(1, args)
  m = fetch_entry_n(mailbox, args.first) or return
  print_line "+OK #{m.octets} octets"
  m.each_line do |line|
    print_message_line line
  end
  print_line '.'
end

 まず、POPd#cmd_STATのときと同じようにcheck_argsメソッドで引数の数をチェックします。

 次にfetch_entry_nメソッドを使い、mailboxからメール番号args.firstに相当するメール(Messageオブジェクト)を得ます。

 問題なくMessageオブジェクトが取得できたら、まずそのメールのオクテット数(m.octets)をprint_lineメソッドで出力します。

 さらに、Messageオブジェクトのeach_lineメソッドを使い、メールの内容を出力します。このとき、print_message_lineという特別な出力用メソッドを使っていますが、このメソッドは、このすぐあとで確認します。

 メールの内容をすべて出力したら、最後に「.」だけの行をprint_lineメソッドで出力して、終わりです。

 さて、さきほど出てきたprint_message_lineメソッドの内容は以下のとおりです。

POPd#print_message_line
def print_message_line(line)
  @output.print line.sub(/\A\./, '..').sub(/\r?\n\z/, "\r\n")
end

 このメソッドでは、1つめのsubで行頭の「.」を「..」に変換し、2つめのsubで改行コードを\r\nに統一しています。改行コードの統一に関しては特に問題ないと思うので、行頭の「.」についてだけ話しましょう。

 既に何度か説明したように、POP3では「.」だけの行でメールの末尾を示します。ということは、もともとメールに「.」だけの行が含まれていた場合、何もしないとそこでメールが終わりということになってしまいます。これは非常に困ります。

 そこでPOP3ではメールの内容を送るときに、行が「.」で始まっていたらもう1つ「.」を付けることになっています。この処理を行うことにより、「.」だけの行は「..」に変わり、メール全体を問題なく送信できるようにしています。そして、POP3クライアント側ではメールを受信したあとに行頭の「.」を削除すれば、元のメールが得られます。

メールの消去 ~DELEコマンド~

 今度はメールを消去するDELEコマンドの実装、POPd#cmd_DELEメソッドを確認します。

POPd#cmd_DELE
def cmd_DELE(mailbox, args)
  return unless check_args(1, args)
  m = fetch_entry_n(mailbox, args.first) or return
  m.delete
  print_line '+OK'
end

 もはや難しいところはないと思います。これまでと同じくcheck_argsメソッドでコマンドの引数の数をチェックし、fetch_entry_nメソッドでメール番号に対応するMessageオブジェクトを取得し、Message#deleteメソッドでそのMessageオブジェクトを削除します。

 ただし、いま「削除する」と言いましたが、DELEコマンドの段階では実際にメールボックスから削除するわけではありません。POP3では、TCP接続を切断する前であればRSETというコマンドを使い、DELEコマンドで削除したメールを復活できるようになっています。ですから、Message#deleteメソッドは実際にメールを削除するわけではなく、「このメールは削除された」というフラグを立てるだけです。削除マークの付いたメールは、QUITコマンドを実行したときに本当に削除されます。

POP3セッションの終了 ~QUITコマンド~

 最後に、POP3セッションを終了するQUITコマンドの実装、POPd#cmd_QUITメソッドを読みましょう。

POPd#cmd_QUIT
def cmd_QUIT(mailbox, args)
  begin
    mailbox.each_message do |m|
      m.delete_really
    end
    print_line '+OK'
  rescue
    print_line '-ERR delete incompleted'
  end
  terminate
end

 さきほど言ったように、QUITコマンドでは削除マークの付いたメールを実際に削除します。そのために、すべてのMessageオブジェクトについてMessage#delete_reallyメソッドを呼び、削除マークの付いているメールを削除します。すべて問題なく削除できたら「+OK」を出力します。

 「+OK」の出力まで終わったら、POPd#terminateメソッドを呼んでTCP接続を切断します。POPd#terminateメソッドのコードは以下のとおりです。

POPd#terminate
def terminate
  begin
    @input.close_read
  rescue
  end
  begin
    @output.close_write
  rescue
  end
  Thread.exit
end

 このterminateメソッド内で使っているメソッドは、どれも普段のプログラミングでは目にしないのではないでしょうか。IO#close_readメソッドは、IOオブジェクト(ソケットはIOのサブクラス)の読み込み側だけを閉じます。IO#close_writeメソッドは、IOオブジェクトの書き込み側だけを閉じます。Thread.exitメソッドは、カレントスレッドだけを終了します。

 この3つのメソッド呼び出しでTCP接続が切断され、POP3セッションを処理していたスレッドが終了します。

おわりに

 いかがでしたでしょうか。普段使っている「メールの受信」機能の裏側にはこんな仕掛けがあるのだということが分かってもらえたと思います。

 また、より一般的なネットワークプログラミングや、システムプログラミングにかかわる事項もたくさんありました。本稿で登場した事項を列挙してみましょう。

  • デーモンとinetd
  • POP3の役割と仕組み
  • Rubyでコマンドラインオプションを解析する方法
  • Rubyでデーモンになる方法
  • RubyでTCPサーバを作る方法
  • Rubyのetcライブラリを使ってアカウント情報にアクセスする方法

 いずれも応用範囲の広い、実践的な事項ばかりです。実際のプログラミングにも応用してみてください。

参考文献

  1. [STD0053] [RFC1939] 『Post Office Protocol - Version 3.』 J. Myers、M. Rose 著、1996年5月
  2. Using maildir format
  3. オブジェクト指向言語Rubyリファレンスマニュアル
  4. UNIXネットワークプログラミング第2版vol.1』 W. Richard Stevens、篠田陽一 訳、ピアソンエデュケーション、2004年4月
  5. 詳解TCP/IP Vol.1』 W. Richard Stevens 著、橘康雄 訳、井上尚司 監訳、ピアソンエデュケーション、2000年12月
  • LINEで送る
  • このエントリーをはてなブックマークに追加

著者プロフィール

  • 青木 峰郎(アオキ ミネロウ)

    ふつうの文系プログラマ。Ruby界隈で活動中。主著は『ふつうのLinuxプログラミング』(ソフトバンククリエイティブ、2005)、『Rubyソースコード完全解説』(インプレス、2002)。

あなたにオススメ

All contents copyright © 2005-2021 Shoeisha Co., Ltd. All rights reserved. ver.1.5