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

Suggestion: tree structure of mailboxes #261

Closed
piernik opened this issue Nov 6, 2017 · 12 comments
Closed

Suggestion: tree structure of mailboxes #261

piernik opened this issue Nov 6, 2017 · 12 comments

Comments

@piernik
Copy link

piernik commented Nov 6, 2017

It's a suggestion. Creating tree structure of mailboxes.
In addition You could mark special folders ex. INBOX as inbox, INBOX.sent as sent and so on.

@Slamdunk
Copy link
Collaborator

Slamdunk commented Nov 6, 2017

Hi, can you elaborate your idea with deeper details?

@piernik
Copy link
Author

piernik commented Nov 6, 2017

Yes. I attach screenshot.

There is folder tree structure. Some folders are special (like "Odebrane"=>"Inbox", "Wysłane"=>"Sent" and so on) - You can mark them in structure.
Some folder are subsolders (Snoozed).
You can prepare that tree as array

maile_structura

@Slamdunk
Copy link
Collaborator

Slamdunk commented Nov 6, 2017

IMAP servers are too heterogeneous to have a unique way for handling folders, and a lot of them support features that others don't.

For this very reason the imap extension returns some flags to help identify them, and this library directly reports them without prior elaboration: without an accepted and used standard we can't do anything but return raw attributes.

If you are able to code your idea and to test it a least against Dovecot and GMail, feel free to open a PR 👍

@piernik
Copy link
Author

piernik commented Nov 6, 2017

I totaly understand - I'll try to make my own class. If it's good I'll post it here

@Slamdunk
Copy link
Collaborator

Slamdunk commented Nov 6, 2017

I'm closing the issue for now; if you have any updates in the future, feel free to reopen in.

@Slamdunk Slamdunk closed this as completed Nov 6, 2017
@piernik
Copy link
Author

piernik commented Nov 7, 2017

I've created two classes.

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

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

    protected $specialFoldersIds = [
        self::INBOX     => ['inbox',],
        self::SENT      => ['sent', 'sent messages', 'INBOX.Sent', 'INBOX.Wysłane', '[Gmail]/Sent Mail', 'elementy wysłane', 'wysłane'],
        self::DRAFT     => ['drafts', 'INBOX.Drafts', '[Gmail]/Drafts', 'szkice',],
        self::SPAM      => ['spam', 'INBOX.spam', '[Gmail]/spam'],
        self::TRASH     => ['trash', 'bin', 'INBOX.trash', '[Gmail]/trash', 'kosz'],
        self::TEMPLATES => ['templates', 'szablony'],
        self::ARCHIVES  => ['archives', 'archiwum'],
    ];

    protected $specialFoldersNames = [
        self::DRAFT     => 'Szkice',
        self::INBOX     => 'Odebrane',
        self::SENT      => 'Wysłane',
        self::SPAM      => 'Spam',
        self::TRASH     => 'Kosz',
        self::TEMPLATES => 'Szablony',
        self::ARCHIVES  => 'Archiwum',
    ];

    protected $specialFoldersOrder = [
        self::INBOX     => 1,
        self::SENT      => 2,
        self::DRAFT     => 3,
        self::SPAM      => 5000,
        self::TRASH     => 6000,
        self::TEMPLATES => 4,
        self::ARCHIVES  => 4000,
    ];

    /**
     * MailboxesTree constructor.
     *
     * @param MailboxInterface[] $mailboxes
     */
    public function __construct($mailboxes)
    {
        $this->mailboxes = $mailboxes;
    }

    protected function parse()
    {
        $this->folders = [];
        foreach ($this->mailboxes AS $k => $mailbox) {
            $mailboxName = $mailbox->getName();
            $this->folders[$mailboxName] = [
                'special' => $this->getSpecialFolder($mailboxName),
            ];
            $this->folders[$mailboxName]['mailboxName'] = $mailboxName;
            $this->folders[$mailboxName]['name'] = $this->getName($mailboxName, $mailbox->getDelimiter());
            $this->folders[$mailboxName]['order'] = $this->getOrder($mailboxName, $mailbox->getDelimiter());
            $this->folders[$mailboxName]['mailbox'] = $mailbox;
        }

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

        return $this->folders;
    }

    /**
     * @return mixed
     */
    public function getFolders()
    {
        if (!$this->folders) {
            $this->parse();
        }

        return $this->folders;
    }

    /**
     * @return mixed
     */
    public function getTreeStructure()
    {
        if (!$this->treeStructure) {
            $treeParser = new MailboxesTreeParser();
            $this->treeStructure = $treeParser->parse($this->getFolders());
        }

        return $this->treeStructure;
    }

    protected function sortByOrder($a, $b)
    {
        return ($a['order'] < $b['order']) ? -1 : 1;
    }

    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 = '.')
    {
        if ($this->folders[$mailboxName]['special']) {
            return $this->specialFoldersNames[$this->folders[$mailboxName]['special']];
        } else {
            $e = explode($delimiter, $mailboxName);

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

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

            return 100 * count($e);
        }
    }
}
class MailboxesTreeParser
{
    public function parse($input, $delimiter = '.')
    {
        $newKeys = array_map(function ($key) use ($delimiter) {
            $k = explode($delimiter, $key);
            $newkey = [];
            foreach ($k as $segment) {
                $newkey[] = $segment;
                $newkey[] = "subfolders";
            }

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

        $arrayToParse = [];
        foreach ($newKeys AS $key) {
            $k = explode(".", $key);
            $keyWithoutLast = implode('.', array_splice($k, 0, -1));
            $arrayToParse[$key] = [];
            $inputKey = str_replace('.', $delimiter, str_replace('.subfolders', '', $key));
            if ($input[$inputKey]) {
                $arrayToParse[$keyWithoutLast . '.mailboxName'] = $inputKey;
                foreach ($input[$inputKey] AS $k => $w) {
                    $arrayToParse[$keyWithoutLast . '.' . $k] = $w;
                }
            }
        }

        $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;
    }
}

usage:

$parser = new MailboxesParser($mailboxes);
$orderedFlatFolders = $parser->getFolders();
$treeStructure = $parser->getTreeStructure();

If You find those useful feel free to use them.

@piernik
Copy link
Author

piernik commented Nov 7, 2017

And simple tests:

class MailboxesParserTest extends TestCase
{
    /** @var MailboxInterface[] */
    protected $mailboxes;

    protected function setUp()
    {
        parent::setUp();
        $this->mailboxes = [
            $this->createMailboxMock('INBOX.Drafts'),
            $this->createMailboxMock('INBOX.normal'),
            $this->createMailboxMock('INBOX'),
            $this->createMailboxMock('INBOX.normal.sub'),
        ];
    }

    public function testParsera()
    {
        $parser = new MailboxesParser($this->mailboxes);
        $foldery = $parser->getFolders();
        $spodziewane = [
            'INBOX' => [
                'special' => 'inbox',
                'name'    => 'Odebrane',
            ],
        ];
        $this->assertCount(4, $foldery);
        $this->assertArrayHasKey('INBOX', $foldery);
        $this->assertEquals('Odebrane', $foldery['INBOX']['name']);
        $this->assertArrayHasKey('INBOX.Drafts', $foldery);
    }

    private function createMailboxMock($mailboxName)
    {
        $mailbox = $this->createMock(MailboxInterface::class);
        $mailbox->method('getName')
            ->willReturn($mailboxName);

        $mailbox->method('getDelimiter')
            ->willReturn('.');

        return $mailbox;
    }
}
class MailboxesTreeParserTest extends TestCase
{
    public function testParsowania()
    {
        $dane = [
            'inbox'               => ['name' => 'Inbox'],
            'inbox.first'         => ['name' => 'First'],
            'inbox.second'        => ['name' => 'Second'],
            'inbox.second.other'  => ['name' => 'Second Other'],
            'inbox.third.another' => ['name' => 'Third Another'],
        ];
        $parser = new MailboxesTreeParser();
        $zwroc = $parser->parse($dane);
        $spodziewane = [
            'inbox' => [
                'name'       => 'Inbox',
                'mailboxName'    => 'inbox',
                'subfolders' => [
                    'first'  => [
                        'name'       => 'First',
                        'mailboxName'    => 'inbox.first',
                        'subfolders' => [],
                    ],
                    'second' => [
                        'name'       => 'Second',
                        'mailboxName'    => 'inbox.second',
                        'subfolders' => [
                            'other' => [
                                'name'       => 'Second Other',
                                'subfolders' => [],
                                'mailboxName'    => 'inbox.second.other',
                            ],
                        ],
                    ],
                    'third'  => [
                        'subfolders' => [
                            'another' => [
                                'name'       => 'Third Another',
                                'subfolders' => [],
                                'mailboxName'    => 'inbox.third.another',
                            ],
                        ],
                    ],
                ],
            ],
        ];
        $this->assertEquals($spodziewane, $zwroc);
    }

    public function testParsowaniaKreska()
    {
        $dane = [
            'inbox'               => ['name' => 'Inbox'],
            'inbox|first'         => ['name' => 'First'],
            'inbox|second'        => ['name' => 'Second'],
        ];
        $parser = new MailboxesTreeParser();
        $zwroc = $parser->parse($dane, '|');
        $spodziewane = [
            'inbox' => [
                'name'       => 'Inbox',
                'mailboxName'    => 'inbox',
                'subfolders' => [
                    'first'  => [
                        'name'       => 'First',
                        'mailboxName'    => 'inbox|first',
                        'subfolders' => [],
                    ],
                    'second' => [
                        'name'       => 'Second',
                        'mailboxName'    => 'inbox|second',
                        'subfolders' => [],
                    ]
                ],
            ],
        ];
        $this->assertEquals($spodziewane, $zwroc);
    }
}

Sorry for polish ;)

@wujku
Copy link
Contributor

wujku commented Nov 7, 2017

@piernik could you create a PR? Your parser looks good.
I suggest to make specialFolders* properties more configurable, maybe as parameters of the constructor.

@Slamdunk Slamdunk reopened this Nov 8, 2017
@piernik
Copy link
Author

piernik commented Nov 8, 2017

I don't know how - I'm not github fluent.
And Yes. It needs more touch... Maybe chnage protected $folders; and protected $treeStructure; separate class (for IDE). Right now I have no time - I did that two casses because I needed them.

@Slamdunk
Copy link
Collaborator

Slamdunk commented Nov 8, 2017

@piernik Github is trivial, the requirement is knowing Git.

If you know Git, then you can follow the Github helps, that sums up in:

  1. Fork this repo
  2. Clone your fork in your computer
  3. Create a branch for the feature
  4. Add code, add tests, commit them to the new branch
  5. Push the new branch to your fork
  6. Come back in the ddeboer/imap home page
  7. Click the New pull request button

😉

@piernik
Copy link
Author

piernik commented Nov 21, 2017

I've uploaded parser: #268

@Slamdunk
Copy link
Collaborator

Closed in favour of #268

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants