feat: add internal addresses
Signed-off-by: Hamza Mahjoubi <hamzamahjoubi221@gmail.com>
This commit is contained in:
Родитель
6b86b4d4e2
Коммит
aab2db92a6
|
@ -355,6 +355,21 @@ return [
|
|||
'url' => '/api/trustedsenders',
|
||||
'verb' => 'GET'
|
||||
],
|
||||
[
|
||||
'name' => 'internal_address#setAddress',
|
||||
'url' => '/api/internalAddress/{address}',
|
||||
'verb' => 'PUT'
|
||||
],
|
||||
[
|
||||
'name' => 'internal_address#removeAddress',
|
||||
'url' => '/api/internalAddress/{address}',
|
||||
'verb' => 'DELETE'
|
||||
],
|
||||
[
|
||||
'name' => 'internal_address#list',
|
||||
'url' => '/api/internalAddress',
|
||||
'verb' => 'GET'
|
||||
],
|
||||
[
|
||||
'name' => 'sieve#updateAccount',
|
||||
'url' => '/api/sieve/account/{id}',
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\Mail\Contracts;
|
||||
|
||||
use OCA\Mail\Db\InternalAddress;
|
||||
|
||||
interface IInternalAddressService {
|
||||
public function isInternal(string $uid, string $address): bool;
|
||||
|
||||
public function add(string $uid, string $address, string $type, ?bool $trust = true);
|
||||
|
||||
/**
|
||||
* @param string $uid
|
||||
* @return InternalAddress[]
|
||||
*/
|
||||
public function getInternalAddresses(string $uid): array;
|
||||
}
|
|
@ -0,0 +1,90 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\Mail\Controller;
|
||||
|
||||
use OCA\Mail\AppInfo\Application;
|
||||
use OCA\Mail\Http\JsonResponse;
|
||||
use OCA\Mail\Http\TrapError;
|
||||
use OCA\Mail\Service\InternalAddressService;
|
||||
use OCP\AppFramework\Controller;
|
||||
use OCP\AppFramework\Http;
|
||||
use OCP\AppFramework\Http\Attribute\NoAdminRequired;
|
||||
use OCP\IRequest;
|
||||
|
||||
class InternalAddressController extends Controller {
|
||||
private ?string $uid;
|
||||
|
||||
public function __construct(IRequest $request,
|
||||
?string $userId,
|
||||
private InternalAddressService $internalAddressService) {
|
||||
parent::__construct(Application::APP_ID, $request);
|
||||
|
||||
$this->internalAddressService = $internalAddressService;
|
||||
$this->uid = $userId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @NoAdminRequired
|
||||
*
|
||||
* @param string $address
|
||||
* @param string $type
|
||||
* @return JsonResponse
|
||||
*/
|
||||
#[TrapError]
|
||||
public function setAddress(string $address, string $type): JsonResponse {
|
||||
$address = $this->internalAddressService->add(
|
||||
$this->uid,
|
||||
$address,
|
||||
$type
|
||||
)->jsonSerialize();
|
||||
|
||||
return JsonResponse::success($address, Http::STATUS_CREATED);
|
||||
}
|
||||
|
||||
/**
|
||||
* @NoAdminRequired
|
||||
*
|
||||
* @param string $address
|
||||
* @param string $type
|
||||
* @return JsonResponse
|
||||
*/
|
||||
#[TrapError]
|
||||
public function removeAddress(string $address, string $type): JsonResponse {
|
||||
if($this->uid === null) {
|
||||
return JsonResponse::error('User not found', Http::STATUS_UNAUTHORIZED);
|
||||
}
|
||||
|
||||
$this->internalAddressService->add(
|
||||
$this->uid,
|
||||
$address,
|
||||
$type,
|
||||
false
|
||||
);
|
||||
|
||||
return JsonResponse::success();
|
||||
}
|
||||
|
||||
/**
|
||||
* @NoAdminRequired
|
||||
*
|
||||
* @return JsonResponse
|
||||
*/
|
||||
#[TrapError]
|
||||
public function list(): JsonResponse {
|
||||
if($this->uid === null) {
|
||||
return JsonResponse::error('User not found', Http::STATUS_UNAUTHORIZED);
|
||||
}
|
||||
$list = $this->internalAddressService->getInternalAddresses(
|
||||
$this->uid
|
||||
);
|
||||
|
||||
return JsonResponse::success($list);
|
||||
}
|
||||
}
|
|
@ -19,6 +19,7 @@ use OCA\Mail\Service\AccountService;
|
|||
use OCA\Mail\Service\AiIntegrations\AiIntegrationsService;
|
||||
use OCA\Mail\Service\AliasesService;
|
||||
use OCA\Mail\Service\Classification\ClassificationSettingsService;
|
||||
use OCA\Mail\Service\InternalAddressService;
|
||||
use OCA\Mail\Service\OutboxService;
|
||||
use OCA\Mail\Service\SmimeService;
|
||||
use OCA\Viewer\Event\LoadViewer;
|
||||
|
@ -70,6 +71,7 @@ class PageController extends Controller {
|
|||
private IUserManager $userManager;
|
||||
private ?IAvailabilityCoordinator $availabilityCoordinator;
|
||||
private ClassificationSettingsService $classificationSettingsService;
|
||||
private InternalAddressService $internalAddressService;
|
||||
|
||||
public function __construct(string $appName,
|
||||
IRequest $request,
|
||||
|
@ -91,7 +93,8 @@ class PageController extends Controller {
|
|||
AiIntegrationsService $aiIntegrationsService,
|
||||
IUserManager $userManager,
|
||||
ContainerInterface $container,
|
||||
ClassificationSettingsService $classificationSettingsService) {
|
||||
ClassificationSettingsService $classificationSettingsService,
|
||||
InternalAddressService $internalAddressService, ) {
|
||||
parent::__construct($appName, $request);
|
||||
|
||||
$this->urlGenerator = $urlGenerator;
|
||||
|
@ -112,6 +115,7 @@ class PageController extends Controller {
|
|||
$this->aiIntegrationsService = $aiIntegrationsService;
|
||||
$this->userManager = $userManager;
|
||||
$this->classificationSettingsService = $classificationSettingsService;
|
||||
$this->internalAddressService = $internalAddressService;
|
||||
|
||||
// TODO: inject directly if support for nextcloud < 28 is dropped
|
||||
try {
|
||||
|
@ -173,6 +177,16 @@ class PageController extends Controller {
|
|||
$this->tagMapper->getAllTagsForUser($this->currentUserId)
|
||||
);
|
||||
|
||||
$this->initialStateService->provideInitialState(
|
||||
'internal-addresses-list',
|
||||
$this->internalAddressService->getInternalAddresses($this->currentUserId)
|
||||
);
|
||||
|
||||
$this->initialStateService->provideInitialState(
|
||||
'internal-addresses',
|
||||
$this->preferences->getPreference($this->currentUserId, 'internal-addresses', false)
|
||||
);
|
||||
|
||||
$this->initialStateService->provideInitialState(
|
||||
'sort-order',
|
||||
$this->preferences->getPreference($this->currentUserId, 'sort-order', 'newest')
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\Mail\Db;
|
||||
|
||||
use JsonSerializable;
|
||||
use OCP\AppFramework\Db\Entity;
|
||||
use ReturnTypeWillChange;
|
||||
|
||||
/**
|
||||
* @method setAddress(string $address): void
|
||||
* @method getAddress(): string
|
||||
* @method setUserId(string $userId): void
|
||||
* @method getUserId(): string
|
||||
* @method setType(string $type): void
|
||||
* @method getType(): string
|
||||
*/
|
||||
class InternalAddress extends Entity implements JsonSerializable {
|
||||
|
||||
protected $address ;
|
||||
protected $userId;
|
||||
protected $type;
|
||||
|
||||
#[ReturnTypeWillChange]
|
||||
public function jsonSerialize() {
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'address' => $this->address,
|
||||
'uid' => $this->userId,
|
||||
'type' => $this->type,
|
||||
];
|
||||
}
|
||||
}
|
|
@ -0,0 +1,103 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\Mail\Db;
|
||||
|
||||
use OCP\AppFramework\Db\DoesNotExistException;
|
||||
use OCP\AppFramework\Db\QBMapper;
|
||||
use OCP\IDBConnection;
|
||||
|
||||
/**
|
||||
* @template-extends QBMapper<InternalAddress>
|
||||
*/
|
||||
class InternalAddressMapper extends QBMapper {
|
||||
public function __construct(IDBConnection $db) {
|
||||
parent::__construct($db, 'mail_internal_address');
|
||||
}
|
||||
|
||||
public function exists(string $uid, string $address): bool {
|
||||
|
||||
$emailObject = new \Horde_Mail_Rfc822_Address($address);
|
||||
$host = $emailObject->host;
|
||||
$qb = $this->db->getQueryBuilder();
|
||||
|
||||
$select = $qb->select('*')
|
||||
->from($this->getTableName())
|
||||
->where(
|
||||
$qb->expr()->orX(
|
||||
$qb->expr()->andX(
|
||||
$qb->expr()->eq('address', $qb->createNamedParameter($address)),
|
||||
$qb->expr()->eq('type', $qb->createNamedParameter('individual'))
|
||||
),
|
||||
$qb->expr()->andX(
|
||||
$qb->expr()->eq('address', $qb->createNamedParameter($host)),
|
||||
$qb->expr()->eq('type', $qb->createNamedParameter('domain'))
|
||||
)
|
||||
),
|
||||
$qb->expr()->eq('user_id', $qb->createNamedParameter($uid))
|
||||
);
|
||||
|
||||
$rows = $this->findEntities($select);
|
||||
|
||||
return $rows !== [];
|
||||
}
|
||||
|
||||
public function create(string $uid, string $address, string $type): int {
|
||||
|
||||
$address = InternalAddress::fromParams([
|
||||
'userId' => $uid,
|
||||
'address' => $address,
|
||||
'type' => $type,
|
||||
]);
|
||||
|
||||
$result = $this->insert($address);
|
||||
|
||||
return $result->getId();
|
||||
}
|
||||
|
||||
public function remove(string $uid, string $address, string $type): void {
|
||||
$qb = $this->db->getQueryBuilder();
|
||||
|
||||
$delete = $qb->delete($this->getTableName())
|
||||
->where(
|
||||
$qb->expr()->eq('user_id', $qb->createNamedParameter($uid)),
|
||||
$qb->expr()->eq('address', $qb->createNamedParameter($address)),
|
||||
$qb->expr()->eq('type', $qb->createNamedParameter($type))
|
||||
);
|
||||
|
||||
$delete->executeStatement();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $uid
|
||||
* @return InternalAddress[]
|
||||
*/
|
||||
public function findAll(string $uid): array {
|
||||
$qb = $this->db->getQueryBuilder();
|
||||
$select = $qb->select('*')
|
||||
->from($this->getTableName())
|
||||
->where($qb->expr()->eq('user_id', $qb->createNamedParameter($uid)));
|
||||
return $this->findEntities($select);
|
||||
}
|
||||
|
||||
public function find(string $uid, string $address): ?InternalAddress {
|
||||
$qb = $this->db->getQueryBuilder();
|
||||
$select = $qb->select('*')
|
||||
->from($this->getTableName())
|
||||
->where(
|
||||
$qb->expr()->eq('user_id', $qb->createNamedParameter($uid)),
|
||||
$qb->expr()->eq('address', $qb->createNamedParameter($address))
|
||||
);
|
||||
try {
|
||||
return $this->findEntity($select);
|
||||
} catch (DoesNotExistException $e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\Mail\Migration;
|
||||
|
||||
use Closure;
|
||||
use OCP\DB\ISchemaWrapper;
|
||||
use OCP\DB\Types;
|
||||
use OCP\Migration\IOutput;
|
||||
use OCP\Migration\SimpleMigrationStep;
|
||||
|
||||
class Version4000Date20240716172702 extends SimpleMigrationStep {
|
||||
/**
|
||||
* @param IOutput $output
|
||||
* @param Closure(): ISchemaWrapper $schemaClosure
|
||||
* @param array $options
|
||||
* @return ISchemaWrapper
|
||||
*/
|
||||
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper {
|
||||
/** @var ISchemaWrapper $schema */
|
||||
$schema = $schemaClosure();
|
||||
$table = $schema->createTable('mail_internal_address');
|
||||
|
||||
$table->addColumn('id', Types::INTEGER, [
|
||||
'autoincrement' => true,
|
||||
'notnull' => true,
|
||||
'length' => 4,
|
||||
]);
|
||||
$table->addColumn('address', Types::STRING, [
|
||||
'notnull' => true,
|
||||
'length' => 255,
|
||||
]);
|
||||
$table->addColumn('type', Types::STRING, [
|
||||
'notnull' => true,
|
||||
'length' => 64,
|
||||
]);
|
||||
$table->addColumn('user_id', Types::STRING, [
|
||||
'notnull' => true,
|
||||
'length' => 64,
|
||||
]);
|
||||
$table->setPrimaryKey(['id']);
|
||||
$table->addUniqueIndex(['address', 'user_id'], 'mail_internal_address_uniq');
|
||||
|
||||
return $schema;
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\Mail\Service;
|
||||
|
||||
use OCA\Mail\Contracts\IInternalAddressService;
|
||||
use OCA\Mail\Db\InternalAddress;
|
||||
use OCA\Mail\Db\InternalAddressMapper;
|
||||
|
||||
class InternalAddressService implements IInternalAddressService {
|
||||
private InternalAddressMapper $mapper;
|
||||
|
||||
public function __construct(InternalAddressMapper $mapper) {
|
||||
$this->mapper = $mapper;
|
||||
}
|
||||
|
||||
public function isInternal(string $uid, string $address): bool {
|
||||
return $this->mapper->exists(
|
||||
$uid,
|
||||
$address
|
||||
);
|
||||
}
|
||||
|
||||
public function add(string $uid, string $address, string $type, ?bool $trust = true): ?InternalAddress {
|
||||
if ($trust && $this->isInternal($uid, $address)) {
|
||||
// Nothing to do
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($trust) {
|
||||
$this->mapper->create(
|
||||
$uid,
|
||||
$address,
|
||||
$type
|
||||
);
|
||||
return $this->getInternalAddress($uid, $address);
|
||||
} else {
|
||||
$this->mapper->remove(
|
||||
$uid,
|
||||
$address,
|
||||
$type
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public function getInternalAddresses(string $uid): array {
|
||||
return $this->mapper->findAll($uid);
|
||||
}
|
||||
|
||||
private function getInternalAddress(string $uid, string $address): ?InternalAddress {
|
||||
return $this->mapper->find($uid, $address);
|
||||
}
|
||||
|
||||
}
|
|
@ -157,6 +157,20 @@
|
|||
<h6>{{ t('mail', 'Trusted senders') }}</h6>
|
||||
<TrustedSenders />
|
||||
|
||||
<h6>{{ t('mail', 'Internal addresses') }}</h6>
|
||||
<p class="settings-hint">
|
||||
{{ t('mail', 'Highlight external email addressesby enabling this feature, manage your internal addresses and domains to ensure recognized contacts stay unmarked.') }}
|
||||
</p>
|
||||
<p class="app-settings">
|
||||
<input id="internal-address-toggle"
|
||||
class="checkbox"
|
||||
type="checkbox"
|
||||
:checked="useInternalAddresses"
|
||||
@change="onToggleInternalAddress">
|
||||
<label for="internal-address-toggle">{{ internalAddressText }}</label>
|
||||
</p>
|
||||
<InternalAddress />
|
||||
|
||||
<h6>{{ t('mail', 'S/MIME') }}</h6>
|
||||
<NcButton class="app-settings-button"
|
||||
type="secondary"
|
||||
|
@ -284,6 +298,7 @@ import HorizontalSplit from 'vue-material-design-icons/ViewSplitHorizontal.vue'
|
|||
import Logger from '../logger.js'
|
||||
import SmimeCertificateModal from './smime/SmimeCertificateModal.vue'
|
||||
import TrustedSenders from './TrustedSenders.vue'
|
||||
import InternalAddress from './InternalAddress.vue'
|
||||
import isMobile from '@nextcloud/vue/dist/Mixins/isMobile.js'
|
||||
import { mapGetters } from 'vuex'
|
||||
|
||||
|
@ -291,6 +306,7 @@ export default {
|
|||
name: 'AppSettingsMenu',
|
||||
components: {
|
||||
TrustedSenders,
|
||||
InternalAddress,
|
||||
NcButton,
|
||||
IconEmail,
|
||||
IconAdd,
|
||||
|
@ -326,6 +342,7 @@ export default {
|
|||
autoTaggingText: t('mail', 'Mark as important'),
|
||||
// eslint-disable-next-line
|
||||
followUpReminderText: t('mail', 'Remind about messages that require a reply but received none'),
|
||||
internalAddressText: t('mail', 'Use internal addresses'),
|
||||
toggleAutoTagging: false,
|
||||
displaySmimeCertificateModal: false,
|
||||
sortOrder: 'newest',
|
||||
|
@ -352,6 +369,9 @@ export default {
|
|||
useAutoTagging() {
|
||||
return this.$store.getters.getPreference('tag-classified-messages', 'true') === 'true'
|
||||
},
|
||||
useInternalAddresses() {
|
||||
return this.$store.getters.getPreference('internal-addresses', 'false') === 'true'
|
||||
},
|
||||
useFollowUpReminders() {
|
||||
return this.$store.getters.getPreference('follow-up-reminders', 'true') === 'true'
|
||||
},
|
||||
|
@ -496,6 +516,17 @@ export default {
|
|||
showError(t('mail', 'Could not update preference'))
|
||||
}
|
||||
},
|
||||
async onToggleInternalAddress(e) {
|
||||
try {
|
||||
await this.$store.dispatch('savePreference', {
|
||||
key: 'internal-addresses',
|
||||
value: e.target.checked ? 'true' : 'false',
|
||||
})
|
||||
} catch (error) {
|
||||
Logger.error('Could not save preferences', { error })
|
||||
showError(t('mail', 'Could not update preference'))
|
||||
}
|
||||
},
|
||||
registerProtocolHandler() {
|
||||
if (window.navigator.registerProtocolHandler) {
|
||||
const url
|
||||
|
@ -593,4 +624,8 @@ p.app-settings {
|
|||
.app-settings-section {
|
||||
list-style: none;
|
||||
}
|
||||
// align it with the checkbox
|
||||
.internal_address{
|
||||
margin-left: 3px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -0,0 +1,171 @@
|
|||
<!--
|
||||
- SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
|
||||
- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div v-for="domain in sortedDomains"
|
||||
:key="domain.address"
|
||||
class="address">
|
||||
{{ domain.address }}
|
||||
<p class="address__type">
|
||||
({{ t('mail', 'domain') }})
|
||||
</p>
|
||||
<ButtonVue type="tertiary"
|
||||
class="button"
|
||||
:aria-label="t('mail', 'Remove')"
|
||||
@click="removeInternalAddress(domain)">
|
||||
{{ t('mail','Remove') }}
|
||||
</ButtonVue>
|
||||
</div>
|
||||
<div v-for="email in sortedEmails"
|
||||
:key="email.address"
|
||||
class="address">
|
||||
{{ email.address }}
|
||||
<p class="address__type">
|
||||
({{ t('mail', 'email') }})
|
||||
</p>
|
||||
<ButtonVue type="tertiary"
|
||||
class="button"
|
||||
:aria-label="t('mail','Remove')"
|
||||
@click="removeInternalAddress(email)">
|
||||
{{ t('mail','Remove') }}
|
||||
</ButtonVue>
|
||||
</div>
|
||||
<ButtonVue type="primary"
|
||||
@click="openDialog = true">
|
||||
<template #icon>
|
||||
<IconAdd :size="20" />
|
||||
</template>
|
||||
{{ t('mail', 'Add internal address') }}
|
||||
</ButtonVue>
|
||||
<NcDialog :open.sync="openDialog"
|
||||
:buttons="buttons"
|
||||
:name="t('mail', 'Add internal address')"
|
||||
@close="openDialog = false">
|
||||
<NcTextField class="input" :label="t('mail', 'Add internal email or domain')" :value.sync="newAddress" />
|
||||
</NcDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import { NcButton as ButtonVue, NcDialog, NcTextField } from '@nextcloud/vue'
|
||||
import prop from 'lodash/fp/prop.js'
|
||||
import sortBy from 'lodash/fp/sortBy.js'
|
||||
import IconAdd from 'vue-material-design-icons/Plus.vue'
|
||||
import IconCancel from '@mdi/svg/svg/cancel.svg'
|
||||
import IconCheck from '@mdi/svg/svg/check.svg'
|
||||
import logger from '../logger.js'
|
||||
import { showError } from '@nextcloud/dialogs'
|
||||
|
||||
const sortByAddress = sortBy(prop('address'))
|
||||
|
||||
export default {
|
||||
name: 'InternalAddress',
|
||||
components: {
|
||||
ButtonVue,
|
||||
NcDialog,
|
||||
NcTextField,
|
||||
IconAdd,
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
openDialog: false,
|
||||
newAddress: '',
|
||||
buttons: [
|
||||
{
|
||||
label: 'Cancel',
|
||||
icon: IconCancel,
|
||||
callback: () => { this.openDialog = false },
|
||||
},
|
||||
{
|
||||
label: 'Ok',
|
||||
type: 'primary',
|
||||
icon: IconCheck,
|
||||
callback: () => { this.addInternalAddress() },
|
||||
},
|
||||
],
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
list() {
|
||||
return this.$store.getters.getInternalAddresses
|
||||
},
|
||||
sortedDomains() {
|
||||
return sortByAddress(this.list.filter(a => a.type === 'domain'))
|
||||
},
|
||||
sortedEmails() {
|
||||
return sortByAddress(this.list.filter(a => a.type === 'individual'))
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async removeInternalAddress(sender) {
|
||||
// Remove the item immediately
|
||||
try {
|
||||
await this.$store.dispatch('removeInternalAddress', { id: sender.id, address: sender.address, type: sender.type })
|
||||
} catch (error) {
|
||||
logger.error(`Could not remove internal address ${sender.email}`, {
|
||||
error,
|
||||
})
|
||||
showError(t('mail', 'Could not remove internal address {sender}', {
|
||||
sender: sender.address,
|
||||
}))
|
||||
}
|
||||
},
|
||||
async addInternalAddress() {
|
||||
const type = this.checkType()
|
||||
try {
|
||||
await this.$store.dispatch('addInternalAddress', {
|
||||
address: this.newAddress,
|
||||
type,
|
||||
}).then(async () => {
|
||||
this.newAddress = ''
|
||||
this.openDialog = false
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error(`Could not add internal address ${this.newAddress}`, {
|
||||
error,
|
||||
})
|
||||
showError(t('mail', 'Could not add internal address {address}', {
|
||||
address: this.newAddress,
|
||||
}))
|
||||
}
|
||||
},
|
||||
checkType() {
|
||||
const parts = this.newAddress.split('@')
|
||||
if (parts.length !== 2) {
|
||||
return 'domain'
|
||||
}
|
||||
// remove '@'' from domain if added by mistake
|
||||
if (parts[0].length === 0) {
|
||||
this.newAddress = parts[1]
|
||||
return 'domain'
|
||||
}
|
||||
return 'individual'
|
||||
},
|
||||
senderType(type) {
|
||||
switch (type) {
|
||||
case 'individual':
|
||||
return t('mail', 'individual')
|
||||
case 'domain':
|
||||
return t('mail', 'domain')
|
||||
}
|
||||
return type
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.address {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
&__type{
|
||||
color: var(--color-text-maxcontrast);
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
|
@ -3,7 +3,7 @@
|
|||
- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
-->
|
||||
<template>
|
||||
<div class="multiselect__tag multiselect__tag--recipient" :title="option.email">
|
||||
<div :class="isInternal?'multiselect__tag multiselect__tag--recipient' :'multiselect__tag multiselect__tag--recipient external'" :title="option.email">
|
||||
<ListItemIcon :no-margin="true"
|
||||
:name="option.label"
|
||||
:url="option.photo"
|
||||
|
@ -29,6 +29,16 @@ export default {
|
|||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isInternal: true,
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
if (this.$store.getters.getPreference('internal-addresses', 'false') === 'true') {
|
||||
this.isInternal = this.$store.getters.isInternalAddress(this.option.email)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
removeRecipient(option, field) {
|
||||
this.$emit('remove-recipient', option, field)
|
||||
|
@ -38,6 +48,13 @@ export default {
|
|||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.external {
|
||||
background-color: var(--color-error) !important;
|
||||
:deep(.option__lineone){
|
||||
color: var(--color-primary-text) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.multiselect
|
||||
.multiselect__tags
|
||||
.multiselect__tags-wrap
|
||||
|
|
|
@ -52,7 +52,7 @@
|
|||
<div class="envelope__header__left__sender-subject-tags">
|
||||
<div class="sender">
|
||||
{{ envelope.from && envelope.from[0] ? envelope.from[0].label : '' }}
|
||||
<p class="sender__email">
|
||||
<p :class="isInternal?'sender__email':'sender__email sender__external'">
|
||||
{{ envelope.from && envelope.from[0] ? envelope.from[0].email : '' }}
|
||||
</p>
|
||||
</div>
|
||||
|
@ -396,6 +396,7 @@ export default {
|
|||
showTaskModal: false,
|
||||
showTagModal: false,
|
||||
rawMessage: '', // Will hold the raw source of the message when requested
|
||||
isInternal: true,
|
||||
enabledSmartReply: loadState('mail', 'llm_freeprompt_available', false),
|
||||
}
|
||||
},
|
||||
|
@ -594,6 +595,9 @@ export default {
|
|||
// assume that this is the relevant envelope to be scrolled to.
|
||||
this.$nextTick(() => this.scrollToCurrentEnvelope())
|
||||
}
|
||||
if (this.$store.getters.getPreference('internal-addresses', 'false') === 'true') {
|
||||
this.isInternal = this.$store.getters.isInternalAddress(this.envelope.from[0].email)
|
||||
}
|
||||
this.$checkInterval = setInterval(() => {
|
||||
const { envelope } = this.$refs
|
||||
const isWidthAvailable = (envelope && envelope.clientWidth > 0)
|
||||
|
@ -887,6 +891,10 @@ export default {
|
|||
&__email{
|
||||
color: var(--color-text-maxcontrast);
|
||||
}
|
||||
|
||||
&__external{
|
||||
color: var(--color-error);
|
||||
}
|
||||
}
|
||||
|
||||
.right {
|
||||
|
|
|
@ -106,9 +106,14 @@ store.commit('savePreference', {
|
|||
key: 'follow-up-reminders',
|
||||
value: getPreferenceFromPage('follow-up-reminders'),
|
||||
})
|
||||
store.commit('savePreference', {
|
||||
key: 'internal-addresses',
|
||||
value: loadState('mail', 'internal-addresses', false),
|
||||
})
|
||||
|
||||
const accountSettings = loadState('mail', 'account-settings')
|
||||
const accounts = loadState('mail', 'accounts', [])
|
||||
const internalAddressesList = loadState('mail', 'internal-addresses-list', [])
|
||||
const tags = loadState('mail', 'tags', [])
|
||||
const outboxMessages = loadState('mail', 'outbox-messages')
|
||||
const disableScheduledSend = loadState('mail', 'disable-scheduled-send')
|
||||
|
@ -133,6 +138,7 @@ accounts.map(fixAccountId).forEach((account) => {
|
|||
})
|
||||
|
||||
tags.forEach(tag => store.commit('addTag', { tag }))
|
||||
internalAddressesList.forEach(internalAddress => store.commit('addInternalAddress', internalAddress))
|
||||
|
||||
store.commit('setScheduledSendingDisabled', disableScheduledSend)
|
||||
store.commit('setSnoozeDisabled', disableSnooze)
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
/**
|
||||
* SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
import { generateUrl } from '@nextcloud/router'
|
||||
import axios from '@nextcloud/axios'
|
||||
|
||||
export async function addInternalAddress(address, type) {
|
||||
const url = generateUrl('/apps/mail/api/internalAddress/{address}?type={type}', {
|
||||
address,
|
||||
type,
|
||||
})
|
||||
const response = await axios.put(url)
|
||||
return response.data.data
|
||||
}
|
||||
|
||||
export async function removeInternalAddress(address, type) {
|
||||
const url = generateUrl('/apps/mail/api/internalAddress/{address}?type={type}', {
|
||||
address,
|
||||
type,
|
||||
})
|
||||
await axios.delete(url)
|
||||
}
|
||||
|
||||
export async function fetchInternalAdresses() {
|
||||
const url = generateUrl('/apps/mail/api/internalAddress')
|
||||
const response = await axios.get(url)
|
||||
return response.data.data
|
||||
}
|
|
@ -103,6 +103,7 @@ import {
|
|||
import * as SmimeCertificateService from '../service/SmimeCertificateService.js'
|
||||
import useOutboxStore from './outboxStore.js'
|
||||
import * as FollowUpService from '../service/FollowUpService.js'
|
||||
import { addInternalAddress, removeInternalAddress } from '../service/InternalAddressService.js'
|
||||
|
||||
const sliceToPage = slice(0, PAGE_SIZE)
|
||||
|
||||
|
@ -1112,6 +1113,25 @@ export default {
|
|||
return result
|
||||
})
|
||||
},
|
||||
async addInternalAddress({ commit }, { address, type }) {
|
||||
return handleHttpAuthErrors(commit, async () => {
|
||||
const internalAddress = await addInternalAddress(address, type)
|
||||
commit('addInternalAddress', internalAddress)
|
||||
console.debug('internal address added')
|
||||
})
|
||||
},
|
||||
async removeInternalAddress({ commit }, { id, address, type }) {
|
||||
return handleHttpAuthErrors(commit, async () => {
|
||||
try {
|
||||
await removeInternalAddress(address, type)
|
||||
commit('removeInternalAddress', { addressId: id })
|
||||
console.debug('internal address removed')
|
||||
} catch (error) {
|
||||
console.error('could not delete internal address', error)
|
||||
throw error
|
||||
}
|
||||
})
|
||||
},
|
||||
async deleteMessage({ getters, commit }, { id }) {
|
||||
return handleHttpAuthErrors(commit, async () => {
|
||||
commit('removeEnvelope', { id })
|
||||
|
|
|
@ -94,6 +94,10 @@ export const getters = {
|
|||
getTag: (state) => (id) => {
|
||||
return state.tags[id]
|
||||
},
|
||||
isInternalAddress: (state) => (address) => {
|
||||
const domain = address.split('@')[1]
|
||||
return state.internalAddress.some((internalAddress) => internalAddress.address === address || internalAddress.address === domain)
|
||||
},
|
||||
getTags: (state) => {
|
||||
return state.tagList.map(tagId => state.tags[tagId])
|
||||
},
|
||||
|
@ -151,4 +155,5 @@ export const getters = {
|
|||
isOneLineLayout: (state) => state.list,
|
||||
hasFetchedInitialEnvelopes: (state) => state.hasFetchedInitialEnvelopes,
|
||||
isFollowUpFeatureAvailable: (state) => state.followUpFeatureAvailable,
|
||||
getInternalAddresses: (state) => state.internalAddress?.filter(internalAddress => internalAddress !== undefined),
|
||||
}
|
||||
|
|
|
@ -106,6 +106,7 @@ export default new Store({
|
|||
smimeCertificates: [],
|
||||
hasFetchedInitialEnvelopes: false,
|
||||
followUpFeatureAvailable: false,
|
||||
internalAddress: [],
|
||||
},
|
||||
getters,
|
||||
mutations,
|
||||
|
|
|
@ -323,6 +323,12 @@ export default {
|
|||
Vue.set(state.tags, tag.id, tag)
|
||||
state.tagList.push(tag.id)
|
||||
},
|
||||
addInternalAddress(state, address) {
|
||||
Vue.set(state.internalAddress, address.id, address)
|
||||
},
|
||||
removeInternalAddress(state, { addressId }) {
|
||||
state.internalAddress = state.internalAddress.filter((address) => address.id !== addressId)
|
||||
},
|
||||
deleteTag(state, { tagId }) {
|
||||
state.tagList = state.tagList.filter((id) => id !== tagId)
|
||||
Vue.delete(state.tags, tagId)
|
||||
|
|
|
@ -0,0 +1,212 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\Mail\Tests\Integration\Db;
|
||||
|
||||
use ChristophWurst\Nextcloud\Testing\DatabaseTransaction;
|
||||
use ChristophWurst\Nextcloud\Testing\TestCase;
|
||||
use ChristophWurst\Nextcloud\Testing\TestUser;
|
||||
use OCA\Mail\Db\InternalAddressMapper;
|
||||
use OCP\IDBConnection;
|
||||
use OCP\IUser;
|
||||
use OCP\Server;
|
||||
|
||||
class InternalAddressMapperTest extends TestCase {
|
||||
use DatabaseTransaction, TestUser;
|
||||
|
||||
/** @var IDBConnection */
|
||||
private $db;
|
||||
|
||||
/** @var IUser */
|
||||
private $user;
|
||||
|
||||
/** @var InternalAddressMapper */
|
||||
private $mapper;
|
||||
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
|
||||
/** @var IDBConnection $db */
|
||||
$this->db = Server::get(IDBConnection::class);
|
||||
$this->user = $this->createTestUser();
|
||||
|
||||
$this->mapper = new InternalAddressMapper(
|
||||
$this->db
|
||||
);
|
||||
}
|
||||
|
||||
public function testDoesntExist(): void {
|
||||
$exists = $this->mapper->exists($this->user->getUID(), "hamza@next.cloud");
|
||||
|
||||
$this->assertFalse($exists);
|
||||
}
|
||||
|
||||
public function testIndividualExists(): void {
|
||||
$uid = $this->user->getUID();
|
||||
$qb = $this->db->getQueryBuilder();
|
||||
$qb->insert('mail_internal_address')
|
||||
->values([
|
||||
'user_id' => $qb->createNamedParameter($uid),
|
||||
'address' => $qb->createNamedParameter('hamza@next.cloud'),
|
||||
'type' => $qb->createNamedParameter('individual')
|
||||
|
||||
])
|
||||
->executeStatement();
|
||||
|
||||
$exists = $this->mapper->exists($uid, "hamza@next.cloud");
|
||||
|
||||
$this->assertTrue($exists);
|
||||
}
|
||||
|
||||
public function testDomainExists(): void {
|
||||
$uid = $this->user->getUID();
|
||||
$qb = $this->db->getQueryBuilder();
|
||||
$qb->insert('mail_internal_address')
|
||||
->values([
|
||||
'user_id' => $qb->createNamedParameter($uid),
|
||||
'address' => $qb->createNamedParameter('next.cloud'),
|
||||
'type' => $qb->createNamedParameter('domain'),
|
||||
|
||||
])
|
||||
->executeStatement();
|
||||
|
||||
$exists = $this->mapper->exists($uid, "hamza@next.cloud");
|
||||
|
||||
$this->assertTrue($exists);
|
||||
}
|
||||
|
||||
public function testCreateIndividual(): void {
|
||||
$uid = $this->user->getUID();
|
||||
$this->mapper->create(
|
||||
$uid,
|
||||
"hamza@next.cloud",
|
||||
'individual'
|
||||
);
|
||||
|
||||
$qb = $this->db->getQueryBuilder();
|
||||
$qb->select('*')
|
||||
->from('mail_internal_address')
|
||||
->where(
|
||||
$qb->expr()->eq('user_id', $qb->createNamedParameter($uid)),
|
||||
$qb->expr()->eq('address', $qb->createNamedParameter("hamza@next.cloud"))
|
||||
);
|
||||
$result = $qb->executeQuery();
|
||||
$rows = $result->fetchAll();
|
||||
$result->closeCursor();
|
||||
$this->assertCount(1, $rows);
|
||||
}
|
||||
|
||||
public function testRemoveIndividual(): void {
|
||||
$uid = $this->user->getUID();
|
||||
$qb = $this->db->getQueryBuilder();
|
||||
$qb->insert('mail_internal_address')
|
||||
->values([
|
||||
'user_id' => $qb->createNamedParameter($uid),
|
||||
'address' => $qb->createNamedParameter('hamza@next.cloud'),
|
||||
'type' => $qb->createNamedParameter('individual'),
|
||||
])
|
||||
->executeStatement();
|
||||
|
||||
$this->mapper->remove(
|
||||
$uid,
|
||||
"hamza@next.cloud",
|
||||
'individual'
|
||||
);
|
||||
|
||||
$qb = $this->db->getQueryBuilder();
|
||||
$qb->select('*')
|
||||
->from('mail_internal_address')
|
||||
->where(
|
||||
$qb->expr()->eq('user_id', $qb->createNamedParameter($uid)),
|
||||
$qb->expr()->eq('address', $qb->createNamedParameter("hamza@next.cloud"))
|
||||
);
|
||||
$result = $qb->executeQuery();
|
||||
$rows = $result->fetchAll();
|
||||
$result->closeCursor();
|
||||
$this->assertEmpty($rows);
|
||||
}
|
||||
|
||||
public function testCreateDomain(): void {
|
||||
$uid = $this->user->getUID();
|
||||
$this->mapper->create(
|
||||
$uid,
|
||||
"next.cloud",
|
||||
'domain'
|
||||
);
|
||||
|
||||
$qb = $this->db->getQueryBuilder();
|
||||
$qb->select('*')
|
||||
->from('mail_internal_address')
|
||||
->where(
|
||||
$qb->expr()->eq('user_id', $qb->createNamedParameter($uid)),
|
||||
$qb->expr()->eq('address', $qb->createNamedParameter("next.cloud")),
|
||||
$qb->expr()->eq('type', $qb->createNamedParameter('domain'))
|
||||
);
|
||||
$result = $qb->executeQuery();
|
||||
$rows = $result->fetchAll();
|
||||
$result->closeCursor();
|
||||
$this->assertCount(1, $rows);
|
||||
}
|
||||
|
||||
public function testRemoveDomain(): void {
|
||||
$uid = $this->user->getUID();
|
||||
$qb = $this->db->getQueryBuilder();
|
||||
$qb->insert('mail_internal_address')
|
||||
->values([
|
||||
'user_id' => $qb->createNamedParameter($uid),
|
||||
'address' => $qb->createNamedParameter('next.cloud'),
|
||||
'type' => $qb->createNamedParameter('domain'),
|
||||
])
|
||||
->executeStatement();
|
||||
|
||||
$this->mapper->remove(
|
||||
$uid,
|
||||
"next.cloud",
|
||||
'domain'
|
||||
);
|
||||
|
||||
$qb = $this->db->getQueryBuilder();
|
||||
$qb->select('*')
|
||||
->from('mail_internal_address')
|
||||
->where(
|
||||
$qb->expr()->eq('user_id', $qb->createNamedParameter($uid)),
|
||||
$qb->expr()->eq('address', $qb->createNamedParameter("next.cloud")),
|
||||
$qb->expr()->eq('type', $qb->createNamedParameter("domain"))
|
||||
);
|
||||
$result = $qb->executeQuery();
|
||||
$rows = $result->fetchAll();
|
||||
$result->closeCursor();
|
||||
$this->assertEmpty($rows);
|
||||
}
|
||||
|
||||
public function testFindAll(): void {
|
||||
$uid = $this->user->getUID();
|
||||
|
||||
$this->db->beginTransaction();
|
||||
|
||||
$data = [
|
||||
['user_id' => $uid, 'address' => 'hamza@next.cloud', 'type' => 'individual'],
|
||||
['user_id' => $uid, 'address' => 'christoph@next.cloud', 'type' => 'individual'],
|
||||
];
|
||||
$sql = 'INSERT INTO oc_mail_internal_address (user_id, address, type) VALUES (:user_id, :address, :type)';
|
||||
$stmt = $this->db->prepare($sql);
|
||||
|
||||
foreach ($data as $row) {
|
||||
$stmt->execute($row);
|
||||
}
|
||||
|
||||
$this->db->commit();
|
||||
|
||||
$results = $this->mapper->findAll($uid);
|
||||
|
||||
$this->assertCount(2, $results);
|
||||
$this->assertEquals($results[0]->getAddress(), 'hamza@next.cloud');
|
||||
$this->assertEquals($results[1]->getAddress(), 'christoph@next.cloud');
|
||||
}
|
||||
}
|
|
@ -20,6 +20,7 @@ use OCA\Mail\Service\AccountService;
|
|||
use OCA\Mail\Service\AiIntegrations\AiIntegrationsService;
|
||||
use OCA\Mail\Service\AliasesService;
|
||||
use OCA\Mail\Service\Classification\ClassificationSettingsService;
|
||||
use OCA\Mail\Service\InternalAddressService;
|
||||
use OCA\Mail\Service\MailManager;
|
||||
use OCA\Mail\Service\OutboxService;
|
||||
use OCA\Mail\Service\SmimeService;
|
||||
|
@ -106,6 +107,9 @@ class PageControllerTest extends TestCase {
|
|||
/** @var ClassificationSettingsService|MockObject */
|
||||
private $classificationSettingsService;
|
||||
|
||||
/** @var InternalAddressService|MockObject */
|
||||
private $internalAddressService;
|
||||
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
|
||||
|
@ -130,6 +134,7 @@ class PageControllerTest extends TestCase {
|
|||
$this->userManager = $this->createMock(IUserManager::class);
|
||||
$this->container = $this->createMock(ContainerInterface::class);
|
||||
$this->classificationSettingsService = $this->createMock(ClassificationSettingsService::class);
|
||||
$this->internalAddressService = $this->createMock(InternalAddressService::class);
|
||||
|
||||
$this->controller = new PageController(
|
||||
$this->appName,
|
||||
|
@ -153,6 +158,8 @@ class PageControllerTest extends TestCase {
|
|||
$this->userManager,
|
||||
$this->container,
|
||||
$this->classificationSettingsService,
|
||||
$this->internalAddressService,
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -160,7 +167,7 @@ class PageControllerTest extends TestCase {
|
|||
$account1 = $this->createMock(Account::class);
|
||||
$account2 = $this->createMock(Account::class);
|
||||
$mailbox = $this->createMock(Mailbox::class);
|
||||
$this->preferences->expects($this->exactly(9))
|
||||
$this->preferences->expects($this->exactly(10))
|
||||
->method('getPreference')
|
||||
->willReturnMap([
|
||||
[$this->userId, 'account-settings', '[]', json_encode([])],
|
||||
|
@ -172,6 +179,7 @@ class PageControllerTest extends TestCase {
|
|||
[$this->userId, 'start-mailbox-id', null, '123'],
|
||||
[$this->userId, 'layout-mode', 'vertical-split', 'vertical-split'],
|
||||
[$this->userId, 'follow-up-reminders', 'true', 'true'],
|
||||
[$this->userId, 'internal-addresses', 'false', 'false'],
|
||||
]);
|
||||
$this->classificationSettingsService->expects(self::once())
|
||||
->method('isClassificationEnabled')
|
||||
|
@ -290,7 +298,7 @@ class PageControllerTest extends TestCase {
|
|||
->method('getLoginCredentials')
|
||||
->willReturn($loginCredentials);
|
||||
|
||||
$this->initialState->expects($this->exactly(17))
|
||||
$this->initialState->expects($this->exactly(19))
|
||||
->method('provideInitialState')
|
||||
->withConsecutive(
|
||||
['debug', true],
|
||||
|
@ -298,6 +306,8 @@ class PageControllerTest extends TestCase {
|
|||
['accounts', $accountsJson],
|
||||
['account-settings', []],
|
||||
['tags', []],
|
||||
['internal-addresses-list', []],
|
||||
['internal-addresses', false],
|
||||
['sort-order', 'newest'],
|
||||
['password-is-unavailable', true],
|
||||
['prefill_displayName', 'Jane Doe'],
|
||||
|
|
Загрузка…
Ссылка в новой задаче