POP3クライアント

ネットの掲示板や会議室で時々、POP3クライアントを作りたいというのを見かけます。BCLではSMTPはあるのに何故かPOP3は無いんですよね。で、考えてみたら、私も作り方知らなかったり。(^^; 折角なので電子メールプロトコルの本ひっくり返しながら、てきとーに実装してみました。ちょっと長いですが、以下、PO3クライアントのサンプル。

using System;
using System.Collections;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Text.RegularExpressions;
using System.Diagnostics;
using System.Security.Cryptography;

public class Pop3Mail {
  // 状態
  const string OK = "+OK";
  const string ERR = "-ERR";
  // コマンド
  const string USER = "USER {0}";
  const string PASS = "PASS {0}";
  const string APOP = "APOP {0} {1}";
  const string QUIT = "QUIT";
  const string STAT = "STAT";
  const string NOOP = "NOOP";
  const string LIST = "LIST";
  const string LIST_N = "LIST {0}";
  const string RETR = "RETR {0}";
  const string DELE = "DELE {0}";
  const string RSET = "RSET";

  const int DEFAULT_PORT = 110;

  private string server;
  private int port;
  private string msg;
  private TcpClient tcp;
  // デフォルトはJISとする
  private Encoding enc = Encoding.GetEncoding(50220);

  public Pop3Mail(string server) : this(server, DEFAULT_PORT) {}

  public Pop3Mail(string server, int port) {
    Server = server;
    Port = port;
  }

  public string Server {
    get { return server; }
    set { server = value; }
  }

  public int Port {
    get { return port; }
    set { port = value; }
  }

  public string ErrorMessge {
    get { return msg; }
  }

  public Encoding Encode {
    get { return enc; }
    set { enc = value; }
  }

  public bool Connect(string user, string pass) {
    // デフォルトはAPOPを使わない
    return Connect(user, pass, false);
  }

  public bool Connect(string user, string pass, bool apop) {
    try {
      tcp = new TcpClient(Server, Port);
    }
    catch (Exception e) {
      msg = e.Message;
      return false;
    }
    msg = ReceiveLine(tcp.GetStream());
    Trace.WriteLine(msg);

    string challenge = null;

    if (apop) {
      // APOP
      Match m = Regex.Match(msg, @"(<[^>]+>)");
      if (!m.Success) {
        msg = String.Format("{0}はAPOPに対応していません。", Server);
        return false;
      }
      challenge = m.Groups[1].Value;
    }
      
    return Login(user, pass, challenge);
  }

  // 認証
  private bool Login(string user, string pass, string challenge) {
    string cmd;
    Stream s = tcp.GetStream();

    if (challenge == null) {
      // ユーザ・パスワード
      cmd = String.Format(USER, user);
      Trace.WriteLine(cmd);
      Send(s, cmd);
      // USER応答
      msg = ReceiveLine(s);
      Trace.WriteLine(msg);
      // PASS
      cmd = String.Format(PASS, pass);
    }
    else {
      // チャレンジ・アンド・レスポンス
      MD5 md5 = MD5.Create();
      md5.ComputeHash(Encoding.ASCII.GetBytes(challenge+pass));
      StringBuilder digest = new StringBuilder();
      foreach (byte b in md5.Hash)
        digest.AppendFormat("{0:x2}", b);
      cmd = String.Format(APOP, user, digest.ToString());
    }

    // 認証
    Trace.WriteLine(cmd);
    Send(s, cmd);

    // 認証応答
    msg = ReceiveLine(s);
    Trace.WriteLine(msg);

    if (msg.StartsWith(ERR)) 
      return false;

    return true;
  }

  public bool Quit() {
    Stream s = tcp.GetStream();
    // Quit
    Send(s, QUIT);
    Trace.WriteLine(QUIT);
    msg = ReceiveLine(s);
    Trace.WriteLine(msg);
    if (msg.StartsWith(ERR))
      return false;
    tcp.Close();
    tcp = null;
    return true;
  }

  public string Stat() {
    Stream s = tcp.GetStream();
    // Stat
    Send(s, STAT);
    Trace.WriteLine(STAT);
    msg = ReceiveLine(s);
    Trace.WriteLine(msg);
    return msg;
  }

  public bool Noop() {
    Stream s = tcp.GetStream();
    // Noop
    Send(s, NOOP);
    Trace.WriteLine(NOOP);
    msg = ReceiveLine(s);
    Trace.WriteLine(msg);
    return msg.StartsWith(OK);
  }

  public string List() {
    Stream s = tcp.GetStream();
    Trace.WriteLine(LIST);
    Send(s, LIST);

    return ReceiveLines(s);
  }

  public string List(int no) {
    Stream s = tcp.GetStream();
    string cmd = String.Format(LIST_N, no);
    Trace.WriteLine(cmd);
    Send(s, cmd);

    msg = ReceiveLine(s);
    if (msg.StartsWith(ERR)) {
      return null;
    }
    // 番号 バイト数を戻す
    return msg.Substring(OK.Length + 1);
  }

  public string Retr(int no) {
    Stream s = tcp.GetStream();
    string cmd = String.Format(RETR, no);
    Trace.WriteLine(cmd);
    Send(s, cmd);
    return ReceiveMessage(s, Encode);
  }

  public bool Dele(int no) {
    Stream s = tcp.GetStream();
    string cmd = String.Format(DELE, no);
    Trace.WriteLine(cmd);
    Send(s, cmd);
    msg = ReceiveLine(s);
    Trace.WriteLine(msg);
    return msg.StartsWith(OK);
  }

  public bool Rset() {
    Stream s = tcp.GetStream();
    Trace.WriteLine(RSET);
    Send(s, RSET);
    msg = ReceiveLine(s);
    Trace.WriteLine(msg);
    return msg.StartsWith(OK);
  }

  private void Send(Stream s, string cmd) {
    byte data = Encoding.ASCII.GetBytes(cmd + "\r\n");
    s.Write(data, 0, data.Length);
  }

  private string ReceiveLine(Stream s) {
    return new StreamReader(s).ReadLine();
  }

  private string ReceiveLines(Stream s) {
    StreamReader sr = new StreamReader(s);
    StringBuilder sb = new StringBuilder();
    string line;
    line = sr.ReadLine();
    if (line == null)
      return null;
    // 最初の応答を保存
    msg = line;
    do {
      Trace.WriteLine(line);
      // .のみの行が来たら終了
      if (Regex.IsMatch(line, @"^\.$"))
        break;
      // "番号 バイト数"の並び
      if (Regex.IsMatch(line, @"^\d+\s\d+"))
        sb.Append(line + ",");
    } while ( (line = sr.ReadLine()) != null);

    line = sb.ToString();
    if (line.EndsWith(",")) 
      return line.TrimEnd(',').Split(',');

    return null;
  }

  private string ReceiveMessage(Stream s, Encoding enc) {
    StreamReader sr = new StreamReader(s, enc);
    StringBuilder sb = new StringBuilder();
    string line;
    line = sr.ReadLine();
    if (line == null || Regex.IsMatch(line, @"^\.$"))
      return null;
    // 最初の応答を保存
    msg = line;
    while ( (line = sr.ReadLine()) != null) {
      Trace.WriteLine(line);
      // .のみの行が来たら終了
      if (Regex.IsMatch(line, @"^\.$"))
        break;
      if (line.StartsWith("..")) {
        // 先頭が..の場合、.を1つ削除する
        line = line.Substring(1);
      }
      sb.Append(line + "\n");
    }

    line = sb.ToString();
    if (line.EndsWith("\n")) 
      return line.TrimEnd('\n').Split('\n');

    return null;
  }
}

// メール取得サンプル
class Program {
  static void Main(string args) {
    if (args.Length < 4) {
      Console.WriteLine("usage: program.exe server user pass no");
      return;
    }
    string server = args[0];  // POPサーバー
    string user = args[1];    // ユーザ
    string pass = args[2];    // パスワード
    int no = int.Parse(args[3]);// メッセージ番号

    Pop3Mail pop = new Pop3Mail(server);
    // POPサーバーに接続
    pop.Connect(user, pass, true);
    // メッセージを取得
    string[] list = pop.Retr(no);
    // 表示
    foreach (string s in list)
      Console.WriteLine(s);
    // セッションの終了
    pop.Quit();
  }
}

途中で力尽きたのでMIMEには未対応です。一応動いているみたいですが、きっと怪しいです。(^^;
勉強用に作ったので、そのまま使うのはやめておいた方が良いです・・・