config = $config; } /** * @throws LoginException * * @return void */ public function connect(): void { $ssl = ''; if ($this->config->isSslEnabled()) { $ssl = 'ssl'; } $this->protocol = new Protocol( $this->config->getServer(), $this->config->getPort(), $ssl ); switch (strtolower($this->config->getAuthType())) { case ImapMailClientConfig::AUTH_BASIC: $this->protocol->login($this->config->getUser(), $this->config->getPassword()); break; case ImapMailClientConfig::AUTH_XOAUTH2: $this->loginOauth(); break; default: throw new LoginException( sprintf('Authentication method "%s" not supported', $this->config->getAuthType()) ); } $this->imap = new ImapClient($this->protocol); } /** * @return void */ public function disconnect(): void { if ($this->protocol === null) { return; } $this->protocol->logout(); } /** * @param string $criteria * * @throws InvalidArgumentException * @throws ClientConnectionException * * @return array */ public function searchMessages(string $criteria): array { $this->ensureConnection(); $criteriaArray = preg_split('/\s/', $criteria); $result = $this->protocol->search($criteriaArray); if ($result === null) { throw new InvalidArgumentException(sprintf('Invalid search criteria "%s".', $criteria)); } return $result; } /** * @param int $msgNumber * * @throws MessageNotFoundException * @throws ClientConnectionException * * @return MailMessageInterface */ public function fetchMessage(int $msgNumber): MailMessageInterface { $this->ensureConnection(); try { $message = $this->imap->getMessage($msgNumber); } catch (Exception $e) { throw new MessageNotFoundException( sprintf('Message number %s not found.', $msgNumber) ); } return $this->parseMessage($message); } /** * @param int $msgNumber * @param string $targetFolder * * @throws ClientConnectionException * * @return void */ public function copyMessage(int $msgNumber, string $targetFolder): void { $this->ensureConnection(); try { $this->imap->copyMessage($msgNumber, $targetFolder); } catch (Exception $e) { throw new ProtocolException( sprintf('Failed to copy Message "%s" to Folder "%s"', $msgNumber, $targetFolder), $e->getCode(), $e ); } } /** * @param int $msgNumber * * @throws ProtocolException * @throws ClientConnectionException * * @return void */ public function deleteMessage(int $msgNumber): void { $this->ensureConnection(); try { $this->imap->removeMessage($msgNumber); } catch (Exception $e) { throw new ProtocolException('Failed do delete Message.', $e->getCode(), $e); } } /** * @param string $inbox * * @throws FolderNotFoundException * @throws ClientConnectionException * * @return MailBoxInfoData */ public function examineInbox(string $inbox = null): MailBoxInfoData { $this->ensureConnection(); if ($inbox === null) { $inbox = $this->config->getInboxFolder(); } $status = $this->protocol->examine($inbox); if ($status === false) { throw new FolderNotFoundException( sprintf('Cannot examine "%s" - folder probably not existing.', $inbox) ); } return new MailBoxInfoData( (int)$status['exists'], (int)$status['recent'], (int)$status['uidvalidity'], $status['flags'][0] ); } /** * @throws ClientConnectionException * * @return bool */ public function expunge(): bool { $this->ensureConnection(); $result = $this->protocol->expunge(); return $result === true; } /** * Sets Flags on message. * * @param int $msgNumber * @param string[] $flags values: '\Seen' '\Answered' '\Flagged' '\Deleted' '\Draft' * * @throws ProtocolException * @throws ClientConnectionException * * @return void */ public function setFlags(int $msgNumber, array $flags): void { $this->ensureConnection(); try { $this->imap->setFlags($msgNumber, $flags); } catch (Exception $e) { throw new ProtocolException($e->getMessage(), $e->getCode(), $e); } } /** * @param string $folder * * @throws FolderNotFoundException * @throws ClientConnectionException * * @return void */ public function selectFolder(string $folder): void { $this->ensureConnection(); try { $this->imap->selectFolder($folder); } catch (Exception $e) { throw new FolderNotFoundException($e->getMessage(), $e->getCode(), $e); } } /** * @throws ProtocolException * @throws ClientConnectionException * * @return void */ public function noop(): void { $this->ensureConnection(); try { $this->imap->noop(); } catch (Exception $e) { throw new ProtocolException('NOOP Command Failed'); } } /** * @param string $message * @param string $targetFolder * * @throws ProtocolException * * @return void */ public function appendMessage(string $message, string $targetFolder): void { try { $this->imap->appendMessage($message, $targetFolder); } catch (Exception $e) { throw new ProtocolException('Failed to append message.', $e->getCode(), $e); } } /** * @throws OAuthException * * @return void */ private function loginOauth(): void { $authString = sprintf( "user=%s\1auth=Bearer %s\1\1", $this->config->getUser(), $this->config->getPassword() ); $authString = base64_encode($authString); $this->protocol->sendRequest('AUTHENTICATE', ['XOAUTH2', $authString]); while (true) { $response = ''; $isPlus = $this->protocol->readLine($response, '+', true); if ($isPlus) { $this->protocol->sendRequest(''); continue; } if (preg_match("/^OK /i", $response)) { return; } if (preg_match('/^NO (.+)/i', $response, $matches)) { throw new LoginException( sprintf('OAuth access denied: %s', $matches[1]) ); } if (preg_match('/^BAD (.+)/i', $response, $matches)) { throw new LoginException( sprintf('OAuth login error: %s', $matches[1]) ); } } } /** * @param Message $message * * @return MailMessageData */ private function parseMessage(Message $message): MailMessageData { /** @var From $from */ $from = $message->getHeader('From'); $list = $from->getAddressList(); $sender = new EmailRecipient($list->current()->getEmail(), $list->current()->getName()); try { $recipients = []; /** @var To $toHeader */ $toHeader = $message->getHeader('To'); foreach ($toHeader->getAddressList() as $recipient) { $recipients[] = new EmailRecipient($recipient->getEmail(), $recipient->getName()); } } catch (Exception $e) { $recipients = []; } try { /** @var Cc $ccHeader */ $ccHeader = $message->getHeader('Cc'); $ccs = []; foreach ($ccHeader->getAddressList() as $cc) { $ccs[] = new EmailRecipient($cc->getEmail(), $cc->getName()); } } catch (Exception $e) { $ccs = []; } $raw = $message->getContent(); $content = null; $parts = $this->parseMessagePartsRecursive($message); if (count($parts) === 0) { $content = $raw; } return new MailMessageData( $sender, $recipients, $ccs, $message->getFlags(), $this->parseHeaders($message->getHeaders()), $content, $this->parseMessagePartsRecursive($message), $raw ); } /** * @param Headers $headers * * @return array */ private function parseHeaders(?Headers $headers): array { if ($headers === null) { return []; } $headerArray = []; foreach ($headers as $header) { $key = strtolower($header->getFieldName()); $val = new MailMessageHeaderValue( $header->getFieldName(), $header->getFieldValue(), $header->getEncoding() ); $headerArray[$key] = $val; } return $headerArray; } /** * @param Part $message * * @return MailMessagePartInterface[] */ private function parseMessagePartsRecursive(Part $message): array { if ($message->countParts() === 0) { return []; } $parts = []; $partsCount = (int)$message->countParts(); for ($i = 1; $i <= $partsCount; $i++) { $part = $message->getPart($i); $headers = $this->parseHeaders($part->getHeaders()); $content = null; $subParts = $this->parseMessagePartsRecursive($part); if (count($subParts) === 0) { $content = $part->getContent(); } $parts[] = new MailMessagePartData( $headers, $content, $subParts ); } return $parts; } /** * @throws ClientConnectionException * * @return void */ private function ensureConnection(): void { if ($this->protocol === null || $this->imap === null) { throw new ClientConnectionException('IMAP client not connected.'); } } }