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