久しぶりの更新。
この間何をしていたかというと…。

指令Z

会社の上司から指令が出た。
「土日もメールチェックしろ。重要なお知らせが来るかもしれないだろ」
なん、だと…!?
そんな緊急性のあるお知らせをメールで流すなんてどうかしている*1
さらにそれを人間がプルしなければならないなんてどうかしている。
大体なんのために僕は会社から携帯を持たされているのだ…。

「仕方がない」は発明の母?

仕方がないのでメールチェッククライアントを作成することにする。
せっかくなので会社のPCでも動くJavaで作成することにする。
会社のメールはWEBメールで、普段の勤務中も人間のほうからプルしに行かなければならず、ちょっと放置していると「メールを見ろ」と周囲から罵倒される*2
いや、こまめにプルしにいく能力がないのは僕の責任なのだけれど、そこはIT企業wらしくITの力でプッシュ方式に変換して能力不足を補いたい。

というわけでソースコード

途中までしかできていないのだけれど。
定期的にメールを取得してコンソールに流すところまではできた。
最終的にはGUIで通知できるようにしたい。

package com.heppokoact;

import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

import com.heppokoact.mailcheck.MailHandler;
import com.heppokoact.mailcheck.MailInfo;
import com.heppokoact.mailcheck.impl.ISEMailCheck;

/**
 * メールチェックアプリケーションの起動クラス。
 * 
 * @author heppokoact
 * 
 */
public class Main {

  /**
   * メールチェックアプリケーションの起動メソッド。
   * 
   * @param args
   *            起動引数。不要。
   */
  public static void main(String[] args) {

    final ScheduledExecutorService service = Executors.newScheduledThreadPool(1);
    service.scheduleWithFixedDelay(new MyCompanyMailCheck(new MailHandler() {
      @Override
      public void handle(List<MailInfo> mails) {
        for (MailInfo mail : mails) {
          System.out.println(String.format("ID: %s, TITLE: %s, FROM: %s", mail.getId(), mail.getTitle(),
              mail.getFrom()));
        }
      }
    }), 20000L, 20000L, TimeUnit.MILLISECONDS);

    service.schedule(new Runnable() {
      @Override
      public void run() {
        service.shutdown();
      }
    }, 60000L, TimeUnit.MILLISECONDS);
  }
}
package com.heppokoact.mailcheck;

/**
 * メールチェックのタスクを表します。
 * メールチェックは非同期多重で繰り返し実行するため、Runnableを実装しています。
 * 
 * @author yoshida
 *
 */
public interface MailCheck extends Runnable {

}
package com.heppokoact.mailcheck;

import java.util.List;

/**
 * チェックしたメールを処理するオブジェクトです。
 * 
 * @author heppokoact
 *
 */
public interface MailHandler {

	/**
	 * チェックしたメールに何らかの処理を行います。
	 * @param mails メール情報
	 */
	public void handle(List<MailInfo> mails);

}
package com.heppokoact.mailcheck;

/**
 * メールから取得した情報を表します。
 * 
 * @author heppokoact
 *
 */
public class MailInfo {

  /**
   * デフォルトコンストラクタ。
   */
  public MailInfo() {
  }

  /**
   * コンストラクタ。
   * @param id メールを一意に識別するためのID 
   * @param title メールのタイトル 
   * @param from メールの差出人 
   */
  public MailInfo(String id, String title, String from) {
    this.id = id;
    this.title = title;
    this.from = from;
  }

  /** メールを一意に識別するためのID */
  private String id;
  /** メールのタイトル */
  private String title;
  /** メールの差出人 */
  private String from;

  /**
   * メールを一意に識別するためのIDを取得します。
   * @return メールを一意に識別するためのID
   */
  public String getId() {
    return id;
  }

  /**
   * メールを一意に識別するためのIDを設定します。
   * @param id メールを一意に識別するためのID
   */
  public void setId(String id) {
    this.id = id;
  }

  /**
   * メールのタイトルを取得します。
   * @return メールのタイトル
   */
  public String getTitle() {
    return title;
  }

  /**
   * メールのタイトルを設定します。
   * @param title メールのタイトル
   */
  public void setTitle(String title) {
    this.title = title;
  }

  /**
   * メールの差出人を取得します。
   * @return メールの差出人
   */
  public String getFrom() {
    return from;
  }

  /**
   * メールの差出人を設定します。
   * @param from メールの差出人
   */
  public void setFrom(String from) {
    this.from = from;
  }

}
package com.heppokoact.mailcheck.impl;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;

import com.heppokoact.mailcheck.MailCheck;
import com.heppokoact.mailcheck.MailHandler;
import com.heppokoact.mailcheck.MailInfo;

/**
 * WEBメールの新着チェックをするクラスの抽象基底クラスです。
 * 
 * @author heppokoact
 *
 */
public abstract class AbstractWebMailCheck implements MailCheck {

  /** チェックした新着メールに対し、何らかの処理を行うオブジェクト */
  private MailHandler handler;
  /** 前回チェック時の最新MailInfoオブジェクト */
  private MailInfo newest;

  /**
   * コンストラクタ。
   * @param handler チェックした新着メールに対し、何らかの処理を行うオブジェクト 
   */
  public AbstractWebMailCheck(MailHandler handler) {
    this.handler = handler;

    // WEBメールからメールを取得し、最新のメールを保持します。
    newest = selectNewest(getWebMails());
  }

  /**
   * 引数なしコンストラクタ。
   */
  protected AbstractWebMailCheck() {
  }

  /**
   * 新着メールチェックを行い、新着メールがあれば何らかの処理を行います。
   */
  @Override
  public void run() {
    List<MailInfo> newMails = getNewMails();
    newest = selectNewest(newMails);
    handler.handle(newMails);
  }

  /**
   * 引数のMailInfoオブジェクトおよび前回チェック時の最新MailInfoオブジェクトの中から
   * 最新のMailInfoオブジェクトを選択して返します。
   * 最新のMailInfoオブジェクトが複数ある場合、どちらかが返されますが、
   * どちらが返されるかは保証されていません。
   * 引数が空またはnullの場合、既存の最新MailInfoオブジェクトを返します。
   * @param mails MailInfoオブジェクトのリスト
   * @return 最新のMailInfoオブジェクト
   */
  protected MailInfo selectNewest(List<MailInfo> mails) {
    if (mails == null || mails.isEmpty()) {
      return newest;
    }

    List<MailInfo> allMails = null;
    if (newest == null) {
      allMails = mails;
    } else {
      allMails = new ArrayList<MailInfo>(mails);
      allMails.add(newest);
    }
    return Collections.max(allMails, mailComparator);
  }

  /**
   * 新着メールを取得します。
   * @return 新着メールのリスト
   */
  protected List<MailInfo> getNewMails() {
    List<MailInfo> newMails = new ArrayList<MailInfo>();
    for (MailInfo mail : getWebMails()) {
      if (isNewerMail(mail)) {
        newMails.add(mail);
      }
    }
    return newMails;
  }

  /**
   * WEBメールからメールを取得します。
   * このメソッドが返すメールは必ずしも最新のメールのリストでなくても構いません。
   * @return WEBメールから取得したメールのリスト
   */
  protected abstract List<MailInfo> getWebMails();

  /**
   * 引数のMailInfoオブジェクトが前回チェック時の最新MailInfoオブジェクトより新しいかどうかを調べます。
   * idを数値に変換し、大きいほうが新しいものとみなします。
   * 前回チェック時の最新MailInfoオブジェクトが存在しない場合は常にtrueを返します。
   * @param other 比較対象のMailInfoオブジェクト
   * @return このMailInfoオブジェクトが引数のMailInfoオブジェクトより新しければtrue
   */
  public boolean isNewerMail(MailInfo mail) {
    if (newest == null) {
      return true;
    }
    return mailComparator.compare(mail, newest) > 0;
  }

  /** 
   * MailInfoオブジェクトの大小を比較します。
   * MailInfoオブジェクトの大小はIDを数値に変換し、その大小で決定します。
   */
  protected Comparator<MailInfo> mailComparator = new Comparator<MailInfo>() {

    @Override
    public int compare(MailInfo o1, MailInfo o2) {
      int o1Id = Integer.parseInt(o1.getId());
      int o2Id = Integer.parseInt(o2.getId());
      return o1Id - o2Id;
    }
  };

  /**
   * チェックした新着メールに対し、何らかの処理を行うオブジェクトを取得します。
   * @return チェックした新着メールに対し、何らかの処理を行うオブジェクト
   */
  protected MailHandler getHandler() {
    return handler;
  }

  /**
   * チェックした新着メールに対し、何らかの処理を行うオブジェクトを設定します。
   * @param handler チェックした新着メールに対し、何らかの処理を行うオブジェクト
   */
  protected void setHandler(MailHandler handler) {
    this.handler = handler;
  }

  /**
   * 前回チェック時の最新MailInfoオブジェクトを取得します。
   * @return 前回チェック時の最新MailInfoオブジェクト
   */
  protected MailInfo getNewest() {
    return newest;
  }

  /**
   * 前回チェック時の最新MailInfoオブジェクトを設定します。
   * @param newest 前回チェック時の最新MailInfoオブジェクト
   */
  protected void setNewest(MailInfo newest) {
    this.newest = newest;
  }

}
package com.heppokoact.mailcheck.impl;

import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.List;

import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;

import org.apache.commons.io.IOUtils;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.HttpClient;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.message.BasicNameValuePair;
import org.cyberneko.html.parsers.DOMParser;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.SAXNotRecognizedException;
import org.xml.sax.SAXNotSupportedException;

import com.heppokoact.mailcheck.MailCheckRuntimeException;
import com.heppokoact.mailcheck.MailHandler;
import com.heppokoact.mailcheck.MailInfo;

/**
 * 会社のWEBメールをチェックするクラスです。
 * 
 * @author heppokoact
 *
 */c15mc3f5.securesites.net/ControlPanel/mail/index.xsl
public class MyCompanyMailCheck extends AbstractWebMailCheck {

  /** 会社メールのログインページのURL */
  private static final String LOGIN_URL = "http://heppokoact.com/login";
  /** 会社メールの受信BOXの1ページ目のURL */
  private static final String RECIEVE_BOX_URL = "http://heppokoact.com/receiveBox";

  /**
   * コンストラクタ。
   * @param handler チェックした新着メールに対し、何らかの処理を行うオブジェクト 
   */
  public MyCompanyMailCheck(MailHandler handler) {
    super(handler);
  }

  /**
   * WEBメールからメールを取得します。
   * このメソッドが返すメールは必ずしも最新のメールのリストでなくても構いません。
   * @return WEBメールから取得したメールのリスト
   */
  @Override
  protected List<MailInfo> getWebMails() {
    Document document = getMailDocument();
    return extractWebMails(document);
  }

  /**
   * WEBメールのHTMLを解析したDocumentオブジェクトからメール情報を抽出します。
   * @param document WEBメールのHTMLを解析したDocumentオブジェクト 
   */
  protected List<MailInfo> extractWebMails(Document document) {
    List<MailInfo> mails = new ArrayList<MailInfo>();

    try {
      XPath xpath = XPathFactory.newInstance().newXPath();
      NodeList list = (NodeList) xpath.evaluate(
          "//TR[@class='unreadroweven' or @class='unreadrowodd' or @class='roweven' or @class='rowodd']",
          document, XPathConstants.NODESET);
      for (int i = 0; i < list.getLength(); i++) {
        Node tr = list.item(i);
        String id = xpath.evaluate("TD/INPUT[@type='checkbox']/@value", tr).trim();
        String title = xpath.evaluate("descendant::TD[4]/A/text()", tr).trim();
        String from = xpath.evaluate("descendant::TD[3]/LABEL/text()", tr).trim();
        mails.add(new MailInfo(id, title, from));
      }

    } catch (XPathExpressionException e) {
      throw new MailCheckRuntimeException("XPATHの構文が間違っています。", e);
    }

    return mails;
  }

  /**
   * WEBメールのHTMLを解析した結果のDocumentオブジェクトを取得します。
   * @return WEBメールのHTMLを解析した結果のDocumentオブジェクト
   */
  protected Document getMailDocument() {
    InputStream in = null;
    DefaultHttpClient client = null;
    try {
      client = new DefaultHttpClient();
      in = getMaiPageHtml(client);
      DOMParser parser = new DOMParser();
      parser.setFeature("http://xml.org/sax/features/namespaces", false);
      parser.parse(new InputSource(in));
      return parser.getDocument();

    } catch (SAXNotRecognizedException e) {
      throw new MailCheckRuntimeException("WEBメールのHTMLが解析できません。", e);
    } catch (SAXNotSupportedException e) {
      throw new MailCheckRuntimeException("WEBメールのHTMLが解析できません。", e);
    } catch (SAXException e) {
      throw new MailCheckRuntimeException("WEBメールのHTMLが解析できません。", e);
    } catch (IOException e) {
      throw new MailCheckRuntimeException("WEBメールのHTMLが読み込めません。", e);
    } finally {
      IOUtils.closeQuietly(in);
      if (client != null) {
        client.getConnectionManager().shutdown();
      }
    }
  }

  /**
   * WEBメールのメール受信BOXの1ページ目ののHTMLを取得します。
   * @param client HTTPクライアント
   * @return WEBメールのメール受信BOXの1ページ目ののHTMLのInputStream
   */
  protected InputStream getMaiPageHtml(DefaultHttpClient client) {
    InputStream in = null;
    try {
      client = new DefaultHttpClient();
      login(client);
      in = getMailStream(client);

    } catch (UnsupportedEncodingException e) {
      throw new MailCheckRuntimeException("UTF-8がサポートされていません。", e);
    } catch (ClientProtocolException e) {
      throw new MailCheckRuntimeException("HTTP通信でエラーが発生しました。", e);
    } catch (IOException e) {
      throw new MailCheckRuntimeException("メールチェックに使用するコネクションでエラーが発生しました。", e);
    }

    return in;
  }

  /**
   * WEBメールの1ページ目のHTMLを取得します。
   * @param client ログインに使用するHTTPクライアント
   * @return WEBメールの1ページ目のHTMLを取得するためのストリーム
   * @throws ClientProtocolException HTTP通信でエラーが発生した場合
   * @throws IOException メールチェックに使用するコネクションでエラーが発生した場合
   */
  protected InputStream getMailStream(HttpClient client) throws ClientProtocolException, IOException {
    HttpGet get = new HttpGet(RECIEVE_BOX_URL);
    HttpResponse res = client.execute(get);
    HttpEntity entity = res.getEntity();
    return new BufferedInputStream(entity.getContent());
  }

  /**
   * メールWEBアプリケーションにログインします。
   * @param client ログインに使用するHTTPクライアント
   * @throws UnsupportedEncodingException UTF−8がサポートされていない環境である場合
   * @throws ClientProtocolException HTTP通信でエラーが発生した場合
   * @throws IOException メールチェックに使用するコネクションでエラーが発生した場合
   */
  protected void login(HttpClient client) throws UnsupportedEncodingException, ClientProtocolException, IOException {
    HttpPost post = new HttpPost(LOGIN_URL);
    List<NameValuePair> params = new ArrayList<NameValuePair>();
    params.add(new BasicNameValuePair("", ""));  // ユーザ名
    params.add(new BasicNameValuePair("" ""));   // パスワード
    post.setEntity(new UrlEncodedFormEntity(params, "UTF-8"));
    HttpResponse res = client.execute(post);
    res.getEntity().consumeContent();
  }
}
package com.heppokoact.mailcheck;

/**
 * メールチェック中に発生した例外をラッピングして通知します。
 * @author heppokoact
 *
 */
public class MailCheckRuntimeException extends RuntimeException {

  private static final long serialVersionUID = 6006228128880409704L;

  public MailCheckRuntimeException(String message) {
    super(message);
  }

  public MailCheckRuntimeException(String message, Throwable t) {
    super(message, t);
  }

  public MailCheckRuntimeException(Throwable t) {
    super(t);
  }
}

*1:ちなみに過去4年間そんなメールは来たことがないが。

*2:POPサーバは非公開!