2018-05-05 PerlでOutlook受信トレイ内のメールを取得・表示
PerlでOutlook受信トレイ内のメールを取得・表示するサンプルコードを作りました。巷で見掛けないようなので。
概要
・Outlookの任意フォルダ内にあるメールのヘッダ,件名,受信日時,本文を標準出力に出力
・WindowsのActivePerlを使用。Windows以外は対象外
・文字エンコードを意識しない版と,UTF-8版の2種類を作成。不慣れな人は,「Win32::OLEでActiveXオブジェクトがどうの」という問題よりも,PerlのUTF-8フラグの扱いでハマると思われます
・実行するPCにはOutlookのインストールが必要。OutlookがインストールされていないPCでは使用できない
詳細
Perl独特の作法で注意すべき点を中心に。
・Win32::OLEクラスを使用しOutlook.Applicationのインスタンスを獲得
・「Outlook.ApplicationからMAPIという名前のName Spaceを獲得。Outlookのデータフォルダ一覧からフォルダ階層を辿って,受信トレイや送信トレイのFolderオブジェクトを‥‥」という以後の手順は,Perlとはほとんど関係がなくて,Microsoftが提供しているVBやC#の説明と同様
・オブジェクトの型はどれもWin32::OLEになる。Perlからは型の区別が付かない。VBA/VBSで作成したものをPerlに移植しながら作業すると「察し」が付きやすくなる
・変数に格納したデータをData::Dumpしたいところだが,ダンプするとPerlインタプリタが強制終了。内部のデータがどうなっているか,覗き見することは無理なようです
・タイムスタンプの扱いもちょっと面倒。日付時刻は,Win32::OLE::Variantオブジェクトで返されます。Win32::OLE::VariantのDateメソッドとTimeメソッドで,日付と時刻に各々分けて取得。一度,ISO8601形式(YYYY-MM-DDTHH:MM:SS)の文字列にしてから,Time::Pieceクラスで解析。Time::Piece クラスのepochメソッドでUNIXエポック秒に変換してみました
文字エンコードなし版
Win32::OLEが使用できることを確認するためのサンプルです。
シェルの標準出力に表示する以上のことはできません。しようとすると途端に難易度が上がります。たとえば,JSON形式でファイル保存する。XML形式でファイル保存する。HTTPでリモートホストにリクエストを投げる,などなど。さらに話を進めたい方は,つぎのUTF-8版をご利用ください。
# このファイルはCP932(Shift_JIS)で保存すること
use strict;
use Win32::OLE;
use Win32::OLE::Const;
use Time::Piece;
#####
use constant TZ => -9*60*60; # 日本時間のタイムゾーン(JST-9)
#####
#my $data_folder = 'Outlook データ ファイル/受信トレイ';
my $data_folder = undef; # 空の場合はフォルダ選択ダイアログを表示
my $outlook;
eval {
  # すでにOutlookが起動しているならそのインスタンスを取得
  $outlook = Win32::OLE->GetActiveObject('Outlook.Application');
};
if($@ or !defined $outlook) {
        $outlook = Win32::OLE->new('Outlook.Application', sub {$_[0]->Quit}) or die;
}
# Windowsタイプライブラリの定数一覧をハッシュにロード
my $const = Win32::OLE::Const->Load($outlook);
my $namespace = $outlook->GetNameSpace('MAPI') or die;
my $folders;
if(!$data_folder) {
  # $data_folderが空の場合はフォルダ選択ダイアログを表示
  $folders = $namespace->PickFolder();
} else {
  $folders = $namespace;
  foreach(split('/', $data_folder)) {
    # フォルダ階層を辿る。指定したフォルダの参照を得る
    $folders = $folders->Folders()->Item($_);
  }
}
# 選択したフォルダのパスを出力
my @path;
for(my $o = $folders; $o; $o = $o->Parent) {
  if($o->Class == $const->{olFolder}) {
    push(@path, $o->Name);
  }
}
print join('/', reverse(@path)), "\n";
print "\n";
# フォルダからアイテム一覧を取得
my $items = $folders->Items();
for(my $item = $items->getFirst(); $item; $item = $items->GetNext()) {
  my $header = $item->PropertyAccessor->GetProperty('http://schemas.microsoft.com/mapi/proptag/0x007D001E');
  my $subject = $item->Subject;
  my $body = $item->Body;
  my $received_time = $item->ReceivedTime; # Win32::OLE::Variantオブジェクト
  my $tp = Varinat2time($received_time, &TZ); # Variant -> シリアル値
  $header =~ s/\x0d\x0a/\n/g; # 改行コードを\nにする
  $body =~ s/\x0d\x0a/\n/g;
  print $header;
  print $subject;
  print Time::Piece->localtime($tp)->strftime('%Y/%m/%d %H:%M:%S'), "\n";
  print "\n";
  print $body, "\n";
}
Win32::OLE->FreeUnusedLibraries();
exit;
sub Varinat2time {
  my $p = shift; # Win32::OLE::Variantオブジェクト
  my $tz = shift || 0;
  # 日付時刻はWin32::OLE::Variant型でラップされる
  # 日付と時刻を分けてパース。Time::Pieceを介して整数のエポック秒に変換する
  my $iso8601 = sprintf('%sT%s', $p->Date('yyyy-MM-dd'), $p->Time('HH:mm:ss'));
  my $tp = Time::Piece->strptime($iso8601, '%Y-%m-%dT%H:%M:%S');
  $tp += $tz; # タイムゾーンを合わせる
  return $tp->epoch;
}
# EOF
UTF-8版
UTF-8フラグの挙動を合わせたバージョンです。以下,概要。
・ソースファイルはUTF-8(BOMなし)で保存する
・Win32::OLEオブジェクトからゲットした文字列は,CP932からUTF-8に文字コードを変換する。Encode::decodeでUTF-8フラグを付ける
・Win32::OLEオブジェクトにセットする文字列は,UTF-8からCP932に文字コードを変換する。Encode::encodeでUTF-8フラグを外す
# UTF-8版
# このファイルはUTF-8N(UTF-8,BOM(Byte Order Mark)なし)で保存すること
# Windowsの「メモ帳」は不適なので注意。ファイル保存するとBOMありになる
use strict;
use utf8;
use Win32::OLE;
use Win32::OLE::Const;
use Time::Piece;
#####
use constant TZ => -9*60*60; # 日本時間のタイムゾーン(JST-9)
use constant CODE_PAGE => 'cp932'; # 文字コード:日本語Windows(CP932)
#####
binmode(STDOUT, ':raw :encoding(cp932)');
#####
#my $data_folder = 'Outlook データ ファイル/受信トレイ';
my $data_folder = undef; # 空の場合はフォルダ選択ダイアログを表示
my $outlook;
eval {
  # すでにOutlookが起動しているならそのインスタンスを取得
  $outlook = Win32::OLE->GetActiveObject('Outlook.Application');
};
if($@ or !defined $outlook) {
        $outlook = Win32::OLE->new('Outlook.Application', sub {$_[0]->Quit}) or die;
}
# Windowsタイプライブラリの定数一覧をハッシュにロード
my $const = Win32::OLE::Const->Load($outlook);
my $namespace = $outlook->GetNameSpace('MAPI') or die;
my $folders;
if(!$data_folder) {
  # $data_folderが空の場合はフォルダ選択ダイアログを表示
  $folders = $namespace->PickFolder();
} else {
  $folders = $namespace;
  foreach(split('/', $data_folder)) {
    # フォルダ階層を辿る。指定したフォルダの参照を得る
    $folders = $folders->Folders()->Item(Encode::encode(CODE_PAGE, $_));
  }
}
# 選択したフォルダのパスを出力
my @path;
for(my $o = $folders; $o; $o = $o->Parent) {
  if($o->Class == $const->{olFolder}) {
    push(@path, Encode::decode(CODE_PAGE, $o->Name));
  }
}
print join('/', reverse(@path)), "\n";
print "\n";
# フォルダからアイテム一覧を取得
my $items = $folders->Items();
for(my $item = $items->getFirst(); $item; $item = $items->GetNext()) {
  my $header = $item->PropertyAccessor->GetProperty('http://schemas.microsoft.com/mapi/proptag/0x007D001E');
  my $subject = Encode::decode(CODE_PAGE, $item->Subject);
  my $body = Encode::decode(CODE_PAGE, $item->Body);
  my $received_time = $item->ReceivedTime; # Win32::OLE::Variantオブジェクト
  my $tp = Varinat2time($received_time, &TZ); # Variant -> シリアル値
  $header =~ s/\x0d\x0a/\n/g; # 改行コードを\nにする
  $body =~ s/\x0d\x0a/\n/g; # 改行コードを\nにする
  print $header;
  print $subject;
  print Time::Piece->localtime($tp)->strftime('%Y/%m/%d %H:%M:%S'), "\n";
  print "\n";
  print $body, "\n";
}
Win32::OLE->FreeUnusedLibraries();
exit;
sub Varinat2time {
  my $p = shift; # Win32::OLE::Variantオブジェクト
  my $tz = shift || 0;
  # 日付時刻はWin32::OLE::Variant型でラップされる
  # 日付と時刻を分けてパース。Time::Pieceを介して整数のエポック秒に変換する
  my $iso8601 = sprintf('%sT%s', $p->Date('yyyy-MM-dd'), $p->Time('HH:mm:ss'));
  my $tp = Time::Piece->strptime($iso8601, '%Y-%m-%dT%H:%M:%S');
  $tp += $tz; # タイムゾーンを合わせる
  return $tp->epoch;
}
# EOF