PHP로 IMAP 이메일 클라이언트 구축

게시 됨: 2022-03-11

개발자는 때때로 전자 메일 사서함에 액세스해야 하는 작업에 직면합니다. 대부분의 경우 이 작업은 인터넷 메시지 액세스 프로토콜(IMAP)을 사용하여 수행됩니다. PHP 개발자로서 나는 처음에 PHP의 내장 IMAP 라이브러리로 눈을 돌렸지만 이 라이브러리는 버그가 있어 디버그하거나 수정할 수 없습니다. 또한 프로토콜의 기능을 최대한 활용하기 위해 IMAP 명령을 사용자 정의할 수 없습니다.

그래서 오늘 우리는 PHP를 사용하여 처음부터 작동하는 IMAP 이메일 클라이언트를 만들 것입니다. Gmail의 특수 명령을 사용하는 방법도 알아보겠습니다.

사용자 정의 클래스인 imap_driver 에서 IMAP를 구현합니다. 클래스를 구축하는 동안 각 단계를 설명합니다. 기사 끝에서 전체 imap_driver.php 를 다운로드할 수 있습니다.

연결 설정

IMAP은 연결 기반 프로토콜이며 일반적으로 SSL 보안이 있는 TCP/IP를 통해 작동하므로 IMAP 호출을 하기 전에 연결을 열어야 합니다.

연결하려는 IMAP 서버의 URL과 포트 번호를 알아야 합니다. 이 정보는 일반적으로 서비스의 웹사이트나 문서에 광고됩니다. 예를 들어 Gmail의 경우 URL은 포트 993의 ssl://imap.gmail.com 입니다.

초기화가 성공했는지 알고 싶기 때문에 클래스 생성자를 비워두고 모든 연결은 사용자 지정 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 . "\n"; exit; }

기본 IMAP 구문

이제 IMAP 서버에 활성 소켓이 열려 있으므로 IMAP 명령 보내기를 시작할 수 있습니다. IMAP 구문을 살펴보겠습니다.

공식 문서는 IETF(Internet Engineering Task Force) 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\r\n"); // 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 . "\n"; exit; }

Gmail은 기본적으로 보안에 대해 매우 엄격합니다. 기본 설정이 있고 계정 프로필의 국가가 아닌 다른 국가에서 액세스를 시도하는 경우 IMAP으로 이메일 계정에 액세스하는 것을 허용하지 않습니다. 그러나 고칠 수 있을 만큼 쉽습니다. 여기에 설명된 대로 Gmail 계정에서 덜 안전한 설정을 지정하면 됩니다.

SELECT 명령

이제 이메일로 유용한 작업을 수행하기 위해 IMAP 폴더를 선택하는 방법을 살펴보겠습니다. command() 메서드 덕분에 구문은 LOGIN 과 유사합니다. 대신 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 . "\n"; 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('jM-Y', time() - 60 * 60 * 24 * 3)); if ($ids === false) { echo "get_uids_failed: " . $imap_driver->error . "\n"; exit; }

BODY.PEEK 를 사용한 FETCH 명령

또 다른 일반적인 작업은 이메일을 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: <CACYy8gU+UFFukbE0Cih8kYRENMXcx1DTVhvg3TBbJ52D8OF6nQ@mail.gmail.com> 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('~^([az][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 . "\n"; return false; }

Gmail IMAP 확장 프로그램

Gmail은 우리의 삶을 훨씬 더 쉽게 만들어 줄 특별한 명령 목록을 제공합니다. Gmail의 IMAP 확장 명령 목록은 여기에서 확인할 수 있습니다. 제 생각에 가장 중요한 명령인 X-GM-RAW 를 검토해 보겠습니다. IMAP과 함께 Gmail 검색 구문을 사용할 수 있습니다. 예를 들어 기본, 소셜, 프로모션, 업데이트 또는 포럼 카테고리에 있는 이메일을 검색할 수 있습니다.

기능적으로 X-GM-RAWSEARCH 명령의 확장이므로 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 . "\n"; return false; }

위의 코드는 "기본" 범주에 나열된 모든 UID를 반환합니다.

참고: 2015년 12월부터 Gmail은 일부 계정에서 '기본' 카테고리를 '업데이트' 카테고리와 혼동하는 경우가 많습니다. 이것은 아직 수정되지 않은 Gmail 버그입니다.

결론

메일이 도착하였습니다. 이제 뭐? PHP로 사용자 정의 IMAP 이메일 클라이언트를 구축하는 방법을 읽고 메일을 확인하십시오.
트위터

전반적으로 사용자 지정 소켓 접근 방식은 개발자에게 더 많은 자유를 제공합니다. IMAP RFC3501에서 모든 명령을 구현할 수 있습니다. 또한 "뒤에서" 무슨 일이 일어나고 있는지 궁금해할 필요가 없기 때문에 코드를 더 잘 제어할 수 있습니다.

이 기사에서 구현한 전체 imap_driver 클래스는 여기에서 찾을 수 있습니다. 그대로 사용할 수 있으며 개발자가 IMAP 서버에 새 기능을 작성하거나 요청하는 데 몇 분 밖에 걸리지 않습니다. 또한 자세한 출력을 위해 클래스에 디버그 기능을 포함했습니다.