Completed
Push — master ( a92383...9ab88c )
by Christoph
11s
created

Mailbox::getFetchIds()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 19
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 20

Importance

Changes 2
Bugs 0 Features 0
Metric Value
c 2
b 0
f 0
dl 0
loc 19
ccs 0
cts 15
cp 0
rs 9.2
cc 4
eloc 13
nc 4
nop 2
crap 20
1
<?php
2
/**
3
 * @author Christoph Wurst <[email protected]>
4
 * @author Clement Wong <[email protected]>
5
 * @author Jan-Christoph Borchardt <[email protected]>
6
 * @author Lukas Reschke <[email protected]>
7
 * @author matiasdelellis <[email protected]>
8
 * @author Robin McCorkell <[email protected]>
9
 * @author Scrutinizer Auto-Fixer <[email protected]>
10
 * @author Thomas Imbreckx <[email protected]>
11
 * @author Thomas I <[email protected]>
12
 * @author Thomas Mueller <[email protected]>
13
 * @author Thomas Müller <[email protected]>
14
 *
15
 * ownCloud - Mail
16
 *
17
 * This code is free software: you can redistribute it and/or modify
18
 * it under the terms of the GNU Affero General Public License, version 3,
19
 * as published by the Free Software Foundation.
20
 *
21
 * This program is distributed in the hope that it will be useful,
22
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
23
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
24
 * GNU Affero General Public License for more details.
25
 *
26
 * You should have received a copy of the GNU Affero General Public License, version 3,
27
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
28
 *
29
 */
30
31
namespace OCA\Mail;
32
33
use Horde_Imap_Client;
34
use Horde_Imap_Client_Exception;
35
use Horde_Imap_Client_Fetch_Query;
36
use Horde_Imap_Client_Ids;
37
use Horde_Imap_Client_Mailbox;
38
use Horde_Imap_Client_Search_Query;
39
use Horde_Imap_Client_Socket;
40
use OCA\Mail\Model\IMAPMessage;
41
use OCA\Mail\Service\IMailBox;
42
43
class Mailbox implements IMailBox {
44
45
	/**
46
	 * @var Horde_Imap_Client_Socket
47
	 */
48
	protected $conn;
49
50
	/**
51
	 * @var array
52
	 */
53
	private $attributes;
54
55
	/**
56
	 * @var string
57
	 */
58
	private $specialRole;
59
60
	/**
61
	 * @var string
62
	 */
63
	private $displayName;
64
65
	/**
66
	 * @var string
67
	 */
68
	private $delimiter;
69
70
	/**
71
	 * @var Horde_Imap_Client_Mailbox
72
	 */
73
	protected $mailBox;
74
75
	/**
76
	 * @param Horde_Imap_Client_Socket $conn
77
	 * @param Horde_Imap_Client_Mailbox $mailBox
78
	 * @param array $attributes
79
	 * @param string $delimiter
80
	 */
81 12
	public function __construct($conn, $mailBox, $attributes, $delimiter='/') {
82 12
		$this->conn = $conn;
83 12
		$this->mailBox = $mailBox;
84 12
		$this->attributes = $attributes;
85 12
		$this->delimiter = $delimiter;
86 12
		$this->getSpecialRoleFromAttributes();
87 12
		if ($this->specialRole === null) {
88 12
			$this->guessSpecialRole();
89 12
		}
90 12
		$this->makeDisplayName();
91 12
	}
92
93 6
	private function getSearchIds($from, $count, $filter) {
94 6
		if ($filter instanceof Horde_Imap_Client_Search_Query) {
0 ignored issues
show
Bug introduced by
The class Horde_Imap_Client_Search_Query does not exist. Is this class maybe located in a folder that is not analyzed, or in a newer version of your dependencies than listed in your composer.lock/composer.json?
Loading history...
95 3
			$query = $filter;
96 3
		} else {
97 3
			$query = new Horde_Imap_Client_Search_Query();
98 3
			if ($filter) {
99
				$query->text($filter, false);
100
			}
101
		}
102 6
		if ($this->getSpecialRole() !== 'trash') {
103 6
			$query->flag(Horde_Imap_Client::FLAG_DELETED, false);
104 6
		}
105 6
		$result = $this->conn->search($this->mailBox, $query, ['sort' => [Horde_Imap_Client::SORT_DATE]]);
106 6
		$ids = array_reverse($result['match']->ids);
107 6
		if ($from >= 0 && $count >= 0) {
108 3
			$ids = array_slice($ids, $from, $count);
109 3
		}
110 6
		return new \Horde_Imap_Client_Ids($ids, false);
111
	}
112
113
	private function getFetchIds($from, $count) {
114
		$q = new Horde_Imap_Client_Fetch_Query();
115
		$q->uid();
116
		$q->imapDate();
117
118
		$result = $this->conn->fetch($this->mailBox, $q);
119
		$uidMap = [];
120
		foreach ($result as $r) {
121
			$uidMap[$r->getUid()] = $r->getImapDate()->getTimeStamp();
122
		}
123
		// sort by time
124
		uasort($uidMap, function($a, $b) {
125
			return $a < $b;
126
		});
127
		if ($from >= 0 && $count >= 0) {
128
			$uidMap = array_slice($uidMap, $from, $count, true);
129
		}
130
		return new \Horde_Imap_Client_Ids(array_keys($uidMap), false);
131
	}
132
133 6
	public function getMessages($from = 0, $count = 2, $filter = '') {
134 6
		if (!$this->conn->capability->query('SORT') && (is_null($filter) || $filter === '')) {
135
			$ids = $this->getFetchIds($from, $count);
136
		} else {
137 6
			$ids = $this->getSearchIds($from, $count, $filter);
138
		}
139
140 6
		$headers = [];
141
142 6
		$fetch_query = new Horde_Imap_Client_Fetch_Query();
143 6
		$fetch_query->envelope();
144 6
		$fetch_query->flags();
145 6
		$fetch_query->size();
146 6
		$fetch_query->uid();
147 6
		$fetch_query->imapDate();
148 6
		$fetch_query->structure();
149
150 6
		$headers = array_merge($headers, [
151 6
			'importance',
152 6
			'list-post',
153
			'x-priority'
154 6
		]);
155 6
		$headers[] = 'content-type';
156
157 6
		$fetch_query->headers('imp', $headers, [
158 6
			'cache' => true,
159
			'peek'  => true
160 6
		]);
161
162 6
		$options = ['ids' => $ids];
163
		// $list is an array of Horde_Imap_Client_Data_Fetch objects.
164 6
		$headers = $this->conn->fetch($this->mailBox, $fetch_query, $options);
165
166 6
		ob_start(); // fix for Horde warnings
167 6
		$messages = [];
168 6
		foreach ($headers->ids() as $message_id) {
169 6
			$header = $headers[$message_id];
170 6
			$message = new IMAPMessage($this->conn, $this->mailBox, $message_id, $header);
171 6
			$messages[] = $message->getListArray();
172 6
		}
173 6
		ob_get_clean();
174
175
		// sort by time
176
		usort($messages, function($a, $b) {
177
			return $a['dateInt'] < $b['dateInt'];
178 6
		});
179
180 6
		return $messages;
181
	}
182
183
	/**
184
	 * @return array
185
	 */
186 3
	public function attributes() {
187 3
		return $this->attributes;
188
	}
189
190
	/**
191
	 * @param string $messageId
192
	 * @param bool $loadHtmlMessageBody
193
	 * @return IMAPMessage
194
	 */
195
	public function getMessage($messageId, $loadHtmlMessageBody = false) {
196
		return new IMAPMessage($this->conn, $this->mailBox, $messageId, null, $loadHtmlMessageBody);
0 ignored issues
show
Bug Best Practice introduced by
The return type of return new \OCA\Mail\Mod... $loadHtmlMessageBody); (OCA\Mail\Model\IMAPMessage) is incompatible with the return type declared by the interface OCA\Mail\Service\IMailBox::getMessage of type OCA\Mail\Message.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
197
	}
198
199
	/**
200
	 * @param int $flags
201
	 * @return array
202
	 */
203 12
	public function getStatus($flags = \Horde_Imap_Client::STATUS_ALL) {
204 12
		return $this->conn->status($this->mailBox, $flags);
205
	}
206
207
	/**
208
	 * @return int
209
	 */
210 3
	public function getTotalMessages() {
211 3
		$status = $this->getStatus(\Horde_Imap_Client::STATUS_MESSAGES);
212 3
		return (int) $status['messages'];
213
	}
214
215 12
	protected function makeDisplayName() {
216 12
		$parts = explode($this->delimiter, $this->mailBox->utf8, 2);
217
218 12
		if (count($parts) > 1) {
219
			$displayName = $parts[1];
220 12
		} elseif (strtolower($this->mailBox->utf8) === 'inbox') {
221 6
			$displayName = 'Inbox';
222 6
		} else {
223 12
			$displayName = $this->mailBox->utf8;
224
		}
225
226 12
		$this->displayName = $displayName;
227 12
	}
228
229 13
	public function getFolderId() {
230 13
		return $this->mailBox->utf8;
231
	}
232
233
	/**
234
	 * @return string
235
	 */
236 6
	public function getParent() {
237 6
		$folderId = $this->getFolderId();
238 6
		$parts = explode($this->delimiter, $folderId, 2);
239
240 6
		if (count($parts) > 1) {
241
			return $parts[0];
242
		}
243
244 6
		return null;
245
	}
246
247
	/**
248
	 * @return string
249
	 */
250 9
	public function getSpecialRole() {
251 9
		return $this->specialRole;
252
	}
253
254
	/**
255
	 * @return string
256
	 */
257 6
	public function getDisplayName() {
258 6
		return $this->displayName;
259
	}
260
261
	/**
262
	 * @param string $displayName
263
	 */
264 6
	public function setDisplayName($displayName) {
265 6
		$this->displayName = $displayName;
266 6
	}
267
268
	/**
269
	 * @param integer $accountId
270
	 * @return array
271
	 */
272 6
	public function getListArray($accountId, $status = null) {
273 6
		$displayName = $this->getDisplayName();
274
		try {
275 6
			if (is_null($status)) {
276 3
				$status = $this->getStatus();
277 3
			}
278 6
			$total = $status['messages'];
279 6
			$specialRole = $this->getSpecialRole();
280 6
			$unseen = ($specialRole === 'trash') ? 0 : $status['unseen'];
281 6
			$isEmpty = ($total === 0);
282 6
			$noSelect = in_array('\\noselect', $this->attributes);
283 6
			$parentId = $this->getParent();
284 6
			$parentId = ($parentId !== null) ? base64_encode($parentId) : null;
285
			return [
286 6
				'id' => base64_encode($this->getFolderId()),
287 6
				'parent' => $parentId,
288 6
				'name' => $displayName,
289 6
				'specialRole' => $specialRole,
290 6
				'unseen' => $unseen,
291 6
				'total' => $total,
292 6
				'isEmpty' => $isEmpty,
293 6
				'accountId' => $accountId,
294 6
				'noSelect' => $noSelect,
295 6
				'uidvalidity' => $status['uidvalidity'],
296 6
				'uidnext' => $status['uidnext'],
297 6
				'delimiter' => $this->delimiter
298 6
			];
299
		} catch (Horde_Imap_Client_Exception $e) {
0 ignored issues
show
Bug introduced by
The class Horde_Imap_Client_Exception does not exist. Is this class maybe located in a folder that is not analyzed, or in a newer version of your dependencies than listed in your composer.lock/composer.json?
Loading history...
300
			return [
301
				'id' => base64_encode($this->getFolderId()),
302
				'parent' => null,
303
				'name' => $displayName,
304
				'specialRole' => null,
305
				'unseen' => 0,
306
				'total' => 0,
307
				'error' => $e->getMessage(),
308
				'isEmpty' => true,
309
				'accountId' => $accountId,
310
				'noSelect' => true
311
			];
312
		}
313
	}
314
	/**
315
	 * Get the special use role of the mailbox
316
	 *
317
	 * This method reads the attributes sent by the server
318
	 *
319
	 */
320 12
	protected function getSpecialRoleFromAttributes() {
321
		/*
322
		 * @todo: support multiple attributes on same folder
323
		 * "any given server or  message store may support
324
		 *  any combination of the attributes"
325
		 *  https://tools.ietf.org/html/rfc6154
326
		 */
327 12
		$result = null;
328 12
		if (is_array($this->attributes)) {
329
			/* Convert attributes to lowercase, because gmail
330
			 * returns them as lowercase (eg. \trash and not \Trash)
331
			 */
332
			$specialUseAttributes = [
333 12
				strtolower(Horde_Imap_Client::SPECIALUSE_ALL),
334 12
				strtolower(Horde_Imap_Client::SPECIALUSE_ARCHIVE),
335 12
				strtolower(Horde_Imap_Client::SPECIALUSE_DRAFTS),
336 12
				strtolower(Horde_Imap_Client::SPECIALUSE_FLAGGED),
337 12
				strtolower(Horde_Imap_Client::SPECIALUSE_JUNK),
338 12
				strtolower(Horde_Imap_Client::SPECIALUSE_SENT),
339 12
				strtolower(Horde_Imap_Client::SPECIALUSE_TRASH)
340 12
			];
341
342 12
			$attributes = array_map(function($n) {
343 6
				return strtolower($n);
344 12
			}, $this->attributes);
345
346 12
			foreach ($specialUseAttributes as $attr)  {
347 12
				if (in_array($attr, $attributes)) {
348 6
					$result = ltrim($attr, '\\');
349 6
					break;
350
				}
351 12
			}
352
353 12
		}
354
355 12
		$this->specialRole = $result;
356 12
	}
357
358
	/**
359
	 * Assign a special role to this mailbox based on its name
360
	 */
361 12
	protected function guessSpecialRole() {
362
363
		$specialFoldersDict = [
364 12
			'inbox'   => ['inbox'],
365 12
			'sent'    => ['sent', 'sent items', 'sent messages', 'sent-mail', 'sentmail'],
366 12
			'drafts'  => ['draft', 'drafts'],
367 12
			'archive' => ['archive', 'archives'],
368 12
			'trash'   => ['deleted messages', 'trash'],
369 12
			'junk'    => ['junk', 'spam', 'bulk mail'],
370 12
		];
371
372 12
		$lowercaseExplode = explode($this->delimiter, $this->getFolderId(), 2);
373 12
		$lowercaseId = strtolower(array_pop($lowercaseExplode));
374 12
		$result = null;
375 12
		foreach ($specialFoldersDict as $specialRole => $specialNames) {
376 12
			if (in_array($lowercaseId, $specialNames)) {
377 6
				$result = $specialRole;
378 6
				break;
379
			}
380 12
		}
381
382 12
		$this->specialRole = $result;
383 12
	}
384
385
	/**
386
	 * @param int $messageId
387
	 * @param string $attachmentId
388
	 * @return Attachment
389
	 */
390
	public function getAttachment($messageId, $attachmentId) {
391
		return new Attachment($this->conn, $this->mailBox, $messageId, $attachmentId);
392
	}
393
394
	/**
395
	 * @param string $rawBody
396
	 * @param array $flags
397
	 */
398 6
	public function saveMessage($rawBody, $flags = []) {
399
400 6
		$this->conn->append($this->mailBox, [
401
			[
402 6
				'data' => $rawBody,
403
				'flags' => $flags
404 6
			]
405 6
		]);
406 6
	}
407
408
	/**
409
	 * Save draft
410
	 *
411
	 * @param string $rawBody
412
	 * @return int UID of the saved draft
413
	 */
414
	public function saveDraft($rawBody) {
415
416
		$uids = $this->conn->append($this->mailBox, [
417
			[
418
				'data' => $rawBody,
419
				'flags' => [
420
					Horde_Imap_Client::FLAG_DRAFT,
421
					Horde_Imap_Client::FLAG_SEEN
422
				]
423
			]
424
		]);
425
		return $uids->current();
426
	}
427
428
	/**
429
	 * @param int $uid
430
	 * @param string $flag
431
	 * @param boolean $add
432
	 */
433
	public function setMessageFlag($uid, $flag, $add) {
434
		$options = [
435
			'ids' => new Horde_Imap_Client_Ids($uid)
436
		];
437
		if ($add) {
438
			$options['add'] = [$flag];
439
		} else {
440
			$options['remove'] = [$flag];
441
		}
442
		$this->conn->store($this->mailBox, $options);
443
	}
444
445
	/**
446
	 * @param $fromUid
447
	 * @param $toUid
448
	 * @return array
449
	 */
450 3
	public function getMessagesSince($fromUid, $toUid) {
451 3
		$query = new Horde_Imap_Client_Search_Query();
452 3
		$query->ids(new Horde_Imap_Client_Ids("$fromUid:$toUid"));
453 3
		return $this->getMessages(-1, -1, $query);
454
	}
455
456
	/**
457
	 * @return Horde_Imap_Client_Mailbox
458
	 */
459
	public function getHordeMailBox() {
460
		return $this->mailBox;
461
	}
462
463
}
464