System.Net.Sockets.TcpClientクラスを使ってPOPサーバーからメールを受信するサンプル。 .NET FrameworkにはPOPクライアントにあたるクラスが用意されていないため、独自に実装する必要がある。

POPクライアントの実装

以下のコードはPOPサーバーに接続してメールを取得し、ファイルに保存するPOPクライアントのサンプル。 SslStreamを使ってSSLによる接続を行う。

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

POPサーバーに接続してメールを取得するPOPクライアント
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"; // 取得したメールを保存するファイルのパス

  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;
  }
}
POPサーバーに接続してメールを取得するPOPクライアント
Imports System
Imports System.IO
Imports System.Net
Imports System.Net.Security
Imports System.Net.Sockets
Imports System.Security.Cryptography.X509Certificates
Imports System.Text

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

  Shared Sub Main()
    ' POPサーバーに接続
    Using client As New PopClient(host, port)
      ' 接続をSSLにアップグレード
      client.UpgradeToSsl()

      '
      ' initial greetingを受信
      '
      Dim response As String = client.Receive(False)

      If Not response.StartsWith("+OK") Then
        ' レスポンスがOK以外の場合は例外をスローする
        Throw New ApplicationException("unexpected or error response (greeting banner)")
      End If

      '
      ' USERコマンド・PASSコマンドを送信してログイン
      '
      client.Send("USER " + user + vbCrLf)

      response = client.Receive(False)

      If Not response.StartsWith("+OK") Then
        ' コマンドのレスポンスがOK以外の場合は例外をスローする
        Throw New ApplicationException("unexpected or error response (USER)")
      End If

      client.Send("PASS " + pass + vbCrLf)

      response = client.Receive(False)

      If Not response.StartsWith("+OK") Then
        Throw New ApplicationException("unexpected or error response (PASS)")
      End If

      '
      ' RETRコマンドを送信してメール本文を取得する
      '
      client.Send("RETR " + mailNumber.ToString() + vbCrLf)

      response = client.Receive(True)

      If Not response.StartsWith("+OK") Then
        Throw New ApplicationException("unexpected or error response (RETR)")
      End If

      '
      ' 受信したRETRレスポンスからメール本文を抽出する
      '
      Dim indexOfBodyStart As Integer = response.IndexOf(vbCrLf) + 2
      Dim body As String = response.Substring(indexOfBodyStart, response.Length - indexOfBodyStart - 2).Replace(vbCrLf + ".", vbCrLf)

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

      ' 本文取得後にメールを削除する場合
      If deleteAfterRetrieve Then
        '
        ' DELEコマンドを送信して削除予定にする
        '
        client.Send("DELE " + mailNumber.ToString() + vbCrLf)

        response = client.Receive(False)

        If Not response.StartsWith("+OK") Then
          Throw New ApplicationException("unexpected or error response (DELE)")
        End If
      End If

      '
      ' QUITコマンドを送信してログアウトする
      '
      client.Send("QUIT" + vbCrLf)

      client.Receive(False) ' レスポンスの検証は省略
    End Using
  End Sub
End Class

''' <summary>TcpClientを継承してSSL接続とPOPコマンドの送受信に最適化したクライアント。</summary>
Class PopClient
  Inherits TcpClient

  Private stream As Stream
  Private host As String
  Private receiveBuffer(1023) As Byte

  Public Sub New(ByVal host As String, ByVal port As Integer)
    MyBase.New(host, port)

    Me.host = host
    Me.stream = GetStream()
  End Sub

  ''' <summary>現在の接続をSSLにアップグレードする。</summary>
  Public Sub UpgradeToSsl()
    Dim sslStream As New SslStream(stream, False, AddressOf ValidateRemoteCertificate)

    sslStream.AuthenticateAsClient(host)

    stream = sslStream
  End Sub

  ''' <summary>サーバー証明書の検証を行う。</summary>
  Private Shared Function ValidateRemoteCertificate(ByVal sender As Object, _
                                                    ByVal certificate As X509Certificate, _
                                                    ByVal chain As X509Chain, _
                                                    ByVal sslPolicyErrors As SslPolicyErrors) As Boolean
    ' 証明書の検証を省略したい場合は、常にTrueを返す
    'Return True

    If sslPolicyErrors = SslPolicyErrors.None Then
      Return True
    Else
      ' エラーがあれば標準エラーに表示
      Console.Error.WriteLine(sslPolicyErrors)
      Return False
    End If
  End Function

  ''' <summary>POPコマンドを送信する。</summary>
  Public Sub Send(ByVal command As String)
    Dim commandBytes() As Byte = Encoding.ASCII.GetBytes(command)

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

    ' 送信した内容をコンソールに表示
    Console.Write("C:" + vbTab + command)
  End Sub

  ''' <summary>POPレスポンスを受信する。</summary>
  Public Function Receive(ByVal expectMultiline As Boolean) As String
    Dim sb As New StringBuilder()

    Do
      Dim len As Integer = stream.Read(receiveBuffer, 0, receiveBuffer.Length)

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

      ' 読み取り可能なデータがある場合はさらに受信を続ける
      If (0 < Available) Then Continue Do

      If expectMultiline Then
        ' レスポンスが複数行の場合は、CRLF.CRLFで終端されるまで受信した時点で受信を終了する
        If 5 <= sb.Length AndAlso sb.ToString(sb.Length - 5, 5) = vbCrLf + "." + vbCrLf Then Exit Do
      Else
        ' レスポンスが一行の場合は、CRLFで終端されるまで受信した時点で受信を終了する
        If 2 <= sb.Length AndAlso sb.ToString(sb.Length - 2, 2) = vbCrLf Then Exit Do
      End If
    Loop

    Dim response As String = sb.ToString()

    ' 受信した内容を整形してコンソールに表示
    Console.Write("S:" + vbTab + sb.Replace(vbCrLf, vbCrLf + vbTab).ToString(0, sb.Length - 1))

    Return response
  End Function
End Class

上記サンプルの制限事項や改良可能な点など。

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

POPプロトコル概説

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

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

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の仕様については適宜RFC 1939 - Post Office Protocol - Version 3や文中のリンクを参照のこと。

接続

initial greeting
S:	+OK Gpop ready for requests from xxx.xxx.xxx.xxx f82mb55742923ioe

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

ログイン (USERコマンド・PASSコマンド)

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-MD5CRAM-MD5などを使って認証したい場合はAUTHコマンドを使用する。 (RFC 5034 - The Post Office Protocol (POP3) Simple Authentication and Security Layer (SASL) Authentication MechanismCRAM-MD5による認証)

メールの取得 (RETRコマンド)

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と競合するのを避けるためのもので、このようにして行頭に付加された.はクライアント側で取り除く必要がある。

行頭にピリオドを含むメール
MIME-Version: 1.0
Content-Type: text/plain
Content-Transfer-Encoding: 7bit

line 1
.line 2
.
上記メールを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コマンドを送信する必要がある。 (RFC 1939 5)

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

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

ログアウト (QUITコマンド)

QUITコマンドとレスポンス
C:	QUIT
S:	+OK Farewell.

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