javamail – こみなのメモ帳 / 趣味と実益のネタ帳 Wed, 27 Jul 2022 10:12:46 +0000 ja hourly 1 https://wordpress.org/?v=6.1.1 javamailを使ってYahoo!メール(IMAP)でフォルダ移動する /archives/1032/ /archives/1032/#respond Wed, 27 Jul 2022 10:12:46 +0000 https://www.komina.info/?p=1032 「楽天ブログに自動で記事を投稿したい」というタイトルから脱線してきたのでそのまんまの件名に変えました。

前回は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メソッドで物理削除しています。

関連記事

https://www.komina.info/archives/887

参考資料

]]>
/archives/1032/feed/ 0
楽天ブログに自動で記事を投稿したい (2) /archives/901/ /archives/901/#respond Wed, 20 Oct 2021 05:47:46 +0000 https://www.komina.info/?p=901 前回はpop3でYahooメールの受信箱を巡回して特定メールを読みだすプログラムを書きましたが、受信箱の中のメールをすべて走査する仕組みだったため、受信箱にいっぱいメールがあると時間がかかります。

調べてみると、IMAPというプロトコルであればサーバ側でメールの検索が行えるとのこと。処理時間や通信量の削減が期待できそうなので、さっそくテストコードを書いてみました。

IMAPサーバへの接続設定は下表になります。

設定項目入力内容
受信メール(IMAP)サーバー)imap.mail.yahoo.co.jp
受信メール(IMAP)ポート番号993
アカウント名、または、ログイン名Yahoo! JAPAN ID
パスワードYahoo! JAPAN IDのパスワード
IMAPでの設定

IMAPサーバへの接続はPOPサーバへの接続の時とあまり変わりません。メール検索条件であるSearchTermオブジェクトを生成して、メッセージを取得しているところが今回のポイントです。

  • Subjectに “の日記” が含まれていること。
  • 受信日が本日であること。(時分秒の指定は無視されるため、EQ(一致)、 new Date()で機能します)
  • Fromに “no-reply@plaza.rakuten.co.jp” が含まれていること。

今回はこの3つの条件で、記事投稿用メールアドレスについてのメールかどうかを判定しています。

// https://mvnrepository.com/artifact/com.sun.mail/javax.mail
@Grab(group='com.sun.mail', module='javax.mail', version='1.6.2')

import javax.mail.Folder
import javax.mail.Message
import javax.mail.Session
import javax.mail.Store
import javax.mail.URLName
import javax.mail.search.AndTerm
import javax.mail.search.ComparisonTerm
import javax.mail.search.FromStringTerm
import javax.mail.search.ReceivedDateTerm
import javax.mail.search.SearchTerm
import javax.mail.search.SubjectTerm

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', '*')
Session session = Session.getDefaultInstance(properties)

URLName urln = new URLName('imap', host, 993, null, user, pass)
Store store = session.getStore(urln)
store.connect()
Folder inbox = store.getFolder("INBOX")
inbox.open(Folder.READ_ONLY)

SearchTerm[] stlst = [
        new SubjectTerm('日の日記'),
        new ReceivedDateTerm(ComparisonTerm.EQ, new Date()),
        new FromStringTerm('no-reply@plaza.rakuten.co.jp')
    ]
SearchTerm st = new AndTerm(stlst)
    
inbox.search(st).each { Message msg->
    println("Subject : ${msg.getSubject()}")
    println(" From    : ${msg.getFrom()}")
    println(" ReplyTo : ${msg.getReplyTo()}")
}

inbox.close()
store.close()

検索条件は抽象クラスSearchTermを継承した条件クラスのオブジェクトを生成し、単体あるいは組み合わせて指定します。私自身、すこし手こずったので全体像をメモしておきます。
ちなみに日本語で検索するのは思ったように動きませんでした。コツがあるのかもしれません。

抽象クラスメモ
javax.mail.search.SearchTerm
 AddressTerm
  FromTermfromに指定Addressが存在するか。ざっくり絞り込むならFromStringTerm の方が向いていそう。
  RecipientTermrecipientに指定Addressが存在するか。ざっくり絞り込むならRecipientStringTermの方が向いていそう。
 ComparisonTerm比較条件の抽象クラス
  DateTerm日付比較
   ReceivedDateTerm受信日で絞り込む。条件は ComparisonTerm で定義されているものを指定。日付はDate型で指定するが時分秒は無視される。
   SentDateTerm
  IntegerComparisonTerm数値比較
   MessageNumberTerm
   SizeTerm
  FlagTermメッセージのフラグ状態で抽出する。フラグの種類はjavax.mail.Flagsで定義されている。
  ModifiedSinceTerm
  OlderTerm
  YoungerTerm
  StringTerm指定の文字列が含まれているか。
   AddressStringTerm
    FromStringTerm
    RecipientStringTerm
   BodyTerm
   HeaderTerm
   MessageIDTerm
   SubjectTerm
 AndTerm2つ以上の条件オブジェクトをAND結合する。
 OrTerm2つ以上の条件オブジェクトをOR結合する。
 NotTerm指定した条件オブジェクトの論理を反転する。
型階層イメージ

関連記事

https://www.komina.info/archives/887
]]>
/archives/901/feed/ 0
楽天ブログに自動で記事を投稿したい /archives/887/ /archives/887/#respond Thu, 26 Aug 2021 09:55:11 +0000 https://www.komina.info/?p=887 楽天ブログではメールで記事を投稿する機能があります。この機能を使えばプログラムからブログ投稿が簡単にできそうです。ブラウザをコントロールしてweb画面から記事を投稿するよりはるかに簡単そうです。

ということで調べてみたのですが、「決められたアドレスへメール送信することで投稿」というものではなく、「楽天ブログから配信されるメールに書かれたアドレスへメールを送信することで投稿」できる仕組みのようなのです。

配信についてはブログの管理画面から、何曜日に送ってほしいか設定できるようになっていました。試しに設定してみると、該当する曜日の朝9時過ぎにメールが届きました。

Subject: 19日の日記
Date: Thu, 19 Aug 2021 09:06:57 +0900 (JST)
From: info@plaza.rakuten.co.jp no-reply@plaza.rakuten.co.jp
Reply-To: ******1234@mb.plaza.rakuten.co.jp
To: *******9999@yahoo.co.jp

<※このメールに返信しても日記を更新することは出来ません※>

<日記を更新する場合は、 *******1234@mb.plaza.rakuten.co.jp にメールを 送信して下さい>
<メールの件名が日記の件名、メールの本文が日記の本文として、日記が更新さ れます>
<画像を添付すると、画像付きの日記が更新できます>

<配信停止URL…

このメールに返信することで投稿を行えることは確認できました。反映もそこそこ早いみたいです。
しかしプログラムから自動でメール送信するという目標への技術的なハードルが一気に高くなってしまいました。

私は楽天用にYahooメールを使っているので、Yahooの受信箱を巡回、条件を満たすメールから記事投稿用のアドレスを取得することになります。

まずは下記のページを参考にpopアクセスを有効にしておく必要があります。(すでにメールクライアントを使っている人は不要です)

メールソフトで送受信するには(Yahoo!メールアドレスの場合) (yahoo-net.jp)

https://support.yahoo-net.jp/PccMail/s/article/H000007321
設定項目入力内容
受信メール(POP)サーバーpop.mail.yahoo.co.jp
受信メール(POP)ポート番号995
受信メール(POP)通信方法SSL(暗号化)
アカウント名、または、ログイン名Yahoo! JAPAN ID
パスワードYahoo! JAPAN IDのパスワード
popでの設定

ということで、Yahooメールの受信箱から投稿用のメールを探すプログラムを作ってみました。

// https://mvnrepository.com/artifact/com.sun.mail/javax.mail
@Grab(group='com.sun.mail', module='javax.mail', version='1.6.2')

import javax.mail.Folder
import javax.mail.Message
import javax.mail.Session
import javax.mail.Store
import javax.mail.URLName
import java.text.DateFormat
import java.text.SimpleDateFormat
import java.util.regex.Matcher
import java.util.regex.Pattern
import javax.mail.Address


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

Properties properties = System.properties
properties.setProperty("mail.pop3.socketFactory.class", SSL_FACTORY)
properties.setProperty('mail.pop3.port', '995')
properties.setProperty('mail.pop3.socketFactory.port', '995')
properties.setProperty("mail.pop3.ssl.trust", "*")  // Trust all Servers

Session session = Session.getDefaultInstance(properties)

URLName urln = new URLName('pop3', host, 995, null, user, pass)
Store store = session.getStore(urln)

store.connect()
Folder inbox = store.getFolder('Inbox')
inbox.open(Folder.READ_ONLY)

DateFormat sdf = new SimpleDateFormat('yyyy/MM/dd')
Date today = new Date()
String todayYmd = sdf.format(today)
println("今日の日付 : ${todayYmd} の投稿メール")
println("メッセージ数 : ${inbox.messageCount}")

Pattern ptn = ~/^(?<day>[0-9]{1,2})日の日記$/

Message[] messages = inbox.getMessages()
messages.each { Message it->
    Matcher mt = ptn.matcher(it.subject)
    Date sentDate = it.getSentDate()
    if (mt.find() && sentDate != null) {
        println("Subject : ${it.getSubject()}")
        String sentYmd = sdf.format(sentDate)
        int day = mt.group('day').toInteger()
        if (todayYmd == sentYmd) {
            println(" From    : ${it.getFrom()}")
            println(" ReplyTo : ${it.getReplyTo()}")
        }
    }
}

inbox.close(true)
store.close()

ユーザIDはメールアドレスではなく、あくまでID部分になります。
また、本格的に使う場合は、try-catchを使ってinbox.closeやstore.closeはfinally句に書くとかした方がいいと思います。

そんなこんなで投稿用のメールアドレスの取得が確立できました。

参考にしたページ

JavaMail POP3 over SSL Connect failed;

https://stackoverflow.com/questions/22037210/javamail-pop3-over-ssl-connect-failed

java mail pop3 imap 受信

https://qiita.com/koryo/items/0fa66023d7ca8efcff41

A JavaMail POP reader example (pop3 reader)

https://alvinalexander.com/java/javamail-pop-pop3-reader-email-inbox-example/

追記

うまく動かないときはデバッグ出力を有効にしましょう。

properties.setProperty("mail.debug", "true")

IDやパスワードが正しいはずなのにどうしてもエラーになってしまう場合、こんなエラーメッセージ「(#AUTH403) Incorrect username or password.」が出ていませんか?

これは海外からのアクセス制限が有効になっているときの応答らしいです。クラウド上のPCでは、意図しないうちに海外からのアクセスになっていることがあります。下記ヘルプを参考に制限を無効化しましょう。

Yahoo!メール ヘルプ – 海外からのアクセス制限 –
海外からのメールソフトによるアクセスを制限できます。海外から、メールソフトやスマートフォンを利用してYahoo!メールを閲覧しない場合は、アクセス制限を設定することで、メールアカウントを第三者に利用されることや、メールの情報が漏えいする危険性を軽減できます。

https://support.yahoo-net.jp/SccMail/s/article/H000010027

関連記事

]]>
/archives/887/feed/ 0