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