開発者は、電子メールメールボックスへのアクセスを必要とするタスクに遭遇することがあります。ほとんどの場合、これは インターネットメッセージアクセスプロトコル、またはIMAP 。として PHP開発者 、私は最初にPHPに目を向けました 組み込みのIMAPライブラリ 、ただし、このライブラリにはバグがあり、デバッグや変更は不可能です。また、プロトコルの機能を最大限に活用するためにIMAPコマンドをカスタマイズすることもできません。
プットオプションの価値
そこで本日は、PHPを使用してゼロから機能するIMAP電子メールクライアントを作成します。使い方も見ていきます Gmailの特別なコマンド 。
IMAPをカスタムクラスimap_driver
に実装します。クラスを構築する際の各ステップについて説明します。全体をダウンロードできますimap_driver.php
記事の終わりに。
IMAPは接続ベースのプロトコルであり、通常はTCP / IPを介して動作します。 SSLセキュリティ したがって、IMAP呼び出しを行う前に、接続を開く必要があります。
接続するIMAPサーバーのURLとポート番号を知る必要があります。この情報は通常、サービスのWebサイトまたはドキュメントで宣伝されます。例えば、 Gmailの場合 、URLはssl://imap.gmail.com
ですポート993。
初期化が成功したかどうかを知りたいので、クラスコンストラクターを空のままにし、すべての接続はカスタムinit()
で行われます。 false
を返すメソッド接続を確立できない場合:
class imap_driver { private $fp; // file pointer public $error; // error message ... public function init($host, $port) { if (!($this->fp = fsockopen($host, $port, $errno, $errstr, 15))) { $this->error = 'Could not connect to host ($errno) $errstr'; return false; } if (!stream_set_timeout($this->fp, 15)) { $this->error = 'Could not set timeout'; return false; } $line = fgets($this->fp); // discard the first line of the stream return true; } private function close() { fclose($this->fp); } ... }
上記のコードでは、fsockopen()
の両方に15秒のタイムアウトを設定しました接続を確立し、データストリーム自体が開いたら要求に応答するため。多くの場合、サーバーが応答しないため、ネットワークへのすべての呼び出しに対してタイムアウトを設定することが重要です。このようなフリーズを処理できる必要があります。
また、ストリームの最初の行を取得して無視します。通常、これはサーバーからの単なる挨拶メッセージ、またはサーバーが接続されていることの確認です。特定のメールサービスのドキュメントをチェックして、これが当てはまることを確認してください。
次に、上記のコードを実行して、init()
を確認します。成功しました:
include('imap_driver.php'); // test for init() $imap_driver = new imap_driver(); if ($imap_driver->init('ssl://imap.gmail.com', 993) === false) { echo 'init() failed: ' . $imap_driver->error . '
'; exit; }
これで、IMAPサーバーに対してアクティブなソケットが開かれたので、IMAPコマンドの送信を開始できます。 IMAP構文を見てみましょう。
正式なドキュメントは、インターネット技術特別調査委員会(IETF)にあります。 RFC3501 。 IMAPの相互作用は通常、クライアントがコマンドを送信し、サーバーが成功の兆候で応答し、要求された可能性のあるデータで構成されます。
コマンドの基本的な構文は次のとおりです。
line_number command arg1 arg2 ...
行番号または「タグ」は、コマンドの一意の識別子であり、サーバーが複数のコマンドを一度に処理する場合に、サーバーが応答しているコマンドを示すために使用します。
これは、LOGIN
を示す例です。コマンド:
00000001 LOGIN [email protected] password
サーバーの応答は、「タグなし」のデータ応答で始まる場合があります。たとえば、Gmailはログインの成功に、サーバーの機能とオプションに関する情報を含むタグなしの応答で応答し、メールメッセージをフェッチするコマンドは、メッセージ本文を含むタグなしの応答を受信します。いずれの場合も、応答は常に「タグ付き」コマンド完了応答行で終了し、応答が適用されるコマンドの行番号、完了ステータスインジケーター、およびコマンドに関する追加のメタデータ(存在する場合)を識別します。
line_number status metadata1 metadata2 ...
GmailがLOGIN
にどのように応答するかを次に示します。コマンド:
* CAPABILITY 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 00000001 OK [email protected] authenticated (Success)
00000001 NO [AUTHENTICATIONFAILED] Invalid credentials (Failure)
ステータスは、成功を示すOK
、失敗を示すNO
、または無効なコマンドまたは不正な構文を示すBAD
のいずれかになります。
コマンドをIMAPサーバーに送信し、応答とエンドラインを取得する関数を作成してみましょう。
class imap_driver { private $command_counter = '00000001'; public $last_response = array(); public $last_endline = ''; private function command($command) { $this->last_response = array(); $this->last_endline = ''; fwrite($this->fp, '$this->command_counter $command
'); // send the command while ($line = fgets($this->fp)) { // fetch the response one line at a time $line = trim($line); // trim the response $line_arr = preg_split('/s+/', $line, 0, PREG_SPLIT_NO_EMPTY); // split the response into non-empty pieces by whitespace if (count($line_arr) > 0) { $code = array_shift($line_arr); // take the first segment from the response, which will be the line number if (strtoupper($code) == $this->command_counter) { $this->last_endline = join(' ', $line_arr); // save the completion response line to parse later break; } else { $this->last_response[] = $line; // append the current line to the saved response } } else { $this->last_response[] = $line; } } $this->increment_counter(); } private function increment_counter() { $this->command_counter = sprintf('%08d', intval($this->command_counter) + 1); } ... }
LOGIN
コマンドこれで、command()
を呼び出す特定のコマンドの関数を記述できます。ボンネットの下で機能します。 LOGIN
の関数を書いてみましょうコマンド:
class imap_driver { ... public function login($login, $pwd) { $this->command('LOGIN $login $pwd'); if (preg_match('~^OK~', $this->last_endline)) { return true; } else { $this->error = join(', ', $this->last_response); $this->close(); return false; } } ... }
これで、このようにテストできます。 (テストするには、アクティブな電子メールアカウントが必要であることに注意してください。)
... // test for login() if ($imap_driver->login(' [email protected] ', 'password') === false) { echo 'login() failed: ' . $imap_driver->error . '
'; exit; }
Gmailはデフォルトでセキュリティに関して非常に厳格であることに注意してください。デフォルト設定があり、アカウントプロファイルの国以外の国からアクセスしようとすると、IMAPでメールアカウントにアクセスできなくなります。しかし、修正するのは簡単です。説明されているように、Gmailアカウントで安全性の低い設定を設定するだけです ここに 。
SELECT
コマンドそれでは、メールで何か役立つことをするためにIMAPフォルダを選択する方法を見てみましょう。 LOGIN
のおかげで、構文はcommand()
の構文と似ています。方法。 SELECT
を使用します代わりにコマンドを実行し、フォルダを指定します。
class imap_driver { ... public function select_folder($folder) { $this->command('SELECT $folder'); if (preg_match('~^OK~', $this->last_endline)) { return true; } else { $this->error = join(', ', $this->last_response); $this->close(); return false; } } ... }
それをテストするために、受信ボックスを選択してみましょう。
... // test for select_folder() if ($imap_driver->select_folder('INBOX') === false) { echo 'select_folder() failed: ' . $imap_driver->error . '
'; return false; }
IMAPのより高度なコマンドのいくつかを実装する方法を見てみましょう。
SEARCH
コマンド電子メール分析の一般的なルーチンは、特定の日付範囲の電子メールを検索したり、フラグが立てられた電子メールを検索したりすることです。検索条件をSEARCH
に渡す必要がありますコマンドを引数として、スペースを区切り文字として使用します。たとえば、2015年11月20日以降にすべてのメールを取得する場合は、次のコマンドを渡す必要があります。
データの視覚化に最適なツール
00000005 SEARCH SINCE 20-Nov-2015
そして、応答は次のようになります。
* SEARCH 881 882 00000005 OK SEARCH completed
可能な検索用語の詳細なドキュメントを見つけることができます ここに SEARCH
の出力コマンドは、空白で区切られた電子メールのUIDのリストです。 UIDは、ユーザーのアカウント内のメールの一意の識別子であり、時系列で表示されます。1は最も古いメールです。 SEARCH
を実装するにはコマンドは、結果のUIDを返す必要があります。
class imap_driver { ... public function get_uids_by_search($criteria) { $this->command('SEARCH $criteria'); if (preg_match('~^OK~', $this->last_endline) && is_array($this->last_response) && count($this->last_response) == 1) { $splitted_response = explode(' ', $this->last_response[0]); $uids = array(); foreach ($splitted_response as $item) { if (preg_match('~^d+$~', $item)) { $uids[] = $item; // put the returned UIDs into an array } } return $uids; } else { $this->error = join(', ', $this->last_response); $this->close(); return false; } } ... }
このコマンドをテストするために、過去3日間のメールを受け取ります。
... // test for get_uids_by_search() $ids = $imap_driver->get_uids_by_search('SINCE ' . date('j-M-Y', time() - 60 * 60 * 24 * 3)); if ($ids === false) { echo 'get_uids_failed: ' . $imap_driver->error . '
'; exit; }
FETCH
BODY.PEEK
を使用したコマンドもう1つの一般的なタスクは、電子メールをSEEN
としてマークせずに電子メールヘッダーを取得することです。 IMAPマニュアルから、 電子メールの全部または一部を取得するためのコマンド FETCH
です。最初の引数は、関心のある部分を示し、通常はBODY
です。が渡されます。これにより、メッセージ全体とそのヘッダーが返され、SEEN
としてマークされます。代替引数BODY.PEEK
メッセージをSEEN
としてマークせずに、同じことを行います。
IMAP構文では、フェッチする電子メールのセクション(この例では[HEADER]
)も角括弧内に指定する必要があります。その結果、コマンドは次のようになります。
00000006 FETCH 2 BODY.PEEK[HEADER]
そして、次のような応答が期待されます。
* 2 FETCH (BODY[HEADER] {438} MIME-Version: 1.0 x-no-auto-attachment: 1 Received: by 10.170.97.214; Fri, 30 May 2014 09:13:45 -0700 (PDT) Date: Fri, 30 May 2014 09:13:45 -0700 Message-ID: < [email protected] om> Subject: The best of Gmail, wherever you are From: Gmail Team < [email protected] > To: Example Test < [email protected] > Content-Type: multipart/alternative; boundary=001a1139e3966e26ed04faa054f4 ) 00000006 OK Success
ヘッダーをフェッチするための関数を作成するには、ハッシュ構造(キーと値のペア)で応答を返すことができる必要があります。
class imap_driver { ... public function get_headers_from_uid($uid) { $this->command('FETCH $uid BODY.PEEK[HEADER]'); if (preg_match('~^OK~', $this->last_endline)) { array_shift($this->last_response); // skip the first line $headers = array(); $prev_match = ''; foreach ($this->last_response as $item) { if (preg_match('~^([a-z][a-z0-9-_]+):~is', $item, $match)) { $header_name = strtolower($match[1]); $prev_match = $header_name; $headers[$header_name] = trim(substr($item, strlen($header_name) + 1)); } else { $headers[$prev_match] .= ' ' . $item; } } return $headers; } else { $this->error = join(', ', $this->last_response); $this->close(); return false; } } ... }
そして、このコードをテストするために、関心のあるメッセージのUIDを指定するだけです。
... // test for get_headers_by_uid if (($headers = $imap_driver->get_headers_from_uid(2)) === false) { echo 'get_headers_by_uid() failed: ' . $imap_driver->error . '
'; return false; }
Gmailには、私たちの生活をはるかに楽にする特別なコマンドのリストが用意されています。 GmailのIMAP拡張コマンドのリストが利用可能です ここに 。私の意見では、最も重要なコマンドを確認しましょう。 X-GM-RAW
。これにより、IMAPでGmail検索構文を使用できるようになります。たとえば、プライマリ、ソーシャル、プロモーション、更新、またはフォーラムのカテゴリにある電子メールを検索できます。
機能的には、X-GM-RAW
SEARCH
の拡張ですコマンドを使用して、上記のコードをSEARCH
に再利用できます。コマンド。キーワードX-GM-RAW
を追加するだけです。および基準:
... // test for gmail extended search functionality $ids = $imap_driver->get_uids_by_search(' X-GM-RAW 'category:primary''); if ($ids === false) { echo 'get_uids_failed: ' . $imap_driver->error . '
'; return false; }
上記のコードは、「プライマリ」カテゴリにリストされているすべてのUIDを返します。
注:2015年12月の時点で、Gmailは一部のアカウントで「プライマリ」カテゴリと「更新」カテゴリを混同することがよくあります。これはGmailのバグで、まだ修正されていません。
全体として、カスタムソケットアプローチは開発者により多くの自由を提供します。ですべてのコマンドを実装することが可能になります IMAP RFC3501 。また、「舞台裏」で何が起こっているのか不思議に思う必要がないため、コードをより適切に制御できます。
完全なimap_driver
この記事で実装したクラスは ここに 。そのまま使用でき、開発者が新しい関数を記述したり、IMAPサーバーにリクエストしたりするのに数分しかかかりません。また、詳細な出力のために、クラスにデバッグ機能を含めました。