1<?php 2// (c) Copyright by authors of the Tiki Wiki CMS Groupware Project 3// 4// All Rights Reserved. See copyright.txt for details and a complete list of authors. 5// Licensed under the GNU LESSER GENERAL PUBLIC LICENSE. See license.txt for details. 6// $Id$ 7 8namespace Tiki\MailIn\Source; 9 10use Tiki\MailIn\Exception\TransportException; 11use Zend\Mail\Header\ContentType; 12use Zend\Mail\Storage\Part; 13use Zend\Mail\Storage\Pop3 as ZendPop3; 14use Zend\Mail\Exception\ExceptionInterface as ZendMailException; 15 16class Pop3 implements SourceInterface 17{ 18 protected $host; 19 protected $port; 20 protected $username; 21 protected $password; 22 23 function __construct($host, $port, $username, $password) 24 { 25 $this->host = $host; 26 $this->port = (int) $port; 27 $this->username = $username; 28 $this->password = $password; 29 } 30 31 function test() 32 { 33 try { 34 $pop = $this->connect(); 35 $pop->close(); 36 37 return true; 38 } catch (TransportException $e) { 39 return false; 40 } 41 } 42 43 /** 44 * @return \Generator 45 * @throws TransportException 46 */ 47 function getMessages() 48 { 49 $pop = $this->connect(); 50 $toDelete = []; 51 52 foreach ($pop as $i => $source) { 53 /* @var $source \Zend\Mail\Storage\Message */ 54 $message = new Message($i, function () use ($i, & $toDelete) { 55 $toDelete[] = $i; 56 }); 57 $from = $source->from ?: $source->{'return-path'}; 58 if (! empty($source->{'message-id'})) { 59 $message->setMessageId(str_replace(['<', '>'], '', $source->{'message-id'})); 60 } 61 $message->setRawFrom($from); 62 $message->setSubject($source->subject); 63 $message->setRecipient($source->to); 64 $message->setHtmlBody($this->getBody($source, 'text/html')); 65 $message->setBody($this->getBody($source, 'text/plain')); 66 67 $this->handleAttachments($message, $source); 68 69 yield $message; 70 } 71 72 // Due to an issue in Zend_Mail_Storage, deletion must be done in reverse order 73 $toDelete = array_reverse($toDelete); 74 75 foreach ($toDelete as $i) { 76 $pop->removeMessage($i); 77 } 78 79 $pop->close(); 80 } 81 82 /** 83 * @return \Zend\Mail\Storage\Pop3 84 * @throws TransportException 85 */ 86 protected function connect() 87 { 88 try { 89 $pop = new ZendPop3([ 90 'host' => $this->host, 91 'port' => $this->port, 92 'user' => $this->username, 93 'password' => $this->password, 94 'ssl' => $this->port == 995, 95 ]); 96 97 return $pop; 98 } catch (ZendMailException $e) { 99 throw new TransportException(tr("Login failed for POP3 account on %0:%1 for user %2", $this->host, $this->password, $this->username)); 100 } 101 } 102 103 /** 104 * @param Part $part 105 * @param string $type 106 * @param string $return 107 * 108 * @return string 109 */ 110 private function getBody($part, $type, $return = '') 111 { 112 /** @var ContentType $contentType */ 113 $contentType = $part->getHeaders()->get('Content-Type'); 114 if (! $part->isMultipart() && (! $contentType || $contentType->getType() === $type)) { 115 $return .= $this->decode($part); 116 } 117 118 if ($part->isMultipart()) { 119 for ($i = 1; $i <= $part->countParts(); ++$i) { 120 $p = $part->getPart($i); 121 $ret = $this->getBody($p, $type, $return); 122 123 $pType = $p->getHeaders()->get('Content-Type'); 124 if ($contentType->getType() === 'multipart/mixed' && $type === 'text/html' && $pType && $pType->getType() === $type) { 125 // mainly to remove the html, head and body tags 126 $return .= strip_tags($ret, '<b><br><dd><div><dl><dt><em><h1><h2><h3><h4><h5><h6><hr><i><img><li><ol><p><s><span><strong><table><tr><td><u><ul>'); 127 // TODO work out how to insert inline file's id when using multipart/alternative 128 } else { 129 if ($ret) { 130 return $ret; 131 } 132 } 133 } 134 } 135 return $return; 136 } 137 138 /** 139 * @param $message Message 140 * @param $part Part 141 */ 142 private function handleAttachments($message, $part) 143 { 144 if ($part->isMultipart()) { 145 // check each part 146 for ($i = 1; $i <= $part->countParts(); ++$i) { 147 $p = $part->getPart($i); 148 if ($p->isMultipart()) { 149 $this->handleAttachments($message, $p); 150 } 151 $headers = $p->getHeaders()->toArray(); 152 153 // filter out any non-binary parts 154 if (! isset($headers['Content-Transfer-Encoding']) || $headers['Content-Transfer-Encoding'] !== 'base64') { 155 continue; 156 } 157 158 if (isset($headers['Content-Id'])) { 159 $contentId = $headers['Content-Id']; 160 $contentId = trim($contentId, '<>'); 161 } elseif (isset($headers['x-attachment-id'])) { 162 $contentId = $headers['x-attachment-id']; 163 } else { 164 $contentId = uniqid(); 165 } 166 $fileName = ''; 167 $fileType = ''; 168 $fileData = $this->decode($p); 169 $fileSize = mb_strlen($fileData, '8bit'); 170 171 if (isset($headers['Content-Type'])) { 172 $type = $headers['Content-Type']; 173 $pos = strpos($type, ';'); 174 if ($pos === false) { 175 $fileType = $type; 176 } else { 177 $fileType = substr($type, 0, $pos); 178 } 179 180 if (preg_match('/name="([^"]+)"/', $type, $parts)) { 181 $fileName = $parts[1]; 182 } 183 } 184 185 if (! $fileName && isset($headers['Content-Disposition'])) { 186 $dispo = $headers['Content-Disposition']; 187 if (preg_match('/name="([^"]+)"/', $dispo, $parts)) { 188 $fileName = $parts[1]; 189 } 190 } 191 192 $message->addAttachment($contentId, $fileName, $fileType, $fileSize, $fileData); 193 } 194 } 195 } 196 197 /** 198 * @param $part Part 199 * @return string 200 */ 201 private function decode($part) 202 { 203 $content = $part->getContent(); 204 if ($part->getHeaders()->get('Content-Transfer-Encoding')) { 205 switch ($part->getHeader('Content-Transfer-Encoding')->getFieldValue()) { 206 case 'base64': 207 $content = base64_decode($content); 208 break; 209 case 'quoted-printable': 210 $content = quoted_printable_decode($content); 211 break; 212 } 213 } 214 215 if ($part->getHeaders()->get('Content-Type')) { 216 if (preg_match('/charset="?iso-8859-1"?/i', $part->getHeader('Content-Type')->getFieldValue())) { 217 $content = utf8_encode($content); //convert to utf8 218 } 219 } 220 221 return $content; 222 } 223} 224