2014-09-25T22:03:59の更新内容

programming/netfx/tips/receive_mail_pop/index.wiki.txt

current previous
1,341 0,0
+
${smdncms:title,メールの受信(POP)}
+
//${smdncms:header_title,メールの受信(POP) (C#・VB)}
+
${smdncms:keywords,C#,VB.NET,TcpClient,POP,メール,受信,SSL}
+
${smdncms:tags,api/.net,lang/c#,lang/vb}
+
${smdncms:document_versions,codelang=cs,codelang=vb}
+

          
+
&msdn(netfx,type,System.Net.Sockets.TcpClient){System.Net.Sockets.TcpClientクラス};を使ってPOPサーバーからメールを受信するサンプル。
+

          
+
-関連するページ
+
--[[programming/netfx/tips/receive_mail_imap]]
+
--[[programming/netfx/tips/cram_md5]]
+
--[[programming/netfx/tips/quoted_printable]]
+
--[[programming/netfx/text_format_conversion]]
+
--[[programming/netfx/tips/echo_server]]
+
--[[works/libs/Smdn.Net.Pop3.Client]] (C#で実装したPOPクライアントライブラリ)
+
--[[works/libs/Smdn.Formats.Mime]] (C#で実装したメール解析ライブラリ)
+

          
+
#adunit
+

          
+
*POPクライアントの実装 [#implementation]
+
以下のコードはPOPサーバーに接続してメールを取得し、ファイルに保存するPOPクライアントのサンプル。 &msdn(netfx,type,System.Net.Security.SslStream){SslStream};を使ってSSLによる接続を行う。
+

          
+
.NET Framework 4.5、Mono 3.8で動作確認済み。 またPOPサーバーはGMailサーバー(gpop)およびDovecot v2.2.13での動作を確認している。 送信するコマンドとレスポンス解析上の要点などについては[[後述する>#pop_outline]]。
+

          
+
#code(cs){{
+
using System;
+
using System.IO;
+
using System.Net;
+
using System.Net.Security;
+
using System.Net.Sockets;
+
using System.Security.Cryptography.X509Certificates;
+
using System.Text;
+

          
+
class Sample {
+
  const string host = "pop.gmail.com"; // 接続先IMAPサーバーのホスト名
+
  const int port = 995; // 接続先のポート番号
+
  const string user = "user"; // ログインユーザー名
+
  const string pass = "pass"; // ログインパスワード
+
  const int mailNumber = 1; // 取得するメールの番号(1から始まるメールボックス内での通番)
+
  const bool deleteAfterRetrieve = false; // 取得後にメールを削除するか否か
+
  const string mailFilePath = "mail.eml"; // 取得したメールを保存するファイルのパス
+

          
+
  public static void Main()
+
  {
+
    // POPサーバーに接続
+
    using (var client = new PopClient(host, port)) {
+
      // 接続をSSLにアップグレード
+
      client.UpgradeToSsl();
+

          
+
      /*
+
       * initial greetingを受信
+
       */
+
      var response = client.Receive(false);
+

          
+
      if (!response.StartsWith("+OK"))
+
        // レスポンスがOK以外の場合は例外をスローする
+
        throw new ApplicationException("unexpected or error response (greeting banner)");
+

          
+
      /*
+
       * USERコマンド・PASSコマンドを送信してログイン
+
       */
+
      client.Send(string.Format("USER {0}\r\n", user));
+

          
+
      response = client.Receive(false);
+

          
+
      if (!response.StartsWith("+OK"))
+
        // コマンドのレスポンスがOK以外の場合は例外をスローする
+
        throw new ApplicationException("unexpected or error response (USER)");
+

          
+
      client.Send(string.Format("PASS {0}\r\n", pass));
+

          
+
      response = client.Receive(false);
+

          
+
      if (!response.StartsWith("+OK"))
+
        throw new ApplicationException("unexpected or error response (PASS)");
+

          
+
      /*
+
       * RETRコマンドを送信してメール本文を取得する
+
       */
+
      client.Send(string.Format("RETR {0}\r\n", mailNumber));
+

          
+
      response = client.Receive(true);
+

          
+
      if (!response.StartsWith("+OK"))
+
        throw new ApplicationException("unexpected or error response (RETR)");
+

          
+
      /*
+
       * 受信したRETRレスポンスからメール本文を抽出する
+
       */
+
      var indexOfBodyStart = response.IndexOf("\r\n") + 2;
+
      var body = response.Substring(indexOfBodyStart, response.Length - indexOfBodyStart - 2).Replace("\r\n.", "\r\n");
+

          
+
      // 抽出したメール本文をファイルに保存する
+
      File.WriteAllText(mailFilePath, body, Encoding.ASCII);
+

          
+
      // 本文取得後にメールを削除する場合
+
      if (deleteAfterRetrieve) {
+
        /*
+
         * DELEコマンドを送信して削除予定にする
+
         */
+
        client.Send(string.Format("DELE {0}\r\n", mailNumber));
+

          
+
        response = client.Receive(false);
+

          
+
        if (!response.StartsWith("+OK"))
+
          throw new ApplicationException("unexpected or error response (DELE)");
+
      }
+

          
+
      /*
+
       * QUITコマンドを送信してログアウトする
+
       */
+
      client.Send("QUIT\r\n");
+

          
+
      client.Receive(false); // レスポンスの検証は省略
+
    }
+
  }
+
}
+

          
+
/// <summary>TcpClientを継承してSSL接続とPOPコマンドの送受信に最適化したクライアント。</summary>
+
class PopClient : TcpClient {
+
  private Stream stream;
+
  private string host;
+
  private byte[] receiveBuffer = new byte[1024];
+

          
+
  public PopClient(string host, int port)
+
    : base(host, port)
+
  {
+
    this.host = host;
+
    this.stream = GetStream();
+
  }
+

          
+
  /// <summary>現在の接続をSSLにアップグレードする。</summary>
+
  public void UpgradeToSsl()
+
  {
+
    var sslStream = new SslStream(stream, false, ValidateRemoteCertificate);
+

          
+
    sslStream.AuthenticateAsClient(host);
+

          
+
    stream = sslStream;
+
  }
+

          
+
  /// <summary>サーバー証明書の検証を行う。</summary>
+
  private static bool ValidateRemoteCertificate(object sender,
+
                                                X509Certificate certificate,
+
                                                X509Chain chain,
+
                                                SslPolicyErrors sslPolicyErrors)
+
  {
+
    // 証明書の検証を省略したい場合は、常にtrueを返す
+
    //return true;
+

          
+
    if (sslPolicyErrors == SslPolicyErrors.None) {
+
      return true;
+
    }
+
    else {
+
      // エラーがあれば標準エラーに表示
+
      Console.Error.WriteLine(sslPolicyErrors);
+
      return false;
+
    }
+
  }
+

          
+
  /// <summary>POPコマンドを送信する。</summary>
+
  public void Send(string command)
+
  {
+
    var commandBytes = Encoding.ASCII.GetBytes(command);
+

          
+
    stream.Write(commandBytes, 0, commandBytes.Length);
+

          
+
    // 送信した内容をコンソールに表示
+
    Console.Write("C:\t{0}", command);
+
  }
+

          
+
  /// <summary>POPレスポンスを受信する。</summary>
+
  public string Receive(bool expectMultiline)
+
  {
+
    var sb = new StringBuilder();
+

          
+
    for (;;) {
+
      var len = stream.Read(receiveBuffer, 0, receiveBuffer.Length);
+

          
+
      sb.Append(Encoding.ASCII.GetString(receiveBuffer, 0, len));
+

          
+
      // 読み取り可能なデータがある場合はさらに受信を続ける
+
      if (0 < Available)
+
        continue;
+

          
+
      if (expectMultiline) {
+
        // レスポンスが複数行の場合は、CRLF.CRLFで終端されるまで受信した時点で受信を終了する
+
        if (5 <= sb.Length && sb.ToString(sb.Length - 5, 5) == "\r\n.\r\n")
+
          break;
+
      }
+
      else {
+
        // レスポンスが一行の場合は、CRLFで終端されるまで受信した時点で受信を終了する
+
        if (2 <= sb.Length && sb[sb.Length - 2] == '\r' && sb[sb.Length - 1] == '\n')
+
          break;
+
      }
+
    }
+

          
+
    var response = sb.ToString();
+

          
+
    // 受信した内容を整形してコンソールに表示
+
    Console.Write("S:\t{0}", sb.Replace("\r\n", "\r\n\t").ToString(0, sb.Length - 1));
+

          
+
    return response;
+
  }
+
}
+
}}
+

          
+
上記サンプルの制限事項や改良可能な点など。
+
+サーバーがUSER/PASSコマンドでのログインを制限している場合は、認証に失敗する (AUTHコマンドやAPOPを使用するように書き換える必要がある)
+
+レスポンスをバイト配列ではなく文字列に変換して解析するため、少なくとも受信するメールの二倍のサイズのメモリを消費する
+
+また、レスポンス全体をバッファに格納してから解析を行うため、巨大なファイルが添付されたメールを受信する場合などには注意を要する
+
+Encoding.ASCIIを使ってレスポンスを文字列に変換するため、``Content-Transfer-Encoding: 8bit``のメールでは文字化けが発生する可能性がある
+
+UpgradeToSsl()の呼び出しを行わなければSSLを使用しない通常のPOP接続を行うことができる
+
+STLSコマンドを送信してレスポンスを受信した後にUpgradeToSsl()を呼び出すようにすればSTLSに対応することができる
+

          
+

          
+

          
+
*POPプロトコル概説 [#pop_outline]
+
POPサーバーからメールを受信するために必要となる知識として、POPコマンドとレスポンスについて解説する。 以下はPOPサーバーに接続してメールを受信する処理を行うために送受信するコマンドとレスポンスの流れ。 (この例ではGMailに接続している)
+

          
+
``C:``の行はクライアントが送信するコマンド、``S:``の行はサーバーから返送されるレスポンスを表す。 ``C:``と``S:``自体はクライアントとサーバーを区別して表すためのラベルで、実際に送受信される内容ではない点に注意。 またPOPでは、コマンド・レスポンスとも行末は常に``CRLF``で終端される。
+

          
+
#code(,POPでメールを受信する際の送受信内容){{
+
S:	+OK Gpop ready for requests from xxx.xxx.xxx.xxx f82mb55742923ioe
+
C:	USER user
+
S:	+OK send PASS
+
C:	PASS pass
+
S:	+OK Welcome.
+
C:	RETR 1
+
S:	+OK message follows
+
	Message-ID: <5422CEEF.2080103@example.com>
+
	Date: Wed, 24 Sep 2014 23:02:23 +0900
+
	From: me@example.com
+
	User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:31.0) Gecko/20100101 Thunderbird/31.1.1
+
	MIME-Version: 1.0
+
	To: you@example.com
+
	Subject: =?ISO-2022-JP?B?GyRCJUYlOSVIGyhC?=
+
	Content-Type: text/plain; charset=iso-2022-jp
+
	Content-Transfer-Encoding: base64
+
	
+
	GyRCJUYlOSVIJWEhPCVrGyhC
+
	.
+
C:	QUIT
+
S:	+OK Farewell.
+
}}
+

          
+
以下で上記の送受信内容について1ステップずつ解説する。 メールの受信を行う際の要点のみを解説するので、POPの仕様については適宜&urn2url(urn:ietf:rfc:1939);や文中のリンクを参照のこと。
+

          
+
**接続
+
#code(,initial greeting){{
+
S:	+OK Gpop ready for requests from xxx.xxx.xxx.xxx f82mb55742923ioe
+
}}
+

          
+
接続が確立した後(SSL接続の場合はセッションが確立した後)、サーバー側からグリーティングバナーが送信されてくる。 ``+OK``で始まる場合は成功であり、以降POPコマンドを受信できる状態であることを表す。 ``OK``以降の文字列はサーバーによって異なる。
+

          
+

          
+
**ログイン (USERコマンド・PASSコマンド)
+
#code(,USER・PASSコマンドとレスポンス){{
+
C:	USER user
+
S:	+OK send PASS
+
C:	PASS pass
+
S:	+OK Welcome.
+
}}
+

          
+
``USER``コマンドおよび``PASS``コマンドを使い、ユーザー名とパスワードを指定してPOPサーバーにログインする。 ここではユーザー名に``user``、パスワードに``pass``を指定している。
+

          
+
コマンド``USER``の結果は、サーバーから返送されるレスポンスにある``+OK``となる。 認証に失敗したなどコマンドが失敗した場合は``-ERR``となる。 ``+OK``以降の文字列はコマンドの結果を表す一種のコメントでありプロトコル上意味のある文字列では無く、その内容はサーバーによって異なる。
+

          
+
``USER``コマンドが成功したら、次に``PASS``コマンドでパスワードを送信する。 コマンドの成否は``USER``コマンドと同様にレスポンス先頭の``+OK``または``-ERR``で表される。
+

          
+
``PASS``コマンドは平文での認証を行う。 ``DIGEST-MD5``や``CRAM-MD5``などを使って認証したい場合は``AUTH``コマンドを使用する。 (&urn2url(urn:ietf:rfc:5034);、[[programming/netfx/tips/cram_md5]])
+

          
+

          
+
**メールの取得 (RETRコマンド)
+
#code(,RETRコマンドとレスポンス){{
+
C:	RETR 1
+
S:	+OK message follows
+
	Message-ID: <5422CEEF.2080103@example.com>
+
	Date: Wed, 24 Sep 2014 23:02:23 +0900
+
	From: me@example.com
+
	User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:31.0) Gecko/20100101 Thunderbird/31.1.1
+
	MIME-Version: 1.0
+
	To: you@example.com
+
	Subject: =?ISO-2022-JP?B?GyRCJUYlOSVIGyhC?=
+
	Content-Type: text/plain; charset=iso-2022-jp
+
	Content-Transfer-Encoding: base64
+
	
+
	GyRCJUYlOSVIJWEhPCVrGyhC
+
	.
+
}}
+

          
+
メールの内容を取得するために``RETR``コマンドを送信する。 コマンドの引数に取得したいメールの番号(メールボックス内での通番)を指定する。 ここではメール番号に``1``を指定している。
+

          
+
コマンド``RETR``の結果は、サーバーから返送されるレスポンス最初の行の``+OK``となる。 指定された番号のメールがない場合などには``+OK``ではなく``-ERR``が返送される。
+

          
+
レスポンスには``RETR``コマンドの結果を表す一行目(``+OKの行``)に続けて、取得したメールの内容が格納される。 ``RETR``コマンドの結果は他のコマンドとは異なり複数行のレスポンスで返される。 ``.``(ピリオド)のみの行(言い換えると``CRLF.CRLF``の5文字)がレスポンスの終端を表す。
+

          
+
``RETR``コマンドのレスポンスでは、取得したメールのうち``.``で始まる行は行頭に``.``が付加された状態で返される。 これはレスポンスの終端を表す``CRLF.CRLF``と競合するのを避けるためのもので、このようにして行頭に付加された``.``はクライアント側で取り除く必要がある。
+

          
+
#code(,行頭にピリオドを含むメール){{
+
MIME-Version: 1.0
+
Content-Type: text/plain
+
Content-Transfer-Encoding: 7bit
+

          
+
line 1
+
.line 2
+
.
+
}}
+

          
+
#code(,上記メールをRETRコマンドで取得した場合のレスポンス){{
+
C:	RETR 1
+
S:	+OK message follows
+
	MIME-Version: 1.0
+
	Content-Type: text/plain
+
	Content-Transfer-Encoding: 7bit
+
	
+
	line 1
+
	..line 2
+
	..
+
	.
+
}}
+

          
+
メール番号はメールボックスに格納された順で1から始まる連続した番号が割り当てられる。 メールの削除を行った場合にはその番号が割り当て直されて変化する点に注意。
+

          
+
``RETR``コマンドでメールを取得するだけではメールは削除されない。 メールを削除する場合は取得とは別に``DELE``コマンドを送信する必要がある。 (&urn2url(urn:ietf:rfc:1939,#5,short);)
+

          
+
``STAT``コマンドを使用すると、メールボックスに格納されているメールの総数と、総オクテット数を取得することができる。 メールボックスにあるすべてのメールを受信したい場合には、``STAT``コマンドでメールの総数を把握したのち、1件ずつ``RETR``コマンドで取得すればよい。 (&urn2url(urn:ietf:rfc:1939,#5,short);)
+

          
+
``UIDL``コマンドを使用するとメールの''ユニークID''を取得することができる。 ユニークIDはメールボックスに格納される時に割り当てられた以降は変化することがない。 ``UIDL``コマンドはサーバーによってはサポートされていない場合もある。 (&urn2url(urn:ietf:rfc:1939,#7,short);)
+

          
+

          
+
**ログアウト (QUITコマンド)
+
#code(,QUITコマンドとレスポンス){{
+
C:	QUIT
+
S:	+OK Farewell.
+
}}
+

          
+
メールの取得のみを行う場合はこのコマンドを送信せず即座に切断することもできる。 ``DELE``コマンドでメールの削除を行う場合は、``QUIT``コマンドを送信しない限り削除されない。
+

          
+

          
+