Skip to content

Commit

Permalink
Detect and persist the personal namespace
Browse files Browse the repository at this point in the history
Signed-off-by: Christoph Wurst <christoph@winzerhof-wurst.at>
  • Loading branch information
ChristophWurst committed Sep 1, 2020
1 parent df94a5e commit 5603913
Show file tree
Hide file tree
Showing 8 changed files with 233 additions and 5 deletions.
2 changes: 1 addition & 1 deletion appinfo/info.xml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
- **🙈 We’re not reinventing the wheel!** Based on the great [Horde](http://horde.org) libraries.
- **📬 Want to host your own mail server?** We don’t have to reimplement this as you could set up [Mail-in-a-Box](https://mailinabox.email)!
]]></description>
<version>1.5.0-alpha2</version>
<version>1.5.0-alpha3</version>
<licence>agpl</licence>
<author>Christoph Wurst</author>
<author>Roeland Jago Douma</author>
Expand Down
5 changes: 5 additions & 0 deletions lib/Db/MailAccount.php
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@
* @method void setOrder(int $order)
* @method bool getShowSubscribedOnly()
* @method void setShowSubscribedOnly(bool $showSubscribedOnly)
* @method string|null getPersonalNamespace()
* @method void setPersonalNamespace(string|null $personalNamespace)
*/
class MailAccount extends Entity {
protected $userId;
Expand All @@ -90,6 +92,7 @@ class MailAccount extends Entity {
protected $provisioned;
protected $order;
protected $showSubscribedOnly;
protected $personalNamespace;

/**
* @param array $params
Expand Down Expand Up @@ -146,6 +149,7 @@ public function __construct(array $params=[]) {
$this->addType('provisioned', 'bool');
$this->addType('order', 'integer');
$this->addType('showSubscribedOnly', 'boolean');
$this->addType('personalNamespace', 'string');
}

/**
Expand All @@ -166,6 +170,7 @@ public function toJson() {
'editorMode' => $this->getEditorMode(),
'provisioned' => $this->getProvisioned(),
'showSubscribedOnly' => $this->getShowSubscribedOnly(),
'personalNamespace' => $this->getPersonalNamespace(),
];

if (!is_null($this->getOutboundHost())) {
Expand Down
23 changes: 23 additions & 0 deletions lib/IMAP/MailboxSync.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,10 @@

namespace OCA\Mail\IMAP;

use Horde_Imap_Client;
use Horde_Imap_Client_Data_Namespace;
use Horde_Imap_Client_Exception;
use Horde_Imap_Client_Namespace_List;
use OCA\Mail\Exception\ServiceException;
use function in_array;
use function json_encode;
Expand Down Expand Up @@ -81,6 +84,16 @@ public function sync(Account $account, bool $force = false): void {
}

$client = $this->imapClientFactory->getClient($account);
try {
$namespaces = $client->getNamespaces([], [
'ob_return' => true,
]);
$account->getMailAccount()->setPersonalNamespace(
$this->getPersonalNamespace($namespaces)
);
} catch (Horde_Imap_Client_Exception $e) {
$this->logger->debug('Getting namespaces for account ' . $account->getId() . ' failed: ' . $e->getMessage());
}

try {
$folders = $this->folderMapper->getFolders($account, $client);
Expand Down Expand Up @@ -125,6 +138,16 @@ private function persist(Account $account, array $folders, array $existing): voi
$this->mailAccountMapper->update($account->getMailAccount());
}

private function getPersonalNamespace(Horde_Imap_Client_Namespace_List $namespaces): ?string {
foreach ($namespaces as $namespace) {
/** @var Horde_Imap_Client_Data_Namespace $namespace */
if ($namespace->type === Horde_Imap_Client::NS_PERSONAL) {
return $namespace->name;
}
}
return null;
}

private function updateMailboxFromFolder(Folder $folder, Mailbox $mailbox): void {
$mailbox->setDelimiter($folder->getDelimiter());
$mailbox->setAttributes(json_encode($folder->getAttributes()));
Expand Down
31 changes: 31 additions & 0 deletions lib/Migration/Version1050Date20200831124954.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

declare(strict_types=1);

namespace OCA\Mail\Migration;

use Closure;
use OCP\DB\ISchemaWrapper;
use OCP\Migration\IOutput;
use OCP\Migration\SimpleMigrationStep;

class Version1050Date20200831124954 extends SimpleMigrationStep {

/**
* @param IOutput $output
* @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
* @param array $options
* @return ISchemaWrapper
*/
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options) {
/** @var ISchemaWrapper $schema */
$schema = $schemaClosure();

$accountsTable = $schema->getTable('mail_accounts');
$accountsTable->addColumn('personal_namespace', 'string', [
'notnull' => false,
]);

return $schema;
}
}
8 changes: 6 additions & 2 deletions src/store/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -156,10 +156,14 @@ export default {
commit('removeMailbox', { id: mailbox.databaseId })
},
async createMailbox({ commit }, { account, name }) {
const mailbox = await createMailbox(account.id, name)
console.debug(`mailbox ${name} created for account ${account.id}`, { mailbox })
const prefixed = (account.personalNamespace && !name.startsWith(account.personalNamespace))
? account.personalNamespace + name
: name
const mailbox = await createMailbox(account.id, prefixed)
console.debug(`mailbox ${prefixed} created for account ${account.id}`, { mailbox })
commit('addMailbox', { account, mailbox })
commit('expandAccount', account.id)
return mailbox
},
moveAccount({ commit, getters }, { account, up }) {
const accounts = getters.accounts
Expand Down
20 changes: 18 additions & 2 deletions src/store/mutations.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
*/

import { curry } from 'ramda'
import escapeRegExp from 'lodash/fp/escapeRegExp'
import orderBy from 'lodash/fp/orderBy'
import sortedUniqBy from 'lodash/fp/sortedUniqBy'
import Vue from 'vue'
Expand All @@ -34,11 +35,26 @@ const addMailboxToState = curry((state, account, mailbox) => {
mailbox.envelopeLists = {}

// Add all mailboxes (including submailboxes to state, but only toplevel to account
if (mailbox.name.includes(mailbox.delimiter)) {
const nameWithoutPrefix = account.personalNamespace
? mailbox.name.replace(new RegExp(escapeRegExp(account.personalNamespace)), '')
: mailbox.name
if (nameWithoutPrefix.includes(mailbox.delimiter)) {
/**
* Sub-mailbox, e.g. 'Archive.2020' or 'INBOX.Archive.2020'
*/
mailbox.displayName = mailbox.name.substr(mailbox.name.lastIndexOf(mailbox.delimiter) + 1)
mailbox.path = mailbox.name.substr(0, mailbox.name.lastIndexOf(mailbox.delimiter))
} else if (account.personalNamespace && mailbox.name.startsWith(account.personalNamespace)) {
/**
* Top-level mailbox, but with a personal namespace, e.g. 'INBOX.Sent'
*/
mailbox.displayName = nameWithoutPrefix
mailbox.path = account.personalNamespace
} else {
mailbox.displayName = mailbox.name
/**
* Top-level mailbox, e.g. 'INBOX' or 'Draft'
*/
mailbox.displayName = nameWithoutPrefix
mailbox.path = ''
}

Expand Down
75 changes: 75 additions & 0 deletions src/tests/unit/store/actions.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { curry, prop, range, reverse } from 'ramda'
import orderBy from 'lodash/fp/orderBy'

import actions from '../../../store/actions'
import * as MailboxService from '../../../service/MailboxService'
import * as MessageService from '../../../service/MessageService'
import * as NotificationService from '../../../service/NotificationService'
import { UNIFIED_ACCOUNT_ID, UNIFIED_INBOX_ID } from '../../../store/constants'
Expand All @@ -38,6 +39,8 @@ describe('Vuex store actions', () => {
let context

beforeEach(() => {
sinon.stub(MailboxService, 'create')

context = {
commit: sinon.stub(),
dispatch: sinon.stub(),
Expand All @@ -55,6 +58,78 @@ describe('Vuex store actions', () => {
sinon.restore()
})

it('creates a mailbox', async () => {
const account = {
id: 13,
personalNamespace: '',
}
const name = 'Important'
const mailbox = {
'name': 'Important',
}
MailboxService.create.withArgs(13, 'Important').returns(mailbox)

const result = await actions.createMailbox(context, {account, name})

expect(result).to.deep.equal(mailbox)
expect(context.commit).to.have.been.calledTwice
expect(context.commit).to.have.been.calledWith('addMailbox', { account, mailbox})
})

it('creates a sub-mailbox', async () => {
const account = {
id: 13,
personalNamespace: '',
}
const name = 'Archive.2020'
const mailbox = {
'name': 'Archive.2020',
}
MailboxService.create.withArgs(13, 'Archive.2020').returns(mailbox)

const result = await actions.createMailbox(context, {account, name})

expect(result).to.deep.equal(mailbox)
expect(context.commit).to.have.been.calledTwice
expect(context.commit).to.have.been.calledWith('addMailbox', { account, mailbox})
})

it('adds a prefix to new mailboxes if the account has a personal namespace', async () => {
const account = {
id: 13,
personalNamespace: 'INBOX.',
}
const name = 'Important'
const mailbox = {
'name': 'INBOX.Important',
}
MailboxService.create.withArgs(13, 'INBOX.Important').returns(mailbox)

const result = await actions.createMailbox(context, {account, name})

expect(result).to.deep.equal(mailbox)
expect(context.commit).to.have.been.calledTwice
expect(context.commit).to.have.been.calledWith('addMailbox', { account, mailbox})
})

it('adds no prefix to new sub-mailboxes if the account has a personal namespace', async () => {
const account = {
id: 13,
personalNamespace: 'INBOX.',
}
const name = 'INBOX.Archive.2020'
const mailbox = {
'name': 'INBOX.Archive.2020',
}
MailboxService.create.withArgs(13, 'INBOX.Archive.2020').returns(mailbox)

const result = await actions.createMailbox(context, {account, name})

expect(result).to.deep.equal(mailbox)
expect(context.commit).to.have.been.calledTwice
expect(context.commit).to.have.been.calledWith('addMailbox', { account, mailbox})
})

it('combines unified inbox even if no inboxes are present', () => {
context.getters.getMailbox.returns({
isUnified: true,
Expand Down
74 changes: 74 additions & 0 deletions src/tests/unit/store/mutations.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,80 @@ describe('Vuex store mutations', () => {
})
})

it('adds an account with a personal namespace', () => {
const state = {
accountList: [],
accounts: {},
envelopes: {},
mailboxes: {},
}

mutations.addAccount(state, {
accountId: 13,
id: 13,
mailboxes: [
{
databaseId: 345,
name: 'INBOX',
delimiter: '.',
specialUse: ['inbox'],
specialRole: 'inbox',
},
{
databaseId: 346,
name: 'INBOX.Sent',
delimiter: '.',
specialUse: ['sent'],
specialRole: 'sent',
},
],
personalNamespace: 'INBOX.',
})

expect(state).to.deep.equal({
accountList: [13],
accounts: {
13: {
accountId: 13,
id: 13,
mailboxes: [
345,
346,
],
collapsed: true,
personalNamespace: 'INBOX.',
},
},
envelopes: {},
mailboxes: {
345: {
accountId: 13,
databaseId: 345,
name: 'INBOX',
displayName: 'INBOX',
specialUse: ['inbox'],
specialRole: 'inbox',
delimiter: '.',
envelopeLists: {},
path: '',
mailboxes: [],
},
346: {
accountId: 13,
databaseId: 346,
name: 'INBOX.Sent',
displayName: 'Sent',
specialUse: ['sent'],
specialRole: 'sent',
delimiter: '.',
envelopeLists: {},
path: 'INBOX.',
mailboxes: [],
}
},
})
})

it('adds an account with two levels of mailboxes', () => {
const state = {
accountList: [],
Expand Down

0 comments on commit 5603913

Please sign in to comment.