Completed
Pull Request — master (#1536)
by
unknown
08:54
created

Account::getListArray()   B

Complexity

Conditions 4
Paths 3

Size

Total Lines 26
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 18
CRAP Score 4

Importance

Changes 0
Metric Value
dl 0
loc 26
ccs 18
cts 18
cp 1
rs 8.5806
c 0
b 0
f 0
cc 4
eloc 18
nc 3
nop 0
crap 4
1
<?php
2
/**
3
 * @author Alexander Weidinger <[email protected]>
4
 * @author Christian Nöding <[email protected]>
5
 * @author Christoph Wurst <[email protected]>
6
 * @author Christoph Wurst <[email protected]>
7
 * @author Christoph Wurst <[email protected]>
8
 * @author Clement Wong <[email protected]>
9
 * @author Lukas Reschke <[email protected]>
10
 * @author Robin McCorkell <[email protected]>
11
 * @author Scrutinizer Auto-Fixer <[email protected]>
12
 * @author Thomas Imbreckx <[email protected]>
13
 * @author Thomas I <[email protected]>
14
 * @author Thomas Mueller <[email protected]>
15
 * @author Thomas Müller <[email protected]>
16
 *
17
 * ownCloud - Mail
18
 *
19
 * This code is free software: you can redistribute it and/or modify
20
 * it under the terms of the GNU Affero General Public License, version 3,
21
 * as published by the Free Software Foundation.
22
 *
23
 * This program is distributed in the hope that it will be useful,
24
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
25
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
26
 * GNU Affero General Public License for more details.
27
 *
28
 * You should have received a copy of the GNU Affero General Public License, version 3,
29
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
30
 *
31
 */
32
33
namespace OCA\Mail;
34
35
use Horde_Imap_Client_Ids;
36
use Horde_Imap_Client_Mailbox;
37
use Horde_Imap_Client_Socket;
38
use Horde_Imap_Client;
39
use Horde_Mail_Rfc822_Address;
40
use Horde_Mail_Transport;
41
use Horde_Mail_Transport_Mail;
42
use Horde_Mail_Transport_Smtphorde;
43
use Horde_Mime_Headers_Date;
44
use Horde_Mime_Mail;
45
use Horde_Mime_Part;
46
use OCA\Mail\Cache\Cache;
47
use OCA\Mail\Db\MailAccount;
48
use OCA\Mail\Model\IMessage;
49
use OCA\Mail\Model\Message;
50
use OCA\Mail\Model\ReplyMessage;
51
use OCA\Mail\Service\AutoCompletion\AddressCollector;
52
use OCA\Mail\Service\IAccount;
53
use OCA\Mail\Service\IMailBox;
54
use OCP\IConfig;
55
use OCP\ICacheFactory;
56
use OCP\Security\ICrypto;
57
58
class Account implements IAccount {
59
60
	/** @var MailAccount */
61
	private $account;
62
63
	/** @var Mailbox[]|null */
64
	private $mailboxes;
65
66
	/** @var Horde_Imap_Client_Socket */
67
	private $client;
68
69
	/** @var ICrypto */
70
	private $crypto;
71
72
	/** @var IConfig */
73
	private $config;
74
75
	/** @var ICacheFactory */
76
	private $memcacheFactory;
77
78
	/**
79
	 * @param MailAccount $account
80
	 */
81 9
	public function __construct(MailAccount $account) {
82 3
		$this->account = $account;
83 3
		$this->mailboxes = null;
84 9
		$this->crypto = \OC::$server->getCrypto();
85 3
		$this->config = \OC::$server->getConfig();
86 3
		$this->memcacheFactory = \OC::$server->getMemcacheFactory();
87 3
	}
88
89
	/**
90
	 * @return int
91
	 */
92 6
	public function getId() {
93 6
		return $this->account->getId();
94
	}
95
96
	/**
97
	 * @return string
98
	 */
99
	public function getName() {
100
		return $this->account->getName();
101
	}
102
103
	/**
104
	 * @return string
105
	 */
106 3
	public function getEMailAddress() {
107 3
		return $this->account->getEmail();
108
	}
109
110
	/**
111
	 * @return Horde_Imap_Client_Socket
112
	 */
113 13
	public function getImapConnection() {
114 13
		if (is_null($this->client)) {
115
			$host = $this->account->getInboundHost();
116
			$user = $this->account->getInboundUser();
117
			$password = $this->account->getInboundPassword();
118
			$password = $this->crypto->decrypt($password);
119
			$port = $this->account->getInboundPort();
120
			$ssl_mode = $this->convertSslMode($this->account->getInboundSslMode());
121
122
			$params = [
123
				'username' => $user,
124
				'password' => $password,
125
				'hostspec' => $host,
126
				'port' => $port,
127
				'secure' => $ssl_mode,
128
				'timeout' => 20,
129
			];
130 View Code Duplication
			if ($this->config->getSystemValue('app.mail.imaplog.enabled', false)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
131
				$params['debug'] = $this->config->getSystemValue('datadirectory') . '/horde_imap.log';
132
			}
133
			if ($this->config->getSystemValue('app.mail.server-side-cache.enabled', false)) {
134
				if ($this->memcacheFactory->isAvailable()) {
135
					$params['cache'] = [
136
						'backend' => new Cache(array(
137
							'cacheob' => $this->memcacheFactory
138
								->create(md5($this->getId() . $this->getEMailAddress()))
139
						))];
140
				}
141
			}
142
			$this->client = new \Horde_Imap_Client_Socket($params);
143
			$this->client->login();
144
		}
145 13
		return $this->client;
146
	}
147
148
	/**
149
	 * @param string $mailBox
150
	 * @return Mailbox
151
	 */
152 12
	public function createMailbox($mailBox) {
153 12
		$conn = $this->getImapConnection();
154 12
		$conn->createMailbox($mailBox);
155 12
		$this->mailboxes = null;
156
157 12
		return $this->getMailbox($mailBox);
158
	}
159
160
	/**
161
	 * Send a new message or reply to an existing message
162
	 *
163
	 * @param IMessage $message
164
	 * @param int|null $draftUID
165
	 */
166
	public function sendMessage(IMessage $message, $draftUID) {
167
		// build mime body
168
		$from = new Horde_Mail_Rfc822_Address($message->getFrom());
169
		$from->personal = $this->getName();
170
		$headers = [
171
			'From' => $from,
172
			'To' => $message->getToList(),
173
			'Cc' => $message->getCCList(),
174
			'Bcc' => $message->getBCCList(),
175
			'Subject' => $message->getSubject(),
176
		];
177
178
		if (!is_null($message->getRepliedMessage())) {
179
			$headers['In-Reply-To'] = $message->getRepliedMessage()->getMessageId();
180
		}
181
182
		$mail = new Horde_Mime_Mail();
183
		$mail->addHeaders($headers);
184
		if($message->getType() === 'text/html'){
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface OCA\Mail\Model\IMessage as the method getType() does only exist in the following implementations of said interface: OCA\Mail\Model\Message, OCA\Mail\Model\ReplyMessage.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
185
			$mail->setHtmlBody($message->getContent());
186
		}else{
187
			$mail->setBody($message->getContent());
188
		}
189
190
		// Append attachments
191
		foreach ($message->getAttachments() as $attachment) {
192
			$mail->addMimePart($attachment);
193
		}
194
195
		// Send the message
196
		$transport = $this->createTransport();
197
		$mail->send($transport, false, false);
198
199
		// Save the message in the sent folder
200
		$sentFolder = $this->getSentFolder();
201
		/** @var resource $raw */
202
		$raw = stream_get_contents($mail->getRaw());
203
		$sentFolder->saveMessage($raw, [
0 ignored issues
show
Documentation introduced by
$raw is of type resource, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
204
			Horde_Imap_Client::FLAG_SEEN
205
		]);
206
207
		// Delete draft if one exists
208
		if (!is_null($draftUID)) {
209
			$draftsFolder = $this->getDraftsFolder();
210
			$draftsFolder->setMessageFlag($draftUID, Horde_Imap_Client::FLAG_DELETED, true);
211
			$this->deleteDraft($draftUID);
212
		}
213
	}
214
215
	/**
216
	 * @param IMessage $message
217
	 * @param int|null $previousUID
218
	 * @return int
219
	 */
220 7
	public function saveDraft(IMessage $message, $previousUID) {
221
		// build mime body
222
		$from = new Horde_Mail_Rfc822_Address($message->getFrom());
223
		$from->personal = $this->getName();
224
		$headers = [
225
			'From' => $from,
226
			'To' => $message->getToList(),
227
			'Cc' => $message->getCCList(),
228
			'Bcc' => $message->getBCCList(),
229
			'Subject' => $message->getSubject(),
230 7
			'Date' => Horde_Mime_Headers_Date::create(),
231
		];
232
233
		$mail = new Horde_Mime_Mail();
234
		$mail->addHeaders($headers);
235
		$body = new Horde_Mime_Part();
236
		if($message->getType() !== 'text/html'){
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface OCA\Mail\Model\IMessage as the method getType() does only exist in the following implementations of said interface: OCA\Mail\Model\Message, OCA\Mail\Model\ReplyMessage.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
237
			$body->setType('text/plain');
238
		}else{
239
			$body->setType('text/html');
240
		}
241
		$body->setContents($message->getContent());
242
		$mail->setBasePart($body);
243
244
		// create transport and save message
245
		// save the message in the drafts folder
246
		$draftsFolder = $this->getDraftsFolder();
247
		/** @var resource $raw */
248
		$raw = $mail->getRaw();
249
		$newUid = $draftsFolder->saveDraft(stream_get_contents($raw));
250
251
		// delete old version if one exists
252
		if (!is_null($previousUID)) {
253
			$draftsFolder->setMessageFlag($previousUID, \Horde_Imap_Client::FLAG_DELETED,
254
				true);
255
			$this->deleteDraft($previousUID);
256
		}
257
258
		return $newUid;
259
	}
260
261
	/**
262
	 * @param string $mailBox
263
	 */
264 12
	public function deleteMailbox($mailBox) {
265 12
		if ($mailBox instanceof Mailbox) {
266
			$mailBox = $mailBox->getFolderId();
267
		}
268 12
		$conn = $this->getImapConnection();
269 12
		$conn->deleteMailbox($mailBox);
270 3
		$this->mailboxes = null;
271 3
	}
272
273
	/**
274
	 * Lists mailboxes (folders) for this account.
275
	 *
276
	 * Lists mailboxes and also queries the server for their 'special use',
277
	 * eg. inbox, sent, trash, etc
278
	 *
279
	 * @param string $pattern Pattern to match mailboxes against. All by default.
280
	 * @return Mailbox[]
281
	 */
282 6
	protected function listMailboxes($pattern = '*') {
283
		// open the imap connection
284 6
		$conn = $this->getImapConnection();
285
286
		// if successful -> get all folders of that account
287 6
		$mailBoxes = $conn->listMailboxes($pattern, Horde_Imap_Client::MBOX_ALL,
288
			[
289 6
			'delimiter' => true,
290 6
			'attributes' => true,
291 6
			'special_use' => true,
292
			'sort' => true
293 6
		]);
294
295 6
		$mailboxes = [];
296 6
		foreach ($mailBoxes as $mailbox) {
297 6
			$mailboxes[] = new Mailbox($conn, $mailbox['mailbox'],
298 6
				$mailbox['attributes'], $mailbox['delimiter']);
299 6
			if ($mailbox['mailbox']->utf8 === 'INBOX') {
300 6
				$mailboxes[] = new SearchMailbox($conn, $mailbox['mailbox'],
301 6
					$mailbox['attributes'], $mailbox['delimiter']);
302 6
			}
303 6
		}
304
305 6
		return $mailboxes;
306
	}
307
308
	/**
309
	 * @param string $folderId
310
	 * @return \OCA\Mail\Mailbox
311
	 */
312 12
	public function getMailbox($folderId) {
313 12
		$conn = $this->getImapConnection();
314 12
		$parts = explode('/', $folderId);
315 12
		if (count($parts) > 1 && $parts[1] === 'FLAGGED') {
316
			$mailbox = new Horde_Imap_Client_Mailbox($parts[0]);
317
			return new SearchMailbox($conn, $mailbox, []);
318
		}
319 12
		$mailbox = new Horde_Imap_Client_Mailbox($folderId);
320 12
		return new Mailbox($conn, $mailbox, []);
321
	}
322
323
	/**
324
	 * Get a list of all mailboxes in this account
325
	 *
326
	 * @return Mailbox[]
327
	 */
328 7
	public function getMailboxes() {
329 7
		if ($this->mailboxes === null) {
330 6
			$this->mailboxes = $this->listMailboxes();
331 6
			$this->sortMailboxes();
332 6
			$this->localizeSpecialMailboxes();
333 6
		}
334
335 7
		return $this->mailboxes;
336
	}
337
338
	/**
339
	 * @return array
340
	 */
341 3
	public function getListArray() {
342
343 3
		$folders = [];
344 3
		$mailBoxes = $this->getMailboxes();
345
		$mailBoxNames = array_map(function($mb) {
346
			/** @var Mailbox $mb */
347 3
			return $mb->getFolderId();
348
		}, array_filter($mailBoxes, function($mb) {
349
			/** @var Mailbox $mb */
350 3
			return (!$mb instanceof SearchMailbox) && (!in_array('\noselect', $mb->attributes()));
351 3
		}));
352
353 3
		$status = $this->getImapConnection()->status($mailBoxNames);
354 3
		foreach ($mailBoxes as $mailbox) {
355 3
			$s = isset($status[$mailbox->getFolderId()]) ? $status[$mailbox->getFolderId()] : null;
356 3
			$folders[] = $mailbox->getListArray($this->getId(), $s);
357 3
		}
358 3
		$delimiter = reset($folders)['delimiter'];
359
		return [
360 3
			'id' => $this->getId(),
361 3
			'email' => $this->getEMailAddress(),
362 3
			'folders' => array_values($folders),
363 3
			'specialFolders' => $this->getSpecialFoldersIds(),
364 3
			'delimiter' => $delimiter,
365 3
		];
366
	}
367
368
	/**
369
	 * @return Horde_Mail_Transport
370
	 */
371
	public function createTransport() {
372
		$transport = $this->config->getSystemValue('app.mail.transport', 'smtp');
373
		if ($transport === 'php-mail') {
374
			return new Horde_Mail_Transport_Mail();
375
		}
376
377
		$password = $this->account->getOutboundPassword();
378
		$password = $this->crypto->decrypt($password);
379
		$params = [
380
			'host' => $this->account->getOutboundHost(),
381
			'password' => $password,
382
			'port' => $this->account->getOutboundPort(),
383
			'username' => $this->account->getOutboundUser(),
384
			'secure' => $this->convertSslMode($this->account->getOutboundSslMode()),
385
			'timeout' => 2
386
		];
387 View Code Duplication
		if ($this->config->getSystemValue('app.mail.smtplog.enabled', false)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
388
			$params['debug'] = $this->config->getSystemValue('datadirectory') . '/horde_smtp.log';
389
		}
390
		return new Horde_Mail_Transport_Smtphorde($params);
391
	}
392
393
	/**
394
	 * Lists special use folders for this account.
395
	 *
396
	 * The special uses returned are the "best" one for each special role,
397
	 * picked amongst the ones returned by the server, as well
398
	 * as the one guessed by our code.
399
	 *
400
	 * @param bool $base64_encode
401
	 * @return array In the form [<special use>=><folder id>, ...]
402
	 */
403 6
	public function getSpecialFoldersIds($base64_encode=true) {
404 6
		$folderRoles = ['inbox', 'sent', 'drafts', 'trash', 'archive', 'junk', 'flagged', 'all'];
405 6
		$specialFoldersIds = [];
406
407 6
		foreach ($folderRoles as $role) {
408 6
			$folders = $this->getSpecialFolder($role, true);
409 6
			$specialFoldersIds[$role] = (count($folders) === 0) ? null : $folders[0]->getFolderId();
410 6
			if ($specialFoldersIds[$role] !== null && $base64_encode === true) {
411 3
				$specialFoldersIds[$role] = base64_encode($specialFoldersIds[$role]);
412 3
			}
413 6
		}
414 6
		return $specialFoldersIds;
415
	}
416
417
	/**
418
	 * Get the "drafts" mailbox
419
	 *
420
	 * @return Mailbox The best candidate for the "drafts" inbox
421
	 */
422 View Code Duplication
	public function getDraftsFolder() {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
423
		// check for existence
424
		$draftsFolder = $this->getSpecialFolder('drafts', true);
425
		if (count($draftsFolder) === 0) {
426
			// drafts folder does not exist - let's create one
427
			$conn = $this->getImapConnection();
428
			// TODO: also search for translated drafts mailboxes
429
			$conn->createMailbox('Drafts', [
430
				'special_use' => ['drafts'],
431
			]);
432
			return $this->guessBestMailBox($this->listMailboxes('Drafts'));
433
		}
434
		return $draftsFolder[0];
435
	}
436
437
	/**
438
	 * @return IMailBox
439
	 */
440
	public function getInbox() {
441
		$folders = $this->getSpecialFolder('inbox', false);
442
		return count($folders) > 0 ? $folders[0] : null;
443
	}
444
445
	/**
446
	 * Get the "sent mail" mailbox
447
	 *
448
	 * @return Mailbox The best candidate for the "sent mail" inbox
449
	 */
450 View Code Duplication
	public function getSentFolder() {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
451
		//check for existence
452
		$sentFolders = $this->getSpecialFolder('sent', true);
453
		if (count($sentFolders) === 0) {
454
			//sent folder does not exist - let's create one
455
			$conn = $this->getImapConnection();
456
			//TODO: also search for translated sent mailboxes
457
			$conn->createMailbox('Sent', [
458
				'special_use' => ['sent'],
459
			]);
460
			return $this->guessBestMailBox($this->listMailboxes('Sent'));
461
		}
462
		return $sentFolders[0];
463
	}
464
465
	/**
466
	 * @param string $sourceFolderId
467
	 * @param int $messageId
468
	 */
469
	public function deleteMessage($sourceFolderId, $messageId) {
470
		$mb = $this->getMailbox($sourceFolderId);
471
		$hordeSourceMailBox = $mb->getHordeMailBox();
472
		// by default we will create a 'Trash' folder if no trash is found
473
		$trashId = "Trash";
474
		$createTrash = true;
475
476
		$trashFolders = $this->getSpecialFolder('trash', true);
477
478
		if (count($trashFolders) !== 0) {
479
			$trashId = $trashFolders[0]->getFolderId();
480
			$createTrash = false;
481
		} else {
482
			// no trash -> guess
483
			$trashes = array_filter($this->getMailboxes(), function($box) {
484
				/**
485
				 * @var Mailbox $box
486
				 */
487
				return (stripos($box->getDisplayName(), 'trash') !== false);
488
			});
489
			if (!empty($trashes)) {
490
				$trashId = array_values($trashes);
491
				$trashId = $trashId[0]->getFolderId();
492
				$createTrash = false;
493
			}
494
		}
495
496
		$hordeMessageIds = new Horde_Imap_Client_Ids($messageId);
497
		$hordeTrashMailBox = new Horde_Imap_Client_Mailbox($trashId);
498
499
		if ($sourceFolderId === $trashId) {
500
			$this->getImapConnection()->expunge($hordeSourceMailBox,
501
				array('ids' => $hordeMessageIds, 'delete' => true));
502
503
			\OC::$server->getLogger()->info("Message expunged: {message} from mailbox {mailbox}",
504
				array('message' => $messageId, 'mailbox' => $sourceFolderId));
505
		} else {
506
			$this->getImapConnection()->copy($hordeSourceMailBox, $hordeTrashMailBox,
507
				array('create' => $createTrash, 'move' => true, 'ids' => $hordeMessageIds));
508
509
			\OC::$server->getLogger()->info("Message moved to trash: {message} from mailbox {mailbox}",
510
				array('message' => $messageId, 'mailbox' => $sourceFolderId, 'app' => 'mail'));
511
		}
512
	}
513
514
	/**
515
	 *
516
	 * @param int $messageId
517
	 */
518
	public function deleteDraft($messageId) {
0 ignored issues
show
Unused Code introduced by
The parameter $messageId is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
519
		$draftsFolder = $this->getDraftsFolder();
520
521
		$draftsMailBox = new \Horde_Imap_Client_Mailbox($draftsFolder->getFolderId(), false);
522
		$this->getImapConnection()->expunge($draftsMailBox);
523
	}
524
525
	/**
526
	 * Get 'best' mailbox guess
527
	 *
528
	 * For now the best candidate is the one with
529
	 * the most messages in it.
530
	 *
531
	 * @param array $folders
532
	 * @return Mailbox
533
	 */
534
	protected function guessBestMailBox(array $folders) {
535
		$maxMessages = -1;
536
		$bestGuess = null;
537
		foreach ($folders as $folder) {
538
			/** @var Mailbox $folder */
539
			if ($folder->getTotalMessages() > $maxMessages) {
540
				$maxMessages = $folder->getTotalMessages();
541
				$bestGuess = $folder;
542
			}
543
		}
544
		return $bestGuess;
545
	}
546
547
	/**
548
	 * Get mailbox(es) that have the given special use role
549
	 *
550
	 * With this method we can get a list of all mailboxes that have been
551
	 * determined to have a specific special use role. It can also return
552
	 * the best candidate for this role, for situations where we want
553
	 * one single folder.
554
	 *
555
	 * @param string $role Special role of the folder we want to get ('sent', 'inbox', etc.)
556
	 * @param bool $guessBest If set to true, return only the folder with the most messages in it
557
	 *
558
	 * @return Mailbox[] if $guessBest is false, or Mailbox if $guessBest is true. Empty [] if no match.
559
	 */
560 6
	protected function getSpecialFolder($role, $guessBest=true) {
561
562 6
		$specialFolders = [];
563 6
		foreach ($this->getMailboxes() as $mailbox) {
564 6
			if ($role === $mailbox->getSpecialRole()) {
565 6
				$specialFolders[] = $mailbox;
566 6
			}
567 6
		}
568
569 6
		if ($guessBest === true && count($specialFolders) > 1) {
570
			return [$this->guessBestMailBox($specialFolders)];
0 ignored issues
show
Best Practice introduced by
The expression return array($this->gues...lBox($specialFolders)); seems to be an array, but some of its elements' types (null) are incompatible with the return type documented by OCA\Mail\Account::getSpecialFolder of type OCA\Mail\Mailbox[].

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 new Author('Johannes');
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return '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...
571
		} else {
572 6
			return $specialFolders;
573
		}
574
	}
575
576
	/**
577
	 *  Localizes the name of the special use folders
578
	 *
579
	 *  The display name of the best candidate folder for each special use
580
	 *  is localized to the user's language
581
	 */
582 6
	protected function localizeSpecialMailboxes() {
583
584 6
		$l = \OC::$server->getL10N('mail');
585
		$map = [
586
			// TRANSLATORS: translated mail box name
587 6
			'inbox'   => $l->t('Inbox'),
588
			// TRANSLATORS: translated mail box name
589 6
			'sent'    => $l->t('Sent'),
590
			// TRANSLATORS: translated mail box name
591 6
			'drafts'  => $l->t('Drafts'),
592
			// TRANSLATORS: translated mail box name
593 6
			'archive' => $l->t('Archive'),
594
			// TRANSLATORS: translated mail box name
595 6
			'trash'   => $l->t('Trash'),
596
			// TRANSLATORS: translated mail box name
597 6
			'junk'    => $l->t('Junk'),
598
			// TRANSLATORS: translated mail box name
599 6
			'all'     => $l->t('All'),
600
			// TRANSLATORS: translated mail box name
601 6
			'flagged' => $l->t('Favorites'),
602 6
		];
603 6
		$mailboxes = $this->getMailboxes();
604 6
		$specialIds = $this->getSpecialFoldersIds(false);
605 6
		foreach ($mailboxes as $i => $mailbox) {
606 6
			if (in_array($mailbox->getFolderId(), $specialIds) === true) {
607 6
				if (isset($map[$mailbox->getSpecialRole()])) {
608 6
					$translatedDisplayName = $map[$mailbox->getSpecialRole()];
609 6
					$mailboxes[$i]->setDisplayName((string)$translatedDisplayName);
610 6
				}
611 6
			}
612 6
		}
613 6
	}
614
615
	/**
616
	 * Sort mailboxes
617
	 *
618
	 * Sort the array of mailboxes with
619
	 *  - special use folders coming first in this order: all, inbox, flagged, drafts, sent, archive, junk, trash
620
	 *  - 'normal' folders coming after that, sorted alphabetically
621
	 */
622 6
	protected function sortMailboxes() {
623
624 6
		$mailboxes = $this->getMailboxes();
625
		usort($mailboxes, function($a, $b) {
626
			/**
627
			 * @var Mailbox $a
628
			 * @var Mailbox $b
629
			 */
630 6
			$roleA = $a->getSpecialRole();
631 6
			$roleB = $b->getSpecialRole();
632
			$specialRolesOrder = [
633 6
				'all'     => 0,
634 6
				'inbox'   => 1,
635 6
				'flagged' => 2,
636 6
				'drafts'  => 3,
637 6
				'sent'    => 4,
638 6
				'archive' => 5,
639 6
				'junk'    => 6,
640 6
				'trash'   => 7,
641 6
			];
642
			// if there is a flag unknown to us, we ignore it for sorting :
643
			// the folder will be sorted by name like any other 'normal' folder
644 6
			if (array_key_exists($roleA, $specialRolesOrder) === false) {
645 5
				$roleA = null;
646 5
			}
647 6
			if (array_key_exists($roleB, $specialRolesOrder) === false) {
648 6
				$roleB = null;
649 6
			}
650
651 6
			if ($roleA === null && $roleB !== null) {
652 5
				return 1;
653 6
			} elseif ($roleA !== null && $roleB === null) {
654 6
				return -1;
655 6
			} elseif ($roleA !== null && $roleB !== null) {
656 6
				if ($roleA === $roleB) {
657
					return strcasecmp($a->getdisplayName(), $b->getDisplayName());
658
				} else {
659 6
					return $specialRolesOrder[$roleA] - $specialRolesOrder[$roleB];
660
				}
661
			}
662
			// we get here if $roleA === null && $roleB === null
663 5
			return strcasecmp($a->getDisplayName(), $b->getDisplayName());
664 6
		});
665
666 6
		$this->mailboxes = $mailboxes;
667 6
	}
668
669
	/**
670
	 * Convert special security mode values into Horde parameters
671
	 *
672
	 * @param string $sslMode
673
	 * @return false|string
674
	 */
675
	protected function convertSslMode($sslMode) {
676
		switch ($sslMode) {
677
			case 'none':
678
				return false;
679
		}
680
		return $sslMode;
681
	}
682
683
	/**
684
	 * @param $query
685
	 * @return array
686
	 */
687 4
	public function getChangedMailboxes($query) {
688 4
		$imp = $this->getImapConnection();
689 4
		$allBoxes = $this->getMailboxes();
690 4
		$allBoxesMap = [];
691 4
		foreach ($allBoxes as $mb) {
692 4
			$allBoxesMap[$mb->getFolderId()] = $mb;
693 4
		}
694
695
		// filter non existing mailboxes
696
		$mailBoxNames = array_filter(array_keys($query), function($folderId) use ($allBoxesMap) {
697 4
			return isset($allBoxesMap[$folderId]);
698 4
		});
699
700 4
		$status = $imp->status($mailBoxNames);
701
702
		// filter for changed mailboxes
703 4
		$changedBoxes = [];
704 4
		foreach ($status as $folderId => $s) {
705 3
			$uidValidity = $query[$folderId]['uidvalidity'];
706 3
			$uidNext = $query[$folderId]['uidnext'];
707
708 3
			if ($uidNext === $s['uidnext'] &&
709 3
				$uidValidity === $s['uidvalidity']) {
710 3
				continue;
711
			}
712
			// get unread messages
713 3
			if (isset($allBoxesMap[$folderId])) {
714
				/** @var Mailbox $m */
715 3
				$m = $allBoxesMap[$folderId];
716 3
				$role = $m->getSpecialRole();
717 3
				if (is_null($role) || $role === 'inbox') {
718 3
					$newMessages = $m->getMessagesSince($uidNext, $s['uidnext']);
719
					// only trigger updates in case new messages are actually available
720 3
					if (!empty($newMessages)) {
721 3
						$changedBoxes[$folderId] = $m->getListArray($this->getId(), $s);
722 3
						$changedBoxes[$folderId]['messages'] = $newMessages;
723 3
						$newUnreadMessages = array_filter($newMessages, function($m) {
724 3
							return $m['flags']['unseen'];
725 3
						});
726 3
						$changedBoxes[$folderId]['newUnReadCounter'] = count($newUnreadMessages);
727 3
					}
728 3
				}
729 3
			}
730 4
		}
731
732 4
		return $changedBoxes;
733
	}
734
735
	/**
736
	 * @throws \Horde_Imap_Client_Exception
737
	 */
738
	public function reconnect() {
739
		$this->mailboxes = null;
740
		if ($this->client) {
741
			$this->client->close();
742
			$this->client = null;
743
		}
744
		$this->getImapConnection();
745
	}
746
747
	/**
748
	 * @return array
749
	 */
750
	public function getConfiguration() {
751
		return $this->account->toJson();
752
	}
753
754
	/**
755
	 * @return string|Horde_Mail_Rfc822_List
756
	 */
757
	public function getEmail() {
758
		return $this->account->getEmail();
759
	}
760
761
	public function testConnectivity() {
762
		// connect to imap
763
		$this->getImapConnection();
764
765
		// connect to smtp
766
		$smtp = $this->createTransport();
767
		if ($smtp instanceof Horde_Mail_Transport_Smtphorde) {
0 ignored issues
show
Bug introduced by
The class Horde_Mail_Transport_Smtphorde 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...
768
			$smtp->getSMTPObject();
769
		}
770
	}
771
772
	/**
773
	 * Factory method for creating new messages
774
	 *
775
	 * @return IMessage
776
	 */
777
	public function newMessage() {
778
		return new Message();
779
	}
780
781
	/**
782
	 * Factory method for creating new reply messages
783
	 *
784
	 * @return ReplyMessage
785
	 */
786
	public function newReplyMessage() {
787
		return new ReplyMessage();
788
	}
789
790
}
791