javamailを使ってYahoo!メール(IMAP)でフォルダ移動する

  技術情報, 未分類

「楽天ブログに自動で記事を投稿したい」というタイトルから脱線してきたのでそのまんまの件名に変えました。

前回はIMAPプロトコルで狙ったメールの受信を行うプログラムを書きました。

メールを解析した後は不要なのでメーラを使って手作業で消していたのですが、毎回はさすがに面倒なので解析の流れでプログラムで削除まで行うことにしました。

IMAPプロトコルではフォルダ間のメッセージ移動が可能なので、受信箱に届いたメッセージをゴミ箱へ移動させることにします。

String host = 'imap.mail.yahoo.co.jp'
String user = '******'
String pass = '******'
String SSL_FACTORY = "javax.net.ssl.SSLSocketFactory"

Properties properties = System.properties
properties.setProperty('mail.imap.socketFactory.class', SSL_FACTORY)
properties.setProperty('mail.imap.ssl.trust', '*')
properties.setProperty('mail.debug', 'true')

Session session = Session.getInstance(properties)
URLName urln = new URLName('imap', host, 993, null, user, pass)
Store store = session.getStore(urln)
store.connect()
IMAPFolder inbox = store.getFolder('Inbox')
inbox.open(Folder.READ_WRITE)

SearchTerm[] stlst = [
        new SubjectTerm('日の日記'),
        new ReceivedDateTerm(ComparisonTerm.EQ, new Date()),
        new FromStringTerm('no-reply@plaza.rakuten.co.jp')
    ]
SearchTerm st = new AndTerm(stlst)

List<Message> target = []
inbox.search(st).each { Message msg->
    println("Subject  : ${msg.getSubject()}")
    println(" From    : ${msg.getFrom()}")
    println(" ReplyTo : ${msg.getReplyTo()}")
    target.add(msg)
}

if (!target.isEmpty()) {
    Message[] targetArray = target.toArray(new Message[0])
    inbox.moveMessages(targetArray, store.getFolder('Trash'))
}

inbox.close()
store.close()

ポイントは store.getFolder('Inbox')com.sun.mail.imap.IMAPFolderで受けているところです。
メッセージの移動はIMAP固有の機能なのでjavax.mail.FolderにはmoveMessagesメソッドが定義されていません。

問題発生

これで課題解決かと思ったのですが実際に実行してみると例外が発生します。うーん?デバッグ出力を有効にしてみるとMOVEコマンドでエラーが発生していました。

A6 MOVE 15,33 Trash
A6 NO [CANNOT] MOVE It's not possible to perform specified operation

プログラムの書き方の問題ではなくIMAPサーバ側でコマンドを拒否しているようです。メッセージの移動を一切禁止するとかあるのだろうか。

ちなみに上記の A6 というのはコマンドとその応答を紐づけるものです。並列実行を考慮した実装となっているそうです。

原因の調査

さてプロトコル系でうまくいかないときは正常に動いているものを参考にするのが定石です。Thunderbirdという無料メーラーはログを出力する機能があるのでそれを利用することにしました。

環境変数 NSPR_LOG_MODULES にプロトコル種別とログレベルを設定(IMAP:3 など)、NSPR_LOG_FILE に保存先ファイルパスを設定した上でThunderbirdを起動します。(※ログは起動のたびにクリアされます)

Thunderbirdの画面からメッセージの移動を行ったところ以下のコマンドが発行されていました。(抜粋)

65 uid move 394141 "Trash"
* OK [COPYUID 4 394141 359316]
* 2 EXPUNGE
65 OK UID MOVE completed

単なる MOVEコマンドではなく、UID MOVEコマンドを使用していました。

MOVEコマンドで指定しているのはメッセージ番号、これはメールボックス内の連番でセッション内で有効な番号。一方、UID MOVEコマンドで指定しているのはUIDというものでメールボックス内で一意で永続的であることが保証された番号。

どうやらYahoo!メールのIMAPではメッセージ番号を使った移動はサポートしていないということらしい。

解決方法

さてメッセージを移動させる方法は分かりました。しかし UID MOVEコマンドをjavamail-1.6.2がサポートしているのかというとどうも期待できなさそうです。moveuidとかcopyuidとか思わせぶりなメソッドは存在するのですが実際に発行しているコマンドは MOVEコマンドでした。

そうなると自前で発行するしかありません。
javamailには自前のプロトコル処理に差し替える機能があるようですが(プロパティmail. + プロトコル名 +.classなど)、正直そこまでやるほどの労力を使いたくありません。

そこでimap関連の機能を利用しつつ UID MOVE コマンド発行することにしました。

SearchTerm[] stlst = [
        new SubjectTerm('日の日記'),
        new ReceivedDateTerm(ComparisonTerm.EQ, new Date()),
        new FromStringTerm('no-reply@plaza.rakuten.co.jp')
    ]
SearchTerm st = new AndTerm(stlst)
IMAPMessage[] msgs = inbox.search(st)

FetchProfile fp = new FetchProfile()
fp.add(FetchProfile.Item.ENVELOPE)  
fp.add(UIDFolder.FetchProfileItem.UID)
inbox.fetch(msgs, fp)

msgs.each { IMAPMessage msg->
    println("Subject  : ${msg.getSubject()}")
    println(" From    : ${msg.getFrom()}")
    println(" ReplyTo : ${msg.getReplyTo()}")
    println(" UID     : ${msg.getUID()}")
}
if (msgs.length > 0) {
    inbox.getProtocol().simpleCommand('UID MOVE '
        + UIDSet.toString(Utility.toUIDSet(msgs)) 
        + ' "Trash"', null)
}

UID MOVE コマンドを発行するにあたりUIDが必要となるので、まずはfecthメソッドにてUIDを取得します。そしてそのあとにsimpleCommandメソッドを用いてコマンドを発行しています。

UIDSet.toString(Utility.toUIDSet(msgs))は幾つかのUIDをまとめてくれる便利メソッドです。(123,124,125といった連番であれば123:125のようにまとめてくれる)

Yahoo!メールではゴミ箱は使用容量のカウント外となるのでひとまず移動だけでオッケーですね。

おまけ

IMAPでメッセージ削除は論理削除→物理削除の順で行います。
論理削除はメッセージに対してDeletedフラグを立てる形で行われるのですが、こちらも単なるSTOREコマンドではエラーになり、UID STOREコマンドを用いる必要がありました。

if (msgs.length > 0) {
    inbox.getProtocol().simpleCommand('UID STORE '
        + UIDSet.toString(Utility.toUIDSet(msgs))
        + ' +FLAGS '
        + inbox.getProtocol().createFlagList(new Flags(Flags.Flag.DELETED))
        , null)
}
inbox.expunge()

+FLAGSでフラグON、-FLAGSでフラグOFFを意味します。UID STOREコマンドで削除フラグを立てて、expungeメソッドで物理削除しています。

関連記事

参考資料

LEAVE A COMMENT