Skip to content

Instantly share code, notes, and snippets.

@BrianLeishman
Last active March 1, 2018 22:53
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save BrianLeishman/3fe5cacf29c30c7de84b64bdf82b3aeb to your computer and use it in GitHub Desktop.
Save BrianLeishman/3fe5cacf29c30c7de84b64bdf82b3aeb to your computer and use it in GitHub Desktop.
Pure PHP, basic IMAP implementation
<?php
/*
* Copyright (c) 2018, brian
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
require_once "$_SERVER[DOCUMENT_ROOT]/superheader.php";
class imap {
private $Socket;
public $Debug;
public $Folder;
public $Log = '';
private $Passwords = [];
private $ConnectionInformation;
public function __construct(string $Host, int $Port, bool $SSL, string $Username, string $Password, string $Folder = null, bool $Debug = null) {
$this->ConnectionInformation = func_get_args();
$this->Debug = $this->Debug ?? $Debug ?? false;
$this->Folder = $Folder;
$HasSSL = strtolower(substr($Host, 0, 6)) === 'ssl://';
if ($SSL && !$HasSSL) {
$Host = "ssl://$Host";
} else if (!$SSL && $HasSSL) {
$Host = substr($Host, 6);
}
$this->Passwords[] = $Password;
Retry(function() use($Host, $Port, $SSL, $Username, $Password, $Folder) {
if (!($this->Socket = fsockopen($Host, $Port, $ErrorCode, $ErrorString, 60))) {
throw new Exception("Failed to connect to the IMAP server {$ErrorString}", $ErrorCode);
}
if (!stream_set_timeout($this->Socket, 600)) {
throw new Exception("Could not set IMAP timeout");
}
$this->LogEntry("Opened connection with $Host:$Port\n");
$this->exec('LOGIN "' . addcslashes($Username, '\"') . '" "' . addcslashes($Password, '\"') . '"', null, false);
}, 20, [$this, 'LogError'], null, [$this, 'close'], null, false);
if (!strempty($Folder)) {
$this->SelectFolder($Folder);
}
}
public function SelectFolder(string $Folder): void {
$this->Folder = $Folder;
$this->exec('EXAMINE "' . addcslashes($Folder, '\"') . '"');
}
public function GetUIDs(string $Search = 'ALL'): array {
return Retry(function() use($Search): array {
$UIDs = null;
$this->exec("UID SEARCH $Search", function($Line) use(&$UIDs) {
if (!is_array($UIDs) && strtok($Line, $t = " \r\n") === '*' && strtok($t) === 'SEARCH') {
$UIDs = [];
while (($UID = strtok($t)) !== false) {
$UID = (int) $UID;
if ($UID !== 0) {
$UIDs[] = (int) $UID;
} else {
throw new Exception('UID cannot be less than or equal to zero');
}
}
}
unset($Line);
strtok('', '');
return true;
}, false);
if (is_array($UIDs)) {
return $UIDs;
} else {
throw new Exception('UID result is empty');
}
}, 3, [$this, 'LogError'], null, [$this, 'reconnect'], [$this, 'close'], false);
}
public function GetOverviews($UIDs = '1:*'): array {
return Retry(function() use($UIDs): array {
$Overviews = [];
if (is_array_or_alike($UIDs)) {
$UIDs = implode(',', array_map('trim', $UIDs));
} else if (!can_be_string($UIDs)) {
throw new Exception('UIDs must be type string or array');
}
if (strempty($UIDs)) {
return [];
}
$Records = $this->ParseFetchResponse($this->exec("UID FETCH $UIDs ALL"));
foreach ($Records as $Record) {
if (!isset($Record['UID'])) {
throw new Exception('UID missing from record');
}
$Overviews[$Record['UID']] = $Record;
}
return $Overviews;
}, 3, [$this, 'LogError'], null, [$this, 'reconnect'], [$this, 'close'], false);
}
public function GetBodies($UIDs = '1:*'): array {
return Retry(function() use($UIDs): array {
$Bodies = [];
if (is_array_or_alike($UIDs)) {
$UIDs = implode(',', array_map('trim', $UIDs));
} else if (!can_be_string($UIDs)) {
throw new Exception('UIDs must be type string or array');
}
if (strempty($UIDs)) {
return [];
}
$Records = $this->ParseFetchResponse($this->exec("UID FETCH $UIDs BODY.PEEK[]"));
foreach ($Records as $Record) {
if (!isset($Record['UID']) || !isset($Record['BODY[]'])) {
throw new Exception('UID/Body missing from record');
}
$Bodies[$Record['UID']] = $Record['BODY[]'];
}
return $Bodies;
}, 3, [$this, 'LogError'], null, [$this, 'reconnect'], [$this, 'close'], false);
}
public function exec(string $Command, callable $ProcessLine = null, bool $Retry = true): ?string {
$Execute = function() use($Command, $ProcessLine): ?string {
global $IMAP_EXEC_DISABLE_ENDLINE_CHECK;
$IMAP_EXEC_DISABLE_ENDLINE_CHECK = $IMAP_EXEC_DISABLE_ENDLINE_CHECK ?? false;
$Tag = strtoupper(bin2hex(bid2())) . ' ';
$Length = strlen($Tag);
$Response = '';
$Skip = 0;
$Success = false;
$Command = "$Tag$Command\r\n";
$this->LogEntry($Command);
if (is_resource($this->Socket)) {
fwrite($this->Socket, $Command);
while ($Line = fgets($this->Socket)) {
$this->LogEntry($Line);
if ($Skip > 0) {
$Skip--;
continue;
} else if ($Skip < 0) {
$Skip = 0;
}
if (!$IMAP_EXEC_DISABLE_ENDLINE_CHECK && substr($Line, 0, $Length) === $Tag) {
if (substr($Line, $Length, 3) === 'OK ') {
$Success = true;
break;
} else {
throw new Exception("IMAP command failed");
}
}
if ($ProcessLine !== null) {
$Returned = $ProcessLine($Line, $Response, $Skip);
if ($Returned === true) {
continue;
} else if ($Returned === false) {
break;
}
}
$Response .= $Line;
}
}
if (!$Success) {
throw new Exception("IMAP unexpected end of response");
}
return $Response;
};
if ($Retry) {
return Retry($Execute, 3, [$this, 'LogError'], null, [$this, 'reconnect'], [$this, 'close'], false);
} else {
return $Execute();
}
}
public static function ParseFetchResponse(string $s): array {
$c = str_split($s);
$l = count($c);
$Records = [];
for ($i = 0; $i < $l; $i++) {
if ($c === "\r" || $c === "\n") {
continue;
}
$Line = substr($s, $i, min(strpos($s, "\r", $i + 1), strpos($s, "\n", $i + 1)) - $i);
if (strtok($Line, $t = " \r\n") === '*' && ctype_digit(strtok($t)) && strtok($t) === 'FETCH') {
$i += strpos($Line, '(') + 1;
$Record = [];
$Pointers[$d = 0] = &$Record;
for (; $i < $l; $i++) {
unset($t);
switch (true) {
case $c[$i] === '"':
$b = $i++;
while ($i < $l && $c[$i] !== '"') {
if ($c[$i] === '\\') {
$i++;
}
$i++;
}
$t = substr($s, $b, $i + 1 - $b);
break;
case $c[$i] === '{':
$b = $i++;
while ($i < $l && $s[$i] !== '}') {
if (!ctype_digit($s[$i])) {
throw new Exception("Unexpected character '$c[$i]' in size token");
}
$i++;
}
$Size = substr($s, $b + 1, $i++ - 1 - $b);
$i++;
if ($c[$i] === "\r" || $c[$i] === "\n") {
$i++;
}
$t = substr($s, $i, $Size);
$i += $Size - 1;
break;
case $c[$i] === '\\' || ctype_alnum($c[$i]):
$b = $i++;
while ($i < $l && (ctype_alnum($c[$i]) || $c[$i] === '.' || $c[$i] === '[' || $c[$i] === ']')) {
$i++;
}
$t = substr($s, $b, $i-- - $b);
break;
case $c[$i] === '(':
$Pointers[++$d] = &$Pointers[$d - 1][];
break;
case $c[$i] === ')':
unset($Pointers[$d--]);
break;
case ctype_space($c[$i]):
break;
default:
throw new Exception("IMAP fetch parse failed; unexpected '$c[$i]'");
}
if ($d < 0) {
$Records[] = self::AssociateValues($Record);
break;
} else if (isset($t)) {
if (substr($t, 0, 1) === '"') {
$t = stripslashes(substr($t, 1, -1));
} else if ($t === 'NIL') {
$t = null;
}
$Pointers[$d][] = $t;
}
}
} else {
$i += strlen($Line);
}
}
return $Records;
}
public static function AssociateValues(array $Array): array {
if (($Count = count($Array)) & 1 === 1) {
throw new Exception("Array must be even to associate its values\n\n" . DebugArray($Array, 2));
}
$NewArray = [];
$ArrayKeys = array_keys($Array);
for ($i = 0; $i < $Count - 1; $i += 2) {
$NewArray[$Array[$ArrayKeys[$i]]] = $Array[$ArrayKeys[$i + 1]];
}
return $NewArray;
}
public function LogError(string $Message, int $Code): void {
global $argv;
LogAction('<h4>Request</h4><pre>' . DebugArray($_REQUEST ?? [], 3) . '</pre><h4>Arguments</h4><pre>' . DebugArray($argv ?? [], 3) . '</pre>' . nl2br(html("$Code: $Message\n\n")) . '<pre>' . html($this->Log) . '</pre>', 3, rtrim(strtok($Message, "\n")));
exit();
}
public function LogEntry(string $Entry): void {
$Entry = str_replace($this->Passwords, '***', $Entry);
$this->Log .= $Entry;
if ($this->Debug) {
echo $Entry;
}
}
public function reconnect(): void {
$this->close();
$this->ConnectionInformation = array_slice($this->ConnectionInformation, 0, 5);
$this->ConnectionInformation[] = $this->Folder ?? null;
$this->__construct(...$this->ConnectionInformation);
}
public function close(): void {
if (is_resource($this->Socket)) {
fclose($this->Socket);
$this->LogEntry("Closed connection\n" . str_repeat('-', 80) . "\n");
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment