Completed
Pull Request — master (#1272)
by Christoph
02:47
created

Mailbox::attributes()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2
Metric Value
dl 0
loc 3
ccs 0
cts 2
cp 0
rs 10
cc 1
eloc 2
nc 1
nop 0
crap 2
1
<?php
2
/**
3
 * Copyright (c) 2012 Bart Visscher <[email protected]>
4
 * This file is licensed under the Affero General Public License version 3 or
5
 * later.
6
 * See the COPYING-README file.
7
 */
8
9
namespace OCA\Mail;
10
11
use Horde_Imap_Client;
12
use Horde_Imap_Client_Ids;
13
use Horde_Imap_Client_Mailbox;
14
use Horde_Imap_Client_Search_Query;
15
use Horde_Imap_Client_Socket;
16
use OCA\Mail\Model\IMAPMessage;
17
use OCA\Mail\Service\IMailBox;
18
19
class Mailbox implements IMailBox {
20
21
	/**
22
	 * @var Horde_Imap_Client_Socket
23
	 */
24
	protected $conn;
25
26
	/**
27
	 * @var array
28
	 */
29
	private $attributes;
30
31
	/**
32
	 * @var string
33
	 */
34
	private $specialRole;
35
36
	/**
37
	 * @var string
38
	 */
39
	private $displayName;
40
41
	/**
42
	 * @var string
43
	 */
44
	private $delimiter;
45
46
	/**
47
	 * @var Horde_Imap_Client_Mailbox
48
	 */
49
	protected $mailBox;
50
51
	/**
52
	 * @param Horde_Imap_Client_Socket $conn
53
	 * @param Horde_Imap_Client_Mailbox $mailBox
54
	 * @param array $attributes
55
	 * @param string $delimiter
56
	 */
57
	public function __construct($conn, $mailBox, $attributes, $delimiter='/') {
58
		$this->conn = $conn;
59
		$this->mailBox = $mailBox;
60
		$this->attributes = $attributes;
61
		$this->delimiter = $delimiter;
62
		$this->getSpecialRoleFromAttributes();
63
		if ($this->specialRole === null) {
64
			$this->guessSpecialRole();
65
		}
66
		$this->makeDisplayName();
67
	}
68
69
	public function getMessages($from = 0, $count = 2, $filter = '') {
70
		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...
71
			$query = $filter;
72
		} else {
73
			$query = new Horde_Imap_Client_Search_Query();
74
			if ($filter) {
75
				$query->text($filter, false);
76
			}
77
		}
78
		if ($this->getSpecialRole() !== 'trash') {
79
			$query->flag(Horde_Imap_Client::FLAG_DELETED, false);
80
		}
81
		$result = $this->conn->search($this->mailBox, $query, ['sort' => [Horde_Imap_Client::SORT_DATE]]);
82
		$ids = array_reverse($result['match']->ids);
83
		if ($from >= 0 && $count >= 0) {
84
			$ids = array_slice($ids, $from, $count);
85
		}
86
		$ids = new \Horde_Imap_Client_Ids($ids, false);
87
88
		$headers = [];
89
90
		$fetch_query = new \Horde_Imap_Client_Fetch_Query();
91
		$fetch_query->envelope();
92
		$fetch_query->flags();
93
		$fetch_query->size();
94
		$fetch_query->uid();
95
		$fetch_query->imapDate();
96
		$fetch_query->structure();
97
98
		$headers = array_merge($headers, [
99
			'importance',
100
			'list-post',
101
			'x-priority'
102
		]);
103
		$headers[] = 'content-type';
104
105
		$fetch_query->headers('imp', $headers, [
106
			'cache' => true,
107
			'peek'  => true
108
		]);
109
110
		$options = ['ids' => $ids];
111
		// $list is an array of Horde_Imap_Client_Data_Fetch objects.
112
		$headers = $this->conn->fetch($this->mailBox, $fetch_query, $options);
113
114
		ob_start(); // fix for Horde warnings
115
		$messages = [];
116
		foreach ($headers->ids() as $message_id) {
117
			$header = $headers[$message_id];
118
			$message = new IMAPMessage($this->conn, $this->mailBox, $message_id, $header);
119
			$messages[] = $message->getListArray();
120
		}
121
		ob_get_clean();
122
123
		// sort by time
124
		usort($messages, function($a, $b) {
125
			return $a['dateInt'] < $b['dateInt'];
126
		});
127
128
		return $messages;
129
	}
130
131
	/**
132
	 * @return array
133
	 */
134
	public function attributes() {
135
		return $this->attributes;
136
	}
137
138
	/**
139
	 * @param string $messageId
140
	 * @param bool $loadHtmlMessageBody
141
	 * @return IMAPMessage
142
	 */
143
	public function getMessage($messageId, $loadHtmlMessageBody = false) {
144
		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...
145
	}
146
147
	/**
148
	 * @param int $flags
149
	 * @return array
150
	 */
151
	public function getStatus($flags = \Horde_Imap_Client::STATUS_ALL) {
152
		return $this->conn->status($this->mailBox, $flags);
153
	}
154
155
	/**
156
	 * @return int
157
	 */
158
	public function getTotalMessages() {
159
		$status = $this->getStatus(\Horde_Imap_Client::STATUS_MESSAGES);
160
		return (int) $status['messages'];
161
	}
162
163
	protected function makeDisplayName() {
164
		$parts = explode($this->delimiter, $this->mailBox->utf8, 2);
165
166
		if (count($parts) > 1) {
167
			$displayName = $parts[1];
168
		} elseif (strtolower($this->mailBox->utf8) === 'inbox') {
169
			$displayName = 'Inbox';
170
		} else {
171
			$displayName = $this->mailBox->utf8;
172
		}
173
174
		$this->displayName = $displayName;
175
	}
176
177
	public function getFolderId() {
178
		return $this->mailBox->utf8;
179
	}
180
181
	/**
182
	 * @return string
183
	 */
184
	public function getParent() {
185
		$folderId = $this->getFolderId();
186
		$parts = explode($this->delimiter, $folderId, 2);
187
188
		if (count($parts) > 1) {
189
			return $parts[0];
190
		}
191
192
		return null;
193
	}
194
195
	/**
196
	 * @return string
197
	 */
198
	public function getSpecialRole() {
199
		return $this->specialRole;
200
	}
201
202
	/**
203
	 * @return string
204
	 */
205
	public function getDisplayName() {
206
		return $this->displayName;
207
	}
208
209
	/**
210
	 * @param string $displayName
211
	 */
212
	public function setDisplayName($displayName) {
213
		$this->displayName = $displayName;
214
	}
215
216
	/**
217
	 * @param integer $accountId
218
	 * @return array
219
	 */
220
	public function getListArray($accountId, $status = null) {
221
		$displayName = $this->getDisplayName();
222
		try {
223
			if (is_null($status)) {
224
				$status = $this->getStatus();
225
			}
226
			$total = $status['messages'];
227
			$specialRole = $this->getSpecialRole();
228
			$unseen = ($specialRole === 'trash') ? 0 : $status['unseen'];
229
			$isEmpty = ($total === 0);
230
			$noSelect = in_array('\\noselect', $this->attributes);
231
			$parentId = $this->getParent();
232
			$parentId = ($parentId !== null) ? base64_encode($parentId) : null;
233
			return [
234
				'id' => base64_encode($this->getFolderId()),
235
				'parent' => $parentId,
236
				'name' => $displayName,
237
				'specialRole' => $specialRole,
238
				'unseen' => $unseen,
239
				'total' => $total,
240
				'isEmpty' => $isEmpty,
241
				'accountId' => $accountId,
242
				'noSelect' => $noSelect,
243
				'uidvalidity' => $status['uidvalidity'],
244
				'uidnext' => $status['uidnext'],
245
				'delimiter' => $this->delimiter
246
			];
247
		} catch (\Horde_Imap_Client_Exception $e) {
0 ignored issues
show
Bug introduced by
The class Horde_Imap_Client_Exception does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

Loading history...
248
			return [
249
				'id' => base64_encode($this->getFolderId()),
250
				'parent' => null,
251
				'name' => $displayName,
252
				'specialRole' => null,
253
				'unseen' => 0,
254
				'total' => 0,
255
				'error' => $e->getMessage(),
256
				'isEmpty' => true,
257
				'accountId' => $accountId,
258
				'noSelect' => true
259
			];
260
		}
261
	}
262
	/**
263
	 * Get the special use role of the mailbox
264
	 *
265
	 * This method reads the attributes sent by the server
266
	 *
267
	 */
268
	protected function getSpecialRoleFromAttributes() {
269
		/*
270
		 * @todo: support multiple attributes on same folder
271
		 * "any given server or  message store may support
272
		 *  any combination of the attributes"
273
		 *  https://tools.ietf.org/html/rfc6154
274
		 */
275
		$result = null;
276
		if (is_array($this->attributes)) {
277
			/* Convert attributes to lowercase, because gmail
278
			 * returns them as lowercase (eg. \trash and not \Trash)
279
			 */
280
			$specialUseAttributes = [
281
				strtolower(Horde_Imap_Client::SPECIALUSE_ALL),
282
				strtolower(Horde_Imap_Client::SPECIALUSE_ARCHIVE),
283
				strtolower(Horde_Imap_Client::SPECIALUSE_DRAFTS),
284
				strtolower(Horde_Imap_Client::SPECIALUSE_FLAGGED),
285
				strtolower(Horde_Imap_Client::SPECIALUSE_JUNK),
286
				strtolower(Horde_Imap_Client::SPECIALUSE_SENT),
287
				strtolower(Horde_Imap_Client::SPECIALUSE_TRASH)
288
			];
289
290
			$attributes = array_map(function($n) {
291
				return strtolower($n);
292
			}, $this->attributes);
293
294
			foreach ($specialUseAttributes as $attr)  {
295
				if (in_array($attr, $attributes)) {
296
					$result = ltrim($attr, '\\');
297
					break;
298
				}
299
			}
300
301
		}
302
303
		$this->specialRole = $result;
304
	}
305
306
	/**
307
	 * Assign a special role to this mailbox based on its name
308
	 */
309
	protected function guessSpecialRole() {
310
311
		$specialFoldersDict = [
312
			'inbox'   => ['inbox'],
313
			'sent'    => ['sent', 'sent items', 'sent messages', 'sent-mail', 'sentmail'],
314
			'drafts'  => ['draft', 'drafts'],
315
			'archive' => ['archive', 'archives'],
316
			'trash'   => ['deleted messages', 'trash'],
317
			'junk'    => ['junk', 'spam', 'bulk mail'],
318
		];
319
320
		$lowercaseExplode = explode($this->delimiter, $this->getFolderId(), 2);
321
		$lowercaseId = strtolower(array_pop($lowercaseExplode));
322
		$result = null;
323
		foreach ($specialFoldersDict as $specialRole => $specialNames) {
324
			if (in_array($lowercaseId, $specialNames)) {
325
				$result = $specialRole;
326
				break;
327
			}
328
		}
329
330
		$this->specialRole = $result;
331
	}
332
333
	/**
334
	 * @param int $messageId
335
	 * @param string $attachmentId
336
	 * @return Attachment
337
	 */
338
	public function getAttachment($messageId, $attachmentId) {
339
		return new Attachment($this->conn, $this->mailBox, $messageId, $attachmentId);
340
	}
341
342
	/**
343
	 * @param string $rawBody
344
	 * @param array $flags
345
	 */
346
	public function saveMessage($rawBody, $flags = []) {
347
348
		$this->conn->append($this->mailBox, [
349
			[
350
				'data' => $rawBody,
351
				'flags' => $flags
352
			]
353
		]);
354
	}
355
356
	/**
357
	 * Save draft
358
	 *
359
	 * @param string $rawBody
360
	 * @return int UID of the saved draft
361
	 */
362
	public function saveDraft($rawBody) {
363
364
		$uids = $this->conn->append($this->mailBox, [
365
			[
366
				'data' => $rawBody,
367
				'flags' => [
368
					Horde_Imap_Client::FLAG_DRAFT,
369
					Horde_Imap_Client::FLAG_SEEN
370
				]
371
			]
372
		]);
373
		return $uids->current();
374
	}
375
376
	/**
377
	 * @param int $uid
378
	 * @param string $flag
379
	 * @param boolean $add
380
	 */
381
	public function setMessageFlag($uid, $flag, $add) {
382
		$options = [
383
			'ids' => new Horde_Imap_Client_Ids($uid)
384
		];
385
		if ($add) {
386
			$options['add'] = [$flag];
387
		} else {
388
			$options['remove'] = [$flag];
389
		}
390
		$this->conn->store($this->mailBox, $options);
391
	}
392
393
	/**
394
	 * @param $fromUid
395
	 * @param $toUid
396
	 * @return array
397
	 */
398
	public function getMessagesSince($fromUid, $toUid) {
399
		$query = new Horde_Imap_Client_Search_Query();
400
		$query->ids(new Horde_Imap_Client_Ids("$fromUid:$toUid"));
401
		return $this->getMessages(-1, -1, $query);
402
	}
403
404
	/**
405
	 * @return Horde_Imap_Client_Mailbox
406
	 */
407
	public function getHordeMailBox() {
408
		return $this->mailBox;
409
	}
410
411
}
412