PerlからIMAPでGmailを受信する

PerlからGmailを送信する方法については前に調べたけど、今回はGmailを受信する方法を調べてみた。

Net::IMAP::Clientでメールの受信、Email::MIMEで本文のParseをする。

まずは動くコードを。GmailのINBOXから最新10件のDate、From、Subject、本文を出力します。

use strict;
use warnings;
use utf8;
use Encode;
use IO::Socket::SSL;
use Net::IMAP::Client;
use Email::MIME;
use HTML::FormatText;

binmode STDOUT, ":utf8";
binmode STDERR, ":utf8";

my $imap = Net::IMAP::Client->new(
   server   => 'imap.gmail.com',
   user     => $USER_ID,
   pass     => $PASSWORD,
   ssl      => 1,
   port     => 993,
) or die "Could not connect to IMAP server" unless $imap;

$imap->login or die('Login failed: ' . $imap->last_error);

$imap->select('INBOX');
my $messages = $imap->search('ALL');

for(my $i=0; $i<10; $i++){
  my $idx = scalar @$messages - $i - 1;
  my $data = $imap->get_rfc822_body($$messages[$idx]);
  my $parsed = Email::MIME->new($data);
  printf("%s %s %s\n", $parsed->header('Date'), $parsed->header('From'), $parsed->header('Subject'));
  
  my $body = '';
  $parsed->walk_parts(sub {
    my $part = shift;
    return if $part->subparts;
    if($part->content_type =~ m{text/html}i){
      $body = HTML::FormatText->format_string($part->body_str);
    }
    elsif($part->content_type =~ m{text/plain}i){
      $body = $part->body_str;
    }
  });
  # CRLF -> LF
  $body =~ s/\r\n/\n/g;
  printf("%s\n", $body);
}
$imap->logout;

短いコードだけど意外とハマったのでまとめておく。

GmailのSEARCH

「最新n件」のメールを表示させたい。

PODを見ると、Net::IMAP::Client->search の第2引数に 'REVERSE DATE' や '^DATE' のようなソート条件を指定できるらしい。
が、なにか指定するとreturnが空になってしまう。
capabilityを調べてみるとSORTが無いようなので、それが原因なのかなあ。

### $capab: [
###           'IMAP4rev1',
###           'UNSELECT',
###           'IDLE',
###           'NAMESPACE',
###           'QUOTA',
###           'ID',
###           'XLIST',
###           'CHILDREN',
###           'X-GM-EXT-1',
###           'UIDPLUS',
###           'COMPRESS=DEFLATE',
###           'ENABLE',
###           'MOVE',
###           'CONDSTORE',
###           'ESEARCH',
###           'UTF8=ACCEPT',
###           'LIST-EXTENDED',
###           'LIST-STATUS',
###           'LITERAL-',
###           'SPECIAL-USE',
###           'APPENDLIMIT=35651584'
###         ]

第1引数も'NEW'とか'RECENT'とか指定してみると空のリストが帰ってくる。よくわからん。

とりあえず'ALL'で全件とって、後ろからn件見ていくという実装にした。

IMAPは難しいな。。時間があるときにちゃんと調べてみよう。

Headerのdecode

ちょっと調べてみると手動でdecodeしている強者もいるみたいですが、Email::MIMEに任せるのが楽。

日本語がエンコードされたSubjectも上手いことUnicode文字列にしてくれます。

Bodyのdecode

これがちょっとややこしい。bodyなんちゃらは若干バリエーションがあって

  • body_raw : そのまま(base64エンコード、7bit ASCIIなど)のbody
  • body : base64 decodeされたbody
  • body_str : base64 decodeされたbodyをさらにcharsetでUnicode文字列にdecodeされたもの

基本的には body_str メソッドを使えばよい。

マルチパートの処理

普通のテキストメールならよいのだけど、HTMLメールにも対応する場合はマルチパートの処理が必要になる。

ということで walk_parts を使う。だいたいサンプルのパクリですが。。

普通に?partsメソッドをforループで回しても良さそうなんだけど、なんか不穏な解説があるので止めておく。

This is a stupid method. Don't use it.

Email::MIME - easy MIME message handling - metacpan.org

まぁ再帰的な構造だからコールバック関数で処理するというのが自然ではあるが、stupid methodとまで言わなくてもねぇ・・・。

CRLF -> LF

メールの改行はCRLFなのでLFに統一する。