Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MailboxesParser #268

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
Open
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@ vendor/
.php_cs.cache
composer.lock
phpunit.xml
.idea/
phpunit.xml.dist
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need phpunit.xml.dist in the package, please revert .gitignore file

6 changes: 3 additions & 3 deletions phpunit.xml.dist
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>

<phpunit
bootstrap="./vendor/autoload.php"
colors="true"
verbose="true"
bootstrap="./vendor/autoload.php"
colors="true"
verbose="true"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We want to keep one indentation for this file, please revert it.

>
<testsuites>
<testsuite name="ddeboer/imap">
Expand Down
9 changes: 9 additions & 0 deletions src/Exception/MailboxesParserException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php

declare(strict_types=1);

namespace Ddeboer\Imap\Exception;

final class MailboxesParserException extends AbstractException
{
}
245 changes: 245 additions & 0 deletions src/MailboxesParser/MailboxesParser.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,245 @@
<?php
/**
* Created by PhpStorm.
* User: Lukasz
* Date: 2017-11-10
* Time: 19:49
*/

namespace Ddeboer\Imap\MailboxesParser;


use Ddeboer\Imap\Exception\MailboxesParserException;
use Ddeboer\Imap\MailboxInterface;

class MailboxesParser
{
/** @var MailboxInterface[] */
protected $mailboxes;
/** @var ParsedMailbox[] */
protected $folders;
protected $treeStructure;

const INBOX = 'inbox';
const SENT = 'sent';
const DRAFT = 'drafts';
const SPAM = 'spam';
const TRASH = 'trash';
const TEMPLATES = 'templates';
const ARCHIVES = 'archives';

protected $specialFoldersIds = [
self::INBOX => ['inbox',],
self::SENT => ['sent', 'sent messages', 'INBOX.Sent', '[Gmail]/Sent Mail',],
self::DRAFT => ['drafts', 'INBOX.Drafts', '[Gmail]/Drafts',],
self::SPAM => ['spam', 'INBOX.spam', '[Gmail]/spam'],
self::TRASH => ['trash', 'bin', 'INBOX.trash', '[Gmail]/trash'],
self::TEMPLATES => ['templates'],
self::ARCHIVES => ['archives',],
];
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ID of special folder vary a lot from different email provider, I don't want to have a static list to keep up to date.

What about letting the user specify it in the constructor, so this library doesn't have to maintain it?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think those are common ones. All other folders are simply listed

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately not, those are not as common as you may think.
I work with a lot of IMAP servers where these special folders have many other aliases: only for the Trash one I have 15 variants 😞 in English, without counting localizations.

I'm still against to maintain this list: let the user specify them.

Also the tests would be much more easy to implement and verify.


protected $specialFoldersNames = [
self::DRAFT => 'Drafts',
self::INBOX => 'Inbox',
self::SENT => 'Sent',
self::SPAM => 'Spam',
self::TRASH => 'Trash',
self::TEMPLATES => 'Templates',
self::ARCHIVES => 'Archives',
];
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The same goes for folder names: if you need them, let the user specify them as a constructor dependency.

We don't have the resources to maintain translations.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can manually name special folders with setSpecialFoldersNames.
and add special folders Id with addSpecialFolderId


protected $specialFoldersOrder = [
self::INBOX => 1,
self::SENT => 2,
self::DRAFT => 3,
self::TEMPLATES => 4,
self::ARCHIVES => 10000,
self::SPAM => 20000,
self::TRASH => 30000,
];

/**
* MailboxesTree constructor.
*
* @param MailboxInterface[] $mailboxes
*/
public function __construct($mailboxes)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add type-hints

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think in php 7.0 I cannot use public function __construct(MailboxInterface[] $mailboxes)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, but you can typehint the array at least.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did it

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still see __construct($mailboxes).

Can you change this to __construct(array $mailboxes)?

{
$this->mailboxes = $mailboxes;
}

public function setLanguage($lang)
{
$path = __DIR__ . DIRECTORY_SEPARATOR . 'languages' . DIRECTORY_SEPARATOR . $lang . DIRECTORY_SEPARATOR . 'names.php';
if (!is_file($path)) {
throw new MailboxesParserException(\sprintf('File for language names %s does not exist', $path));
}
$names = require $path;
$this->setSpecialFoldersNames($names);

$path = __DIR__ . DIRECTORY_SEPARATOR . 'languages' . DIRECTORY_SEPARATOR . $lang . DIRECTORY_SEPARATOR . 'ids.php';
if (!is_file($path)) {
throw new MailboxesParserException(\sprintf('File for language ids %s does not exist', $path));
}
$ids = require $path;
foreach ($ids AS $specialFolder => $idsArray) {
foreach ($idsArray AS $id) {
$this->addSpecialFolderId($specialFolder, $id);
}
}
}

protected function parse()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add return-type and doc-block

{
$this->folders = [];
usort($this->mailboxes, [$this, "sortByMailboxName"]);
foreach ($this->mailboxes AS $k => $mailbox) {
$mailboxName = $mailbox->getName();
$folder = new ParsedMailbox();
$folder->setMailbox($mailbox);
$folder->setMailboxName($mailboxName);
$special = $this->getSpecialFolder($mailboxName);
$folder->setSpecial($special);
$folder->setName($special ? $this->specialFoldersNames[$special] : $this->getName($mailboxName, $mailbox->getDelimiter()));
$folder->setOrder($special ? $this->specialFoldersOrder[$special] : $this->getOrder($mailboxName, $mailbox->getDelimiter()));
$folder->setLevel($this->getFolderLevel($mailboxName, $mailbox->getDelimiter()));
$folder->setDelimiter($mailbox->getDelimiter());
$this->folders[] = $folder;
}

usort($this->folders, [$this, "sortByOrder"]);

return $this->folders;
}


protected function sortByMailboxName(MailboxInterface $a, MailboxInterface $b)
{
return ($a->getName() < $b->getName()) ? -1 : 1;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can use here the Spaceship operator

}

protected function sortByOrder(ParsedMailbox $a, ParsedMailbox $b)
{
return ($a->getOrder() < $b->getOrder()) ? -1 : 1;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can use here the Spaceship operator

}

/**
* @return ParsedMailbox[]
*/
public function getFolders()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add the return-type

{
if (!$this->folders) {
$this->parse();
}

return $this->folders;
}

/**
* @return mixed
*/
public function getTreeStructure()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add the return-type

{
if (!$this->treeStructure) {
$treeParser = new MailboxesTreeParser();
$this->treeStructure = $treeParser->parse($this->getFolders());
}

return $this->treeStructure;
}

/**
* @param array $specialFoldersNames
*/
public function setSpecialFoldersNames(array $specialFoldersNames)
{
$this->specialFoldersNames = $specialFoldersNames;
}

protected function getSpecialFolder($mailboxName)
{
foreach ($this->specialFoldersIds AS $specialFolderKind => $names) {
$lower = mb_strtolower($mailboxName);
foreach ($names AS $name) {
if ($lower === mb_strtolower($name)) {
return $specialFolderKind;
}
}
}

return null;
}

private function getName($mailboxName, $delimiter = '.')
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add type-hints, return-type and doc-block

{
$e = explode($delimiter, $mailboxName);

return ucfirst($e[count($e) - 1]);
}

private function getOrder($mailboxName, $delimiter)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add type-hints, return-type and doc-block

{
if ($this->folders[$mailboxName]['special']) {
return $this->specialFoldersOrder[$this->folders[$mailboxName]['special']];
} else {
$e = explode($delimiter, $mailboxName);

$level = count($e) - 1;
if ($e[0] === 'INBOX' && $level === 1) {
$level = -1;
}
if ($e[1]) {
array_pop($e);
$parentMailboxName = implode($delimiter, $e);
if ($level === -1) {
$multiplier = 100;
} else {
$power = -1 * ($level * 2);
$multiplier = pow(10, ($power));
}
$parsedParent = null;
/** @var ParsedMailbox $parsedMailbox */
foreach ($this->folders as $parsedMailbox) {
if ($parsedMailbox->getMailboxName() === $parentMailboxName) {
$parsedParent = $parsedMailbox;
break;
}
}
if ($parsedParent) {
$parsedParent->setSubfolders($parsedParent->getSubfolders() + 1);
$order = ($parsedParent->getOrder() + ($parsedParent->getSubfolders() * $multiplier));
} else {
$order = 10;
}
} else {
$order = 10;
}

return $order;
}
}

public function getMailboxNameForSpecial($special)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add type-hints, return-type and doc-block

{
/** @var ParsedMailbox $parsedMailbox */
foreach ($this->folders as $parsedMailbox) {
if ($parsedMailbox->getSpecial() === $special) {
return $parsedMailbox->getMailboxName();
}
}

return null;
}

public function addSpecialFolderId($specialFolder, $id)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add type-hints, return-type and doc-block

{
$this->specialFoldersIds[$specialFolder][] = $id;
}

private function getFolderLevel($mailboxName, $delimiter)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add type-hints, return-type and doc-block

{
$e = explode($delimiter, $mailboxName);

return count($e);
}
}
77 changes: 77 additions & 0 deletions src/MailboxesParser/MailboxesTreeParser.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
<?php
/**
* Created by PhpStorm.
* User: Lukasz
* Date: 2017-11-10
* Time: 19:50
*/

namespace Ddeboer\Imap\MailboxesParser;


class MailboxesTreeParser
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please make it final and add an Interface for it

{
/**
* @param ParsedMailbox[] $input
*
* @return array
*/
public function parse($input)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add type-hints and the return-type

{
$newKeys = array_map(
function ($key, $value) {
/**
* @var ParsedMailbox $value
*/
$k = explode($value->getDelimiter(), $value->getMailboxName());
$newkey = [];
foreach ($k as $segment) {
$newkey[] = $segment;
$newkey[] = "subfolders";
}

return implode(".", $newkey);
}, array_keys($input), $input);

$arrayToParse = [];
foreach ($newKeys AS $index => $value) {
$k = explode(".", $value);
$keyWithoutLast = implode('.', array_splice($k, 0, -1));
$arrayToParse[$value] = [];
if ($input[$index]) {
/** @var ParsedMailbox $parsedMailbox */
$parsedMailbox = $input[$index];
$arrayToParse[$keyWithoutLast . '.mailboxName'] = $parsedMailbox->getMailboxName();
$arrayToParse[$keyWithoutLast . '.name'] = $parsedMailbox->getName();
}
}

$res = [];
array_walk($arrayToParse, function ($value, $key) use (&$res) {
$this->set($res, $key, $value);
});

return $res;
}

private function set(&$array, $key, $value)
{
if (is_null($key)) {
return $array = $value;
}
$keys = explode('.', $key);
while (count($keys) > 1) {
$key = array_shift($keys);
// If the key doesn't exist at this depth, we will just create an empty array
// to hold the next value, allowing us to create the arrays to hold final
// values at the correct depth. Then we'll keep digging into the array.
if (!isset($array[$key]) || !is_array($array[$key])) {
$array[$key] = [];
}
$array = &$array[$key];
}
$array[array_shift($keys)] = $value;

return $array;
}
}
Loading