Completed
Pull Request — master (#1486)
by Christoph
16:42
created

Mailbox::getSearchIds()   B

Complexity

Conditions 6
Paths 12

Size

Total Lines 19
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 7.5821

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 19
ccs 11
cts 17
cp 0.6471
rs 8.8571
cc 6
eloc 14
nc 12
nop 3
crap 7.5821
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 4
	private function getSearchIds($from, $count, $filter) {
94 4
		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
			$query = new Horde_Imap_Client_Search_Query();
98
			if ($filter) {
99
				$query->text($filter, false);
100
			}
101
		}
102 3
		if ($this->getSpecialRole() !== 'trash') {
103 3
			$query->flag(Horde_Imap_Client::FLAG_DELETED, false);
104 3
		}
105 3
		$result = $this->conn->search($this->mailBox, $query, ['sort' => [Horde_Imap_Client::SORT_DATE]]);
106 3
		$ids = array_reverse($result['match']->ids);
107 3
		if ($from >= 0 && $count >= 0) {
108
			$ids = array_slice($ids, $from, $count);
109
		}
110 3
		return new \Horde_Imap_Client_Ids($ids, false);
111
	}
112
113 3
	private function getFetchIds($from, $count) {
114 3
		$q = new Horde_Imap_Client_Fetch_Query();
115 3
		$q->uid();
116 3
		$q->imapDate();
117
		// FIXME: $q->headers('DATE', ['DATE']); could be a better option than the INTERNALDATE
118
119 3
		$result = $this->conn->fetch($this->mailBox, $q);
120 3
		$uidMap = [];
121 3
		foreach ($result as $r) {
122 3
			$uidMap[$r->getUid()] = $r->getImapDate()->getTimeStamp();
123 3
		}
124
		// sort by time
125
		uasort($uidMap, function($a, $b) {
126
			return $a < $b;
127 3
		});
128 3
		if ($from >= 0 && $count >= 0) {
129 3
			$uidMap = array_slice($uidMap, $from, $count, true);
130 3
		}
131 3
		return new \Horde_Imap_Client_Ids(array_keys($uidMap), false);
132
	}
133
134 6
	public function getMessages($from = 0, $count = 2, $filter = '') {
135 6
		if (is_null($filter) || $filter === '') {
136 3
			$ids = $this->getFetchIds($from, $count);
137 3
		} else {
138 3
			$ids = $this->getSearchIds($from, $count, $filter);
139
		}
140
141 6
		$headers = [];
142
143 6
		$fetch_query = new Horde_Imap_Client_Fetch_Query();
144 6
		$fetch_query->envelope();
145 6
		$fetch_query->flags();
146 6
		$fetch_query->size();
147 6
		$fetch_query->uid();
148 6
		$fetch_query->imapDate();
149 6
		$fetch_query->structure();
150
151 6
		$headers = array_merge($headers, [
152 6
			'importance',
153 6
			'list-post',
154
			'x-priority'
155 6
		]);
156 6
		$headers[] = 'content-type';
157
158 6
		$fetch_query->headers('imp', $headers, [
159 6
			'cache' => true,
160
			'peek'  => true
161 6
		]);
162
163 6
		$options = ['ids' => $ids];
164
		// $list is an array of Horde_Imap_Client_Data_Fetch objects.
165 6
		$headers = $this->conn->fetch($this->mailBox, $fetch_query, $options);
166
167 6
		ob_start(); // fix for Horde warnings
168 6
		$messages = [];
169 6
		foreach ($headers->ids() as $message_id) {
170 6
			$header = $headers[$message_id];
171 6
			$message = new IMAPMessage($this->conn, $this->mailBox, $message_id, $header);
172 6
			$messages[] = $message->getListArray();
173 6
		}
174 6
		ob_get_clean();
175
176
		// sort by time
177
		usort($messages, function($a, $b) {
178
			return $a['dateInt'] < $b['dateInt'];
179 6
		});
180
181 6
		return $messages;
182
	}
183
184
	/**
185
	 * @return array
186
	 */
187 3
	public function attributes() {
188 3
		return $this->attributes;
189
	}
190
191
	/**
192
	 * @param string $messageId
193
	 * @param bool $loadHtmlMessageBody
194
	 * @return IMAPMessage
195
	 */
196
	public function getMessage($messageId, $loadHtmlMessageBody = false) {
197
		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...
198
	}
199
200
	/**
201
	 * @param int $flags
202
	 * @return array
203
	 */
204 12
	public function getStatus($flags = \Horde_Imap_Client::STATUS_ALL) {
205 12
		return $this->conn->status($this->mailBox, $flags);
206
	}
207
208
	/**
209
	 * @return int
210
	 */
211 3
	public function getTotalMessages() {
212 3
		$status = $this->getStatus(\Horde_Imap_Client::STATUS_MESSAGES);
213 3
		return (int) $status['messages'];
214
	}
215
216 12
	protected function makeDisplayName() {
217 12
		$parts = explode($this->delimiter, $this->mailBox->utf8, 2);
218
219 12
		if (count($parts) > 1) {
220
			$displayName = $parts[1];
221 12
		} elseif (strtolower($this->mailBox->utf8) === 'inbox') {
222 6
			$displayName = 'Inbox';
223 6
		} else {
224 12
			$displayName = $this->mailBox->utf8;
225
		}
226
227 12
		$this->displayName = $displayName;
228 12
	}
229
230 13
	public function getFolderId() {
231 13
		return $this->mailBox->utf8;
232
	}
233
234
	/**
235
	 * @return string
236
	 */
237 6
	public function getParent() {
238 6
		$folderId = $this->getFolderId();
239 6
		$parts = explode($this->delimiter, $folderId, 2);
240
241 6
		if (count($parts) > 1) {
242
			return $parts[0];
243
		}
244
245 6
		return null;
246
	}
247
248
	/**
249
	 * @return string
250
	 */
251 6
	public function getSpecialRole() {
252 6
		return $this->specialRole;
253
	}
254
255
	/**
256
	 * @return string
257
	 */
258 6
	public function getDisplayName() {
259 6
		return $this->displayName;
260
	}
261
262
	/**
263
	 * @param string $displayName
264
	 */
265 6
	public function setDisplayName($displayName) {
266 6
		$this->displayName = $displayName;
267 6
	}
268
269
	/**
270
	 * @param integer $accountId
271
	 * @return array
272
	 */
273 6
	public function getListArray($accountId, $status = null) {
274 6
		$displayName = $this->getDisplayName();
275
		try {
276 6
			if (is_null($status)) {
277 3
				$status = $this->getStatus();
278 3
			}
279 6
			$total = $status['messages'];
280 6
			$specialRole = $this->getSpecialRole();
281 6
			$unseen = ($specialRole === 'trash') ? 0 : $status['unseen'];
282 6
			$isEmpty = ($total === 0);
283 6
			$noSelect = in_array('\\noselect', $this->attributes);
284 6
			$parentId = $this->getParent();
285 6
			$parentId = ($parentId !== null) ? base64_encode($parentId) : null;
286
			return [
287 6
				'id' => base64_encode($this->getFolderId()),
288 6
				'parent' => $parentId,
289 6
				'name' => $displayName,
290 6
				'specialRole' => $specialRole,
291 6
				'unseen' => $unseen,
292 6
				'total' => $total,
293 6
				'isEmpty' => $isEmpty,
294 6
				'accountId' => $accountId,
295 6
				'noSelect' => $noSelect,
296 6
				'uidvalidity' => $status['uidvalidity'],
297 6
				'uidnext' => $status['uidnext'],
298 6
				'delimiter' => $this->delimiter
299 6
			];
300
		} 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...
301
			return [
302
				'id' => base64_encode($this->getFolderId()),
303
				'parent' => null,
304
				'name' => $displayName,
305
				'specialRole' => null,
306
				'unseen' => 0,
307
				'total' => 0,
308
				'error' => $e->getMessage(),
309
				'isEmpty' => true,
310
				'accountId' => $accountId,
311
				'noSelect' => true
312
			];
313
		}
314
	}
315
	/**
316
	 * Get the special use role of the mailbox
317
	 *
318
	 * This method reads the attributes sent by the server
319
	 *
320
	 */
321 12
	protected function getSpecialRoleFromAttributes() {
322
		/*
323
		 * @todo: support multiple attributes on same folder
324
		 * "any given server or  message store may support
325
		 *  any combination of the attributes"
326
		 *  https://tools.ietf.org/html/rfc6154
327
		 */
328 12
		$result = null;
329 12
		if (is_array($this->attributes)) {
330
			/* Convert attributes to lowercase, because gmail
331
			 * returns them as lowercase (eg. \trash and not \Trash)
332
			 */
333
			$specialUseAttributes = [
334 12
				strtolower(Horde_Imap_Client::SPECIALUSE_ALL),
335 12
				strtolower(Horde_Imap_Client::SPECIALUSE_ARCHIVE),
336 12
				strtolower(Horde_Imap_Client::SPECIALUSE_DRAFTS),
337 12
				strtolower(Horde_Imap_Client::SPECIALUSE_FLAGGED),
338 12
				strtolower(Horde_Imap_Client::SPECIALUSE_JUNK),
339 12
				strtolower(Horde_Imap_Client::SPECIALUSE_SENT),
340 12
				strtolower(Horde_Imap_Client::SPECIALUSE_TRASH)
341 12
			];
342
343 12
			$attributes = array_map(function($n) {
344 6
				return strtolower($n);
345 12
			}, $this->attributes);
346
347 12
			foreach ($specialUseAttributes as $attr)  {
348 12
				if (in_array($attr, $attributes)) {
349 6
					$result = ltrim($attr, '\\');
350 6
					break;
351
				}
352 12
			}
353
354 12
		}
355
356 12
		$this->specialRole = $result;
357 12
	}
358
359
	/**
360
	 * Assign a special role to this mailbox based on its name
361
	 */
362 12
	protected function guessSpecialRole() {
363
364
		$specialFoldersDict = [
365 12
			'inbox'   => ['inbox'],
366 12
			'sent'    => ['sent', 'sent items', 'sent messages', 'sent-mail', 'sentmail'],
367 12
			'drafts'  => ['draft', 'drafts'],
368 12
			'archive' => ['archive', 'archives'],
369 12
			'trash'   => ['deleted messages', 'trash'],
370 12
			'junk'    => ['junk', 'spam', 'bulk mail'],
371 12
		];
372
373 12
		$lowercaseExplode = explode($this->delimiter, $this->getFolderId(), 2);
374 12
		$lowercaseId = strtolower(array_pop($lowercaseExplode));
375 12
		$result = null;
376 12
		foreach ($specialFoldersDict as $specialRole => $specialNames) {
377 12
			if (in_array($lowercaseId, $specialNames)) {
378 6
				$result = $specialRole;
379 6
				break;
380
			}
381 12
		}
382
383 12
		$this->specialRole = $result;
384 12
	}
385
386
	/**
387
	 * @param int $messageId
388
	 * @param string $attachmentId
389
	 * @return Attachment
390
	 */
391
	public function getAttachment($messageId, $attachmentId) {
392
		return new Attachment($this->conn, $this->mailBox, $messageId, $attachmentId);
393
	}
394
395
	/**
396
	 * @param string $rawBody
397
	 * @param array $flags
398
	 */
399 6
	public function saveMessage($rawBody, $flags = []) {
400
401 6
		$this->conn->append($this->mailBox, [
402
			[
403 6
				'data' => $rawBody,
404
				'flags' => $flags
405 6
			]
406 6
		]);
407 6
	}
408
409
	/**
410
	 * Save draft
411
	 *
412
	 * @param string $rawBody
413
	 * @return int UID of the saved draft
414
	 */
415
	public function saveDraft($rawBody) {
416
417
		$uids = $this->conn->append($this->mailBox, [
418
			[
419
				'data' => $rawBody,
420
				'flags' => [
421
					Horde_Imap_Client::FLAG_DRAFT,
422
					Horde_Imap_Client::FLAG_SEEN
423
				]
424
			]
425
		]);
426
		return $uids->current();
427
	}
428
429
	/**
430
	 * @param int $uid
431
	 * @param string $flag
432
	 * @param boolean $add
433
	 */
434
	public function setMessageFlag($uid, $flag, $add) {
435
		$options = [
436
			'ids' => new Horde_Imap_Client_Ids($uid)
437
		];
438
		if ($add) {
439
			$options['add'] = [$flag];
440
		} else {
441
			$options['remove'] = [$flag];
442
		}
443
		$this->conn->store($this->mailBox, $options);
444
	}
445
446
	/**
447
	 * @param $fromUid
448
	 * @param $toUid
449
	 * @return array
450
	 */
451 3
	public function getMessagesSince($fromUid, $toUid) {
452 3
		$query = new Horde_Imap_Client_Search_Query();
453 3
		$query->ids(new Horde_Imap_Client_Ids("$fromUid:$toUid"));
454 3
		return $this->getMessages(-1, -1, $query);
455
	}
456
457
	/**
458
	 * @return Horde_Imap_Client_Mailbox
459
	 */
460
	public function getHordeMailBox() {
461
		return $this->mailBox;
462
	}
463
464
}
465