Laravel5でphp-imapからgmailにアクセス【cloudpack 大阪 BLOG】

PHPからgmailを取得したいと思い、方法を調査していたところphp-imapを使って取得できることがわかりました。が、結構ハマってしまいました。
今回はLaravel5を使ってgmailから固定条件で検索したメールの件名を一覧で表示させるサンプルアプリを作成してみました。

環境

 以下の構成でサンプルアプリを作成しました。
 ApachePHPのインストール等、Laravel5が動作する環境を構築をしておいてください。

サーバー : Amazon Linux
Webサーバー : Apache 2.4
PHP : php 5.6
php framework : Laravel5

php-imapのインストール

 まずはphp-imapモジュールのインストールを行います。

yum install php56-imap

phpのバージョンに合わせてインストールしてください。

Controllerの作成

[project_root]/app/Http/Controller/GmailController.phpを作成する

namespace App\Http\Controllers;

use Illuminate\Http\Request;

use App\Http\Requests;
use App\Http\Controllers\Controller;

class GmailController extends Controller
{
     public function getIndex()
     {
        $server = "imap.gmail.com";
        $port = 993;                 // ポート番号
        $account = "test@gmail.com"; // Gmailアカウント
        $password = "password";      // パスワード
        $mailbox = "{".$server.":".$port."/novalidate-cert/imap/ssl}INBOX";

         // メールサーバ接続
        $mbox=null;
        $mbox = imap_open($mailbox, $account, $password) or die('Cannot connect to Gmail: ' . imap_last_error());
        
        // メール検索(検索条件:件名にTEST MAILが含んでおり、受信日時が2015/08/08以降)
        $result = imap_search($mbox,'SUBJECT "TEST MAIL" SINCE "8 August 2015" ', SE_UID);
        
        $mail=null;
        for($i = 0;$i<count($result);$i++){
            // ヘッダ情報の概要を取得
            $overview = imap_fetch_overview($mbox, $result[$i], FT_UID);
            // 件名、送信元アドレス、送信日を取得
            $subject = mb_convert_encoding(mb_decode_mimeheader($overview[0]->subject), 'utf-8');
            $from = mb_convert_encoding(mb_decode_mimeheader($overview[0]->from), 'utf-8');
            $date = date("Y-m-d H:i:s", strtotime($overview[0]->date));

            $mail['subject'] =$subject;
            $mail['date'] = $date;
            $mail['from'] =$from;

            array_push($mail_data,$mail);
        }
 
        // メールボックスのクローズ
        imap_close($mbox);

        return view('gmail.index')->with('mail_data',$mail_data);

     }
}

Viewの作成

resources/views/app.blade.phpを以下のように修正
共通レイアウトファイルでjqueryとbootstrapを読み込んでおきます。(好みで修正してください)

<!DOCTYPE html>
  <html lang="ja">
  <head>
      <meta charset="UTF-8">
      <title>Gmail Test</title>
      <link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css" rel="stylesheet">
      <script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.4/jquery.min.js"></script>

  </head>
  <body>
      <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/js/bootstrap.min.js"></script>

      <div class="container">
          <div class="row">
              <div class="col-md-12">
                  @yield('content')
              </div>
          </div>
      </div>
  </body>
  </html>


[project_root]/resources/views/gmail/index.blade.php

@extends('app')
@section('content')

<h1>Gmail</h1>

<div  class="container">
    <table class="table table-striped table-bordered">
        <tr>
            <th>#</th>
            <th>date</th>
            <th>subject</th>
            <th>from</th>
        </tr>

@foreach ($mail_data as $index => $recorde)
        <tr>
            <td>{{$index+1 }}</td>
            <td> {{ $recorde['date'] }}</td>
            <td> {{ $recorde['subject'] }}</td>
            <td> {{ $recorde['from'] }}</td>
        </tr>
@endforeach

    </table>
</div>    

@endsection

Routingの設定

Route::controller('gmail', 'GmailController');

実行確認

「url/gmail」にアクセスしてみましょう。Gmailにアクセスしているため表示まで少し時間がかかります。
f:id:cloudfish:20151014212304p:plain
 正しく表示されない場合は、Gmailでブロックされている可能性が高いので、以下のハマったところを参考にしてみてください。

ハマったところ     

imap_openでCouldn't Open Stream エラー

以下のエラー画面が表示されました。
f:id:cloudfish:20151014203321p:plain

 まずは接続先、ポート番号、アカウント、パスワードに誤りがないか確認しましたが問題なさそうです。ググッてみましたが、設定を見直すというところで良く分かりませんでした。
そこで、以前にメールボックスtelnetでアクセスしたことを思い出し、コマンドで直接アクセスできるか試してみました。
 

openssl s_client -connect imap.gmail.com:993 -crlf 
? login test@gmail.com password

参考:telnet(openssl)でIMAPプロトコルを喋ってGmailに命令を出す。 - それマグで!

このコマンドで直接アクセスしても接続出来ませんでした。ということでもしかするとGmail側でブロックされているのかも?という可能性を疑ったところ、「ログイン試行をブロックしました」というメールが届いていました。(早くきづいておけば・・・)
ひとまず接続確認をしたかったので、セキュリティ的にオススメではありませんが「安全性の低いアプリがアカウントにアクセスするのを許可する」(下記参照)設定を行ったところ、コマンドでのアクセス確認ができアプリからも正常にアクセスできました。

参考:安全性の低いアプリがアカウントにアクセスするのを許可する - Google アカウント ヘルプ

このままではセキュリティ的によろしくないので他に方法がないか調べたところ、アプリ用のパスワードを設定する方法がありました。(下記参照)2段階認証を有効にしてアプリ用パスワードを発行し、プログラム内のパスワードをアプリ用パスワードに変更して再度実行して表示されることが確認できました。

参考:アプリ パスワードでログイン - Google アカウント ヘルプ

検索したメールが正しく表示されない

 次に固定文言で検索したメールを表示させたところ、タイトルにその文言が含まれていないメールが表示される問題がありました。
これは、imapの仕組みを良く分かっていなかったことと、imap_searchのリファレンスをしっかり読んでいなかったことが原因でした。コードをコピペして修正するだけではダメですね。
imapではメールにメッセージ番号とUIDの2種類の番号が割り当てられているようで、↑のコードではimap_searchの引数にSE_UIDを指定したためUIDが返されていました。当初、件名を取得するのにimap_headerinfo関数を使用していましたが、これはメッセージ番号をキーに取得する関数なので検索条件に該当しないメールが取得されていました。なので、ヘッダー情報をUIDでも取得できるimap_fetch_overview関数を使うことで解決しました。これでメールを取得できるようになったので、色々と効率化できそうです。