Signed-off-by: Hamza Mahjoubi <hamzamahjoubi221@gmail.com>
This commit is contained in:
Hamza Mahjoubi 2024-07-19 11:26:48 +02:00
Родитель 6b86b4d4e2
Коммит aab2db92a6
20 изменённых файлов: 925 добавлений и 5 удалений

Просмотреть файл

@ -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'],