|||||||||||||||||||||

なんぶ電子

- 更新: 

PHPでIMAPを使う

PHP IMAP

以前Roundcubeを設定したことがありましたが、このアプリケーションの一部はPHPでのIMAPの実装といえるでしょう。

今回はそのようなIMAPを自身のPHPのコーディングに加えてみます。

例示に使う環境は64bitのDebian11です。

IMAP拡張ライブラリ

PHPには、IMAP拡張ライブラリが用意されています。

DebianでAPTでインストールする場合は特に問題ないと思いますが、そうでない場合に注意が必要なのは、IMAPモジュールはNTS(Non Thread Safe)なので、スレッドセーフな環境で使うとトラブルの元です。

環境を確認するには、php -v でNTSの文字があるかで確認をします。NTSとあればNon Thread Safe版ですので、IMAPモジュールとの齟齬はありません。

$ php -v
PHP 7.4.33 (cli) (built: Feb 22 2023 20:07:47) ( NTS )
Copyright (c) The PHP Group
Zend Engine v3.4.0, Copyright (c) Zend Technologies with Zend OPcache v7.4.33, Copyright (c), by Zend Technologies

IMAPライブラリの追加は、PHPのバージョンが7でよければAPTで行うことができます。

ビルドをする場合は configure で --with-imap を指定するのですが、別途 c-client が必要になります。c-clientは C言語で記述されたPOP3やIMAP用のライブラリです。これは libc-client2007e-dev として取得できます(2007eの部分は可変)。

--with-imapオプションに連動して--with-kerberos(ケルベロス認証)と、--with-imap-sslの指定も求められます。

ケルベロス認証はlibkrb5-devとしてAPTで取得可能です。

# apt upddate
...
#apt install php-imap
...

基本的なコード

基本的な使い方は次のようになります。

sample1.php

// サーバーの接続情報
$imap_server = '{imap.example.jp:993/imap/ssl}INBOX';
$username = 'user';
$password = 'password';

$imap_connection = imap_open($imap_server, $username, $password);

// メールボックス内の未読メールを検索 ALL だと全件
$emails = imap_search($imap_connection, 'UNSEEN');

if ($emails) {
  // メールが見つかった場合、ループして各メールを処理
  foreach ($emails as $email_number) {
    // メールのヘッダ情報を取得
    $headers = imap_headerinfo($imap_connection, $email_number);

    // メールの本文を取得
    $body = imap_body($imap_connection, $email_number);

    // メールを既読にする
    imap_setflag_full($imap_connection, $email_number, "\\Seen");
  }
}

// IMAPサーバーから切断
imap_close($imap_connection);

imap_open で接続を開始しますが、この時の第1引数に渡す文字列は次のようになります。

{ サーバーアドレス [:ポート][/オプション...]}[メールボックス名]

アドレスとポートはいいと思います。オプションは決められた文字列があります。先の例では imap(他にpop3なども指定可能です)と ssl を指定しています。

STARTTLSで接続する際は、sslの部分を tls とします。また、ssl 指定時にはデフォルトでサーバーの証明書を検証しますが、検証しない場合は novalidate-cert を指定します。

プレーンテキスト認証(sslとは別)を禁止したい場合は、secure オプションも存在します。

メールボックス名はデフォルトでは INBOX となります。これはカレントユーザーの個人メールボックスを意味する特殊な予約語です。

imap_search ではメールボックスからメールを抽出する作業をしています。この第2引数に UNSEEN を設定する事で未読メールを抽出します。全件抽出の場合は ALL とします。

メールのヘッダを imap_headerinfo で、ボディを imap_body で読み込んだ後は、imap_setflag_full に \\Seen フラグを渡してメールを既読にしています。

マルチバイト文字

先の処理ではBase64やQuoted Printableでエンコーディングされた文字列がそのまま読み出されてしまいます。また、マルチパートにも対応していません。

それらに対応するにはループ内でメッセージを処理する際に imap_fetchstructure を使って次のようにします。

sample2.php

...

foreach ($emails as $email_number) {
  // メールのヘッダ情報を取得
  $headers = imap_headerinfo($imap_connection, $email_number);


  // タイトルを取得
  $title = "";
  foreach(imap_mime_header_decode($headers->subject) as $v) {
    $title.=$v->text;
  }
  
  // メッセージの構造を取得
  $structure = imap_fetchstructure($imap_connection, $email_number);
  
  // メール本文の取得
  $message = '';
  if (isset($structure->parts)) {
    // マルチパートの場合、パートごとに処理
    foreach ($structure->parts as $part_number => $part) {
      if ($part->type == 0) {
        // テキストメールの場合
        $message = imap_fetchbody($imap_connection, $email_number, $part_number+1);
        if ($part->encoding == 1) {
          // 8bitエンコーディングの場合、UTF-8に変換
          $message = imap_utf8($message);
        } elseif ($part->encoding == 4) {
          // Quoted Printableエンコーディングの場合
          $message = quoted_printable_decode($message);
        } elseif ($part->encoding == 3) {
          // Base64エンコーディングの場合
          $message = base64_decode($message);
        }
      } elseif ($part->type == 3) {
        // 添付ファイルの場合
      }
    }
  } else {
    // シングルパートの場合
    $message = imap_body($imap_connection, $email_number);
    if ($structure->encoding == 1) {
      // 8bitエンコーディングの場合、UTF-8に変換
      $message = imap_utf8($message);
    } elseif ($structure->encoding == 4) {
      // Quoted Printableエンコーディングの場合、デコード
      $message = quoted_printable_decode($message);
    } elseif ($structure->encoding == 3) {
      // Base64エンコーディングの場合、デコード
      $message = base64_decode($message);
    }
  }
  
  // メールを既読にする
  imap_setflag_full($imap_connection, $email_number, "\\Seen");
}
...

ここでの imap_fetchstructureの処理は、上記の例は少し乱暴な書き方をしています。

$partをループで回す部分で、typeプロパティでデータ種を判定していますが、この値は公式ページに次のように載っているように、本来定数で扱うべきです。

  • 0:TYPETEXT
  • 1:TYPEMULTIPART
  • 2:TYPEMESSAGE
  • 3:TYPEAPPLICATION
  • 4:TYPEAUDIO
  • 5:TYPEIMAGE
  • 6:TYPEVIDEO
  • 7:TYPEMODEL
  • 8:TYPEOTHER

また、上記のコードではマルチパート部がネストしていた場合を考慮していません。つまり、type=1の値の時は何も処理していません。また、多くの添付ファイルが、Applicationであるという前提でtype=3を添付ファイルとして保存しています。

エンコーディング部も定数が用意されていますので、参考までに書き残しておきます。

  • 0:ENC7BIT
  • 1:ENC8BIT
  • 2:ENCBINARY
  • 3:ENCBASE64
  • 4:ENCQUOTEDPRINTABLE
  • 5:ENCOTHER

サーバー内のCAの証明書情報が古くてエラーになる場合は、「 update-ca-certificates 」を実行します。

またこの imap_open(ライブラリ内の他のものもそうなのかもしれません)で発生する NOTICE や ERROR は @をつけてもブラウザに出力されてしまうようなので、本番環境では「 error_reporting(0); 」を設定しましょう。

筆者紹介


自分の写真
がーふぁ、とか、ふぃんてっく、とか世の中すっかりハイテクになってしまいました。プログラムのコーディングに触れることもある筆者ですが、自分の作業は硯と筆で文字をかいているみたいな古臭いものだと思っています。 今やこんな風にブログを書くことすらAIにとって代わられそうなほど技術は進んでいます。 生活やビジネスでPCを活用しようとするとき、そんな第一線の技術と比べてしまうとやる気が失せてしまいがちですが、おいしいお惣菜をネットで注文できる時代でも、手作りの味はすたれていません。 提示されたもの(アプリ)に自分を合わせるのでなく、自分の活動にあったアプリを作る。それがPC活用の基本なんじゃなかと思います。 そんな意見に同調していただける方向けにLinuxのDebianOSをはじめとした基本無料のアプリの使い方を紹介できたらなと考えています。

広告