diff --git a/src/components/OutOfOfficeForm.vue b/src/components/OutOfOfficeForm.vue
index b26f103e98..2fbca730cc 100644
--- a/src/components/OutOfOfficeForm.vue
+++ b/src/components/OutOfOfficeForm.vue
@@ -21,8 +21,7 @@
-->
-
-
{{ t('mail', 'Oh Snap!') }}
@@ -118,9 +139,15 @@
import { NcDatetimePicker as DatetimePicker, NcButton as Button } from '@nextcloud/vue'
import TextEditor from './TextEditor.vue'
import CheckIcon from 'vue-material-design-icons/Check'
-import { buildOutOfOfficeSieveScript, parseOutOfOfficeState } from '../util/outOfOffice.js'
-import logger from '../logger.js'
import { html, plain, toHtml, toPlain } from '../util/text.js'
+import { loadState } from '@nextcloud/initial-state'
+import { generateUrl } from '@nextcloud/router'
+import OpenInNewIcon from 'vue-material-design-icons/OpenInNew.vue'
+import * as OutOfOfficeService from '../service/OutOfOfficeService.js'
+
+const OOO_DISABLED = 'disabled'
+const OOO_ENABLED = 'enabled'
+const OOO_FOLLOW_SYSTEM = 'system'
export default {
name: 'OutOfOfficeForm',
@@ -129,6 +156,7 @@ export default {
TextEditor,
Button,
CheckIcon,
+ OpenInNewIcon,
},
props: {
account: {
@@ -137,8 +165,14 @@ export default {
},
},
data() {
+ const nextcloudVersion = parseInt(OC.config.version.split('.')[0])
+ const enableSystemOutOfOffice = loadState('mail', 'enable-system-out-of-office', false)
+
return {
- enabled: false,
+ OOO_DISABLED,
+ OOO_ENABLED,
+ OOO_FOLLOW_SYSTEM,
+ enabled: this.account.outOfOfficeFollowsSystem ? OOO_FOLLOW_SYSTEM : OOO_DISABLED,
enableLastDay: false,
firstDay: new Date(),
lastDay: null,
@@ -146,6 +180,8 @@ export default {
message: '',
loading: false,
errorMessage: '',
+ hasPersonalAbsenceSettings: nextcloudVersion >= 28 && enableSystemOutOfOffice,
+ personalAbsenceSettingsUrl: generateUrl('/settings/user/availability'),
}
},
computed: {
@@ -153,6 +189,14 @@ export default {
* @return {boolean}
*/
valid() {
+ if (this.followingSystem) {
+ return true
+ }
+
+ if (this.enabled === OOO_DISABLED) {
+ return true
+ }
+
return !!(this.firstDay
&& (!this.enableLastDay || (this.enableLastDay && this.lastDay))
&& (!this.enableLastDay || (this.lastDay >= this.firstDay))
@@ -160,30 +204,6 @@ export default {
&& this.message)
},
- /**
- * @return {{script: string, scriptName: string}|undefined}
- */
- sieveScriptData() {
- return this.$store.getters.getActiveSieveScript(this.account.id)
- },
-
- /**
- * @return {object|undefined}
- */
- parsedState() {
- if (!this.sieveScriptData?.script) {
- return undefined
- }
-
- try {
- return parseOutOfOfficeState(this.sieveScriptData.script).data
- } catch (error) {
- logger.warn('Failed to parse OOO state', { error })
- }
-
- return undefined
- },
-
/**
* Main address and all aliases formatted for use with sieve.
*
@@ -198,20 +218,15 @@ export default {
...this.account.aliases,
].map(({ name, alias }) => `${name} <${alias}>`)
},
+
+ /**
+ * @return {boolean}
+ */
+ followingSystem() {
+ return this.hasPersonalAbsenceSettings && this.enabled === OOO_FOLLOW_SYSTEM
+ },
},
watch: {
- parsedState: {
- immediate: true,
- handler(state) {
- state ??= {}
- this.enabled = !!state.enabled ?? false
- this.firstDay = state.start ?? new Date()
- this.lastDay = state.end ?? null
- this.enableLastDay = !!this.lastDay
- this.subject = state.subject ?? ''
- this.message = toHtml(plain(state.message ?? '')).value
- },
- },
enableLastDay(enableLastDay) {
if (enableLastDay) {
this.lastDay = new Date(this.firstDay)
@@ -235,37 +250,61 @@ export default {
this.lastDay.setDate(firstDay.getDate() + diffDays)
},
},
+ async mounted() {
+ await this.fetchState()
+ },
methods: {
+ async fetchState() {
+ const { state } = await OutOfOfficeService.fetch(this.account.id)
+
+ if (this.account.outOfOfficeFollowsSystem) {
+ this.enabled = OOO_FOLLOW_SYSTEM
+ } else {
+ this.enabled = state.enabled ? OOO_ENABLED : OOO_DISABLED
+ }
+ if (state.enabled) {
+ this.firstDay = state.start ? new Date(state.start) : new Date()
+ this.lastDay = state.end ? new Date(state.end) : null
+ }
+ this.enableLastDay = !!this.lastDay
+ this.subject = state.subject
+ this.message = toHtml(plain(state.message)).value
+ },
async submit() {
this.loading = true
this.errorMessage = ''
- const state = parseOutOfOfficeState(this.sieveScriptData.script)
- const originalScript = state.sieveScript
-
- const enrichedScript = buildOutOfOfficeSieveScript(originalScript, {
- enabled: this.enabled,
- start: this.firstDay,
- end: this.lastDay,
- subject: this.subject,
- message: toPlain(html(this.message)).value, // CKEditor always returns html data
- allowedRecipients: this.aliases,
- })
-
try {
- await this.$store.dispatch('updateActiveSieveScript', {
- accountId: this.account.id,
- scriptData: {
- ...this.sieveScriptData,
- script: enrichedScript,
- },
- })
+ if (this.followingSystem) {
+ await OutOfOfficeService.followSystem(this.account.id)
+ this.$store.commit('patchAccount', {
+ account: this.account,
+ data: {
+ outOfOfficeFollowsSystem: true,
+ },
+ })
+ } else {
+ await OutOfOfficeService.update(this.account.id, {
+ enabled: this.enabled === OOO_ENABLED,
+ start: this.firstDay,
+ end: this.lastDay,
+ subject: this.subject,
+ message: toPlain(html(this.message)).value, // CKEditor always returns html data
+ allowedRecipients: this.aliases,
+ })
+ this.$store.commit('patchAccount', {
+ account: this.account,
+ data: {
+ outOfOfficeFollowsSystem: false,
+ },
+ })
+ }
+ await this.$store.dispatch('fetchActiveSieveScript', { accountId: this.account.id })
} catch (error) {
this.errorMessage = error.message
} finally {
this.loading = false
}
-
},
},
}
diff --git a/src/service/OutOfOfficeService.js b/src/service/OutOfOfficeService.js
new file mode 100644
index 0000000000..023b8f4642
--- /dev/null
+++ b/src/service/OutOfOfficeService.js
@@ -0,0 +1,66 @@
+/**
+ * @copyright Copyright (c) 2023 Richard Steinmetz
+ *
+ * @author Richard Steinmetz
+ *
+ * @license AGPL-3.0-or-later
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+import { generateUrl } from '@nextcloud/router'
+import axios from '@nextcloud/axios'
+
+/**
+ * @typedef {{ enabled: bool, start: string, end: string, subject: string, message: string }} OutOfOfficeState
+ */
+
+/**
+ * @typedef {{ state: OutOfOfficeState, script: string, untouchedScript: string }} OutOfOfficeStateResponse
+ */
+
+/**
+ * @param {number} accountId
+ * @return {Promise}
+ */
+export async function fetch(accountId) {
+ const url = generateUrl('/apps/mail/api/out-of-office/{accountId}', { accountId })
+
+ const { data } = await axios.get(url)
+ return data.data
+}
+
+/**
+ * @param {number} accountId
+ * @param {OutOfOfficeState} outOfOfficeState
+ * @return {Promise}
+ */
+export async function update(accountId, outOfOfficeState) {
+ const url = generateUrl('/apps/mail/api/out-of-office/{accountId}', { accountId })
+
+ const { data } = await axios.post(url, outOfOfficeState)
+ return data.data
+}
+
+/**
+ * @param {number} accountId
+ * @return {Promise}
+ */
+export async function followSystem(accountId) {
+ const url = generateUrl('/apps/mail/api/out-of-office/{accountId}/follow-system', { accountId })
+
+ const { data } = await axios.post(url)
+ return data.data
+}
diff --git a/src/tests/unit/util/outOfOffice.spec.js b/src/tests/unit/util/outOfOffice.spec.js
deleted file mode 100644
index 89965168cf..0000000000
--- a/src/tests/unit/util/outOfOffice.spec.js
+++ /dev/null
@@ -1,195 +0,0 @@
-/**
- * @copyright Copyright (c) 2022 Richard Steinmetz
- *
- * @author Richard Steinmetz
- *
- * @license AGPL-3.0-or-later
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see .
- *
- */
-
-import {
- buildOutOfOfficeSieveScript, escapeStringForSieve,
- formatDateForSieve,
- parseOutOfOfficeState,
-} from '../../../util/outOfOffice'
-
-describe('outOfOffice', () => {
- describe('parseOutOfOfficeState', () => {
- it('should parse a sieve script containing an enabled vacation responder', () => {
- const script = readTestData('sieve-vacation-on.txt')
- const cleanedScript = readTestData('sieve-vacation-cleaned.txt')
- const expected = {
- sieveScript: cleanedScript,
- data: {
- version: 1,
- enabled: true,
- start: new Date('2022-09-02'),
- end: new Date('2022-09-08'),
- subject: 'On vacation',
- message: 'I\'m on vacation.',
- },
- }
- const actual = parseOutOfOfficeState(script)
- expect(actual).toEqual(expected)
- })
-
- it('should parse a sieve script containing a disabled vacation responder', () => {
- const script = readTestData('sieve-vacation-off.txt')
- const cleanedScript = readTestData('sieve-vacation-cleaned.txt')
- const expected = {
- sieveScript: cleanedScript,
- data: {
- version: 1,
- enabled: false,
- subject: 'On vacation',
- message: 'I\'m on vacation.',
- },
- }
- const actual = parseOutOfOfficeState(script)
- expect(actual).toEqual(expected)
- })
-
- it('should leave a foreign script untouched', () => {
- const script = readTestData('sieve-vacation-cleaned.txt')
- const expected = {
- sieveScript: script,
- data: undefined,
- }
- const actual = parseOutOfOfficeState(script)
- expect(actual).toEqual(expected)
- })
- })
-
- describe('buildOutOfOfficeSieveScript', () => {
- it('should build a correct sieve script when the vacation responder is enabled', () => {
- const script = readTestData('sieve-vacation-cleaned.txt')
- const expected = readTestData('sieve-vacation-on.txt')
- const actual = buildOutOfOfficeSieveScript(script, {
- enabled: true,
- start: new Date('2022-09-02'),
- end: new Date('2022-09-08'),
- subject: 'On vacation',
- message: 'I\'m on vacation.',
- allowedRecipients: [
- 'Test Test ',
- 'Test Alias ',
- ]
- })
- expect(actual).toEqual(expected)
- })
-
- it('should build a correct sieve script when the vacation responder is enabled and no end date is given', () => {
- const script = readTestData('sieve-vacation-cleaned.txt')
- const expected = readTestData('sieve-vacation-on-no-end-date.txt')
- const actual = buildOutOfOfficeSieveScript(script, {
- enabled: true,
- start: new Date('2022-09-02'),
- subject: 'On vacation',
- message: 'I\'m on vacation.',
- allowedRecipients: [
- 'Test Test ',
- 'Test Alias ',
- ]
- })
- expect(actual).toEqual(expected)
- })
-
- it('should build a correct sieve script when the vacation responder is enabled and the message contains special chars', () => {
- const script = readTestData('sieve-vacation-cleaned.txt')
- const expected = readTestData('sieve-vacation-on-special-chars-message.txt')
- const actual = buildOutOfOfficeSieveScript(script, {
- enabled: true,
- start: new Date('2022-09-02'),
- subject: 'On vacation',
- message: 'I\'m on vacation.\n"Hello, World!"\n\\ escaped backslash',
- allowedRecipients: [
- 'Test Test ',
- 'Test Alias ',
- ]
- })
- expect(actual).toEqual(expected)
- })
-
- it('should build a correct sieve script when the vacation responder is enabled and the subject contains special chars', () => {
- const script = readTestData('sieve-vacation-cleaned.txt')
- const expected = readTestData('sieve-vacation-on-special-chars-subject.txt')
- const actual = buildOutOfOfficeSieveScript(script, {
- enabled: true,
- start: new Date('2022-09-02'),
- subject: 'On vacation, \"Hello, World!\", \\ escaped backslash',
- message: 'I\'m on vacation.',
- allowedRecipients: [
- 'Test Test ',
- 'Test Alias ',
- ]
- })
- expect(actual).toEqual(expected)
- })
-
- it('should build a correct sieve script when the vacation responder is enabled and a subject placeholder is used', () => {
- const script = readTestData('sieve-vacation-cleaned.txt')
- const expected = readTestData('sieve-vacation-on-subject-placeholder.txt')
- const actual = buildOutOfOfficeSieveScript(script, {
- enabled: true,
- start: new Date('2022-09-02'),
- end: new Date('2022-09-08'),
- subject: 'Re: ${subject}',
- message: 'I\'m on vacation.',
- allowedRecipients: [
- 'Test Test ',
- 'Test Alias ',
- ]
- })
- expect(actual).toEqual(expected)
- })
-
- it('should build a correct sieve script when the vacation responder is disabled', () => {
- const script = readTestData('sieve-vacation-cleaned.txt')
- const expected = readTestData('sieve-vacation-off.txt')
- const actual = buildOutOfOfficeSieveScript(script, {
- enabled: false,
- subject: 'On vacation',
- message: 'I\'m on vacation.',
- })
- expect(actual).toEqual(expected)
- })
- })
-
- describe('formatDateForSieve', () => {
- it('should format a JS date instance according to YYYY-MM-DD', () => {
- const date = new Date('2022-09-02')
- const expected = '2022-09-02'
- const actual = formatDateForSieve(date)
- expect(actual).toEqual(expected)
- })
- })
-
- describe('escapeStringForSieve', () => {
- it('should escape double quotes', () => {
- const string = 'A string with "quotes"'
- const expected = 'A string with \\"quotes\\"'
- const actual = escapeStringForSieve(string)
- expect(actual).toEqual(expected)
- })
-
- it('should escape backslashes', () => {
- const string = 'A string with \\ backslashes'
- const expected = 'A string with \\\\ backslashes'
- const actual = escapeStringForSieve(string)
- expect(actual).toEqual(expected)
- })
- })
-})
diff --git a/src/util/outOfOffice.js b/src/util/outOfOffice.js
deleted file mode 100644
index dc93a17eb1..0000000000
--- a/src/util/outOfOffice.js
+++ /dev/null
@@ -1,213 +0,0 @@
-/**
- * @copyright Copyright (c) 2022 Richard Steinmetz
- *
- * @author Richard Steinmetz
- *
- * @license AGPL-3.0-or-later
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see .
- *
- */
-
-const MARKER = '### Nextcloud Mail: Vacation Responder ### DON\'T EDIT ###'
-const DATA_MARKER = '# DATA: '
-const VERSION = 1
-
-// Parser states
-const PARSER_COPY = 0
-const PARSER_SKIP = 1
-
-export class OOOParserError extends Error {}
-
-/**
- * Parse embedded out of office state from the given sieve script.
- * Return the original sieve script without any out of office data/script.
- *
- * @param {string} sieveScript
- * @return {{data: object|undefined, sieveScript: string}}
- */
-export function parseOutOfOfficeState(sieveScript) {
- const lines = sieveScript.split(/\r?\n/)
-
- const out = []
- let data
-
- let state = PARSER_COPY
- let nextState = state
-
- for (const line of lines) {
- switch (state) {
- case PARSER_COPY:
- if (line.startsWith(MARKER)) {
- nextState = PARSER_SKIP
- break
- }
- out.push(line)
- break
- case PARSER_SKIP:
- if (line.startsWith(MARKER)) {
- nextState = PARSER_COPY
- } else if (line.startsWith(DATA_MARKER)) {
- const json = line.slice(DATA_MARKER.length)
- data = JSON.parse(json)
- if (data.start) data.start = new Date(data.start)
- if (data.end) data.end = new Date(data.end)
- }
- break
- default:
- throw new OOOParserError('Reached an invalid state')
- }
- state = nextState
- }
-
- return {
- sieveScript: out.join('\n'),
- data,
- }
-}
-
-/**
- * Embed vacation responder action and out of office state into the given sieve script.
- *
- * @param {string} sieveScript
- * @param {object} data
- * @param {bool} data.enabled
- * @param {Date=} data.start First day (inclusive)
- * @param {Date=} data.end Last day (inclusive)
- * @param {string} data.subject
- * @param {string} data.message
- * @param {string[]=} data.allowedRecipients Only respond if recipient of incoming mail is in this list. Format: `Test Test `
- * @return {string} Sieve script
- */
-export function buildOutOfOfficeSieveScript(sieveScript, {
- enabled,
- start,
- end,
- subject,
- message,
- allowedRecipients,
-}) {
- // State to be embedded in the sieve script
- const data = {
- version: VERSION,
- enabled,
- start: start ? formatDateForSieve(start) : undefined,
- end: end ? formatDateForSieve(end) : undefined,
- subject,
- message,
- }
-
- // Save only state if vacation responder is disabled
- if (!enabled) {
- delete data.start
- delete data.end
- return [
- sieveScript,
- MARKER,
- DATA_MARKER + JSON.stringify(data),
- MARKER,
- ].join('\n')
- }
-
- // Build if condition for start and end dates
- let condition
- if (end) {
- condition = `allof(currentdate :value "ge" "date" "${formatDateForSieve(start)}", currentdate :value "le" "date" "${formatDateForSieve(end)}")`
- } else {
- condition = `currentdate :value "ge" "date" "${formatDateForSieve(start)}"`
- }
-
- // Build vacation command
- const vacation = [
- 'vacation',
- ':days 4',
- `:subject "${escapeStringForSieve(subject)}"`,
- ]
-
- if (allowedRecipients?.length) {
- const formattedRecipients = allowedRecipients.map(recipient => `"${recipient}"`).join(', ')
- vacation.push(`:addresses [${formattedRecipients}]`)
- }
-
- vacation.push(`"${escapeStringForSieve(message)}"`)
-
- // Build sieve script
- /* eslint-disable no-template-curly-in-string */
- const subjectSection = [
- 'set "subject" "";',
- 'if header :matches "subject" "*" {',
- '\tset "subject" "${1}";',
- '}',
- ]
- const hasSubjectPlaceholder
- = subject.indexOf('${subject}') !== -1 || message.indexOf('${subject}') !== -1
- /* eslint-enable no-template-curly-in-string */
-
- const requireSection = [
- MARKER,
- 'require "date";',
- 'require "relational";',
- 'require "vacation";',
- ]
- if (hasSubjectPlaceholder) {
- requireSection.push('require "variables";')
- }
- requireSection.push(MARKER)
-
- const vacationSection = [
- MARKER,
- DATA_MARKER + JSON.stringify(data),
- ]
- if (hasSubjectPlaceholder) {
- vacationSection.push(...subjectSection)
- }
- vacationSection.push(
- `if ${condition} {`,
- `\t${vacation.join(' ')};`,
- '}',
- MARKER,
- )
-
- return [
- ...requireSection,
- sieveScript,
- ...vacationSection,
- ].join('\n')
-}
-
-/**
- * Format a JavaScript date object to use with the sieve :vacation action.
- *
- * @param {Date} date JavaScript date object
- * @return {string} YYYY-MM-DD
- */
-export function formatDateForSieve(date) {
- const year = date.getFullYear().toString().padStart(4, '0')
- const month = (date.getMonth() + 1).toString().padStart(2, '0')
- const day = date.getDate().toString().padStart(2, '0')
- return `${year}-${month}-${day}`
-}
-
-/**
- * Escape a string for use in a sieve script.
- * The string has to be surrounded by double quotes (`"`) manually.
- *
- * @param {string} string String to escape
- * @return {string} Escaped string
- */
-export function escapeStringForSieve(string) {
- return string
- .replaceAll(/\\/g, '\\\\')
- .replaceAll(/"/g, '\\"')
-}
diff --git a/tests/Integration/Db/MailAccountTest.php b/tests/Integration/Db/MailAccountTest.php
index 7d7fdaaa45..133197b92d 100644
--- a/tests/Integration/Db/MailAccountTest.php
+++ b/tests/Integration/Db/MailAccountTest.php
@@ -48,6 +48,7 @@ public function testToAPI() {
$a->setOrder(13);
$a->setQuotaPercentage(10);
$a->setTrashRetentionDays(60);
+ $a->setOutOfOfficeFollowsSystem(true);
$this->assertEquals([
'id' => 12345,
@@ -80,7 +81,8 @@ public function testToAPI() {
'trashRetentionDays' => 60,
'junkMailboxId' => null,
'snoozeMailboxId' => null,
- 'searchBody' => false
+ 'searchBody' => false,
+ 'outOfOfficeFollowsSystem' => true,
], $a->toJson());
}
@@ -116,7 +118,8 @@ public function testMailAccountConstruct() {
'trashRetentionDays' => 60,
'junkMailboxId' => null,
'snoozeMailboxId' => null,
- 'searchBody' => false
+ 'searchBody' => false,
+ 'outOfOfficeFollowsSystem' => false,
];
$a = new MailAccount($expected);
// TODO: fix inconsistency
diff --git a/tests/Unit/Controller/OutOfOfficeControllerTest.php b/tests/Unit/Controller/OutOfOfficeControllerTest.php
new file mode 100644
index 0000000000..0bbd5ba57e
--- /dev/null
+++ b/tests/Unit/Controller/OutOfOfficeControllerTest.php
@@ -0,0 +1,205 @@
+
+ *
+ * @author Richard Steinmetz
+ *
+ * @license AGPL-3.0-or-later
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+namespace Unit\Controller;
+
+use ChristophWurst\Nextcloud\Testing\ServiceMockObject;
+use ChristophWurst\Nextcloud\Testing\TestCase;
+use DateTimeImmutable;
+use OCA\Mail\Account;
+use OCA\Mail\Controller\OutOfOfficeController;
+use OCA\Mail\Db\MailAccount;
+use OCA\Mail\Service\OutOfOffice\OutOfOfficeState;
+use OCP\IUser;
+use OCP\User\IAvailabilityCoordinator;
+use OCP\User\IOutOfOfficeData;
+use PHPUnit\Framework\MockObject\MockObject;
+use Psr\Container\ContainerInterface;
+
+class OutOfOfficeControllerTest extends TestCase {
+ private ServiceMockObject $serviceMock;
+ private OutOfOfficeController $outOfOfficeController;
+
+ /** @var ContainerInterface|MockObject */
+ private $container;
+
+ /** @var IAvailabilityCoordinator|MockObject */
+ private $availabilityCoordinator;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ if (!interface_exists(IAvailabilityCoordinator::class)
+ || !interface_exists(IOutOfOfficeData::class)) {
+ $this->markTestSkipped('Out-of-office feature is not available');
+ }
+
+ $this->container = $this->createMock(ContainerInterface::class);
+ $this->availabilityCoordinator = $this->createMock(IAvailabilityCoordinator::class);
+
+ $this->container->expects(self::once())
+ ->method('get')
+ ->with(IAvailabilityCoordinator::class)
+ ->willReturn($this->availabilityCoordinator);
+
+ $this->serviceMock = $this->createServiceMock(OutOfOfficeController::class, [
+ 'userId' => 'user',
+ 'container' => $this->container,
+ ]);
+ $this->outOfOfficeController = $this->serviceMock->getService();
+ }
+
+ private function createOutOfOfficeData(
+ string $id,
+ IUser $user,
+ int $startDate,
+ int $endDate,
+ string $subject,
+ string $message,
+ ): ?IOutOfOfficeData {
+ if (!interface_exists(IOutOfOfficeData::class)) {
+ return null;
+ }
+
+ $data = $this->createMock(IOutOfOfficeData::class);
+ $data->method('getId')->willReturn($id);
+ $data->method('getUser')->willReturn($user);
+ $data->method('getStartDate')->willReturn($startDate);
+ $data->method('getEndDate')->willReturn($endDate);
+ $data->method('getShortMessage')->willReturn($subject);
+ $data->method('getMessage')->willReturn($message);
+ return $data;
+ }
+
+
+ public function disabledOutOfOfficeDataProvider(): array {
+ $user = $this->createMock(IUser::class);
+ return [
+ [null],
+ [$this->createOutOfOfficeData('2', $user, 0, 1, '', '')],
+ [$this->createOutOfOfficeData('2', $user, PHP_INT_MAX - 1, PHP_INT_MAX, '', '')],
+ [$this->createOutOfOfficeData('2', $user, 0, 1500, 'Subject', 'Message')],
+ ];
+ }
+
+ /**
+ * @dataProvider disabledOutOfOfficeDataProvider
+ */
+ public function testFollowSystemWithDisabledOutOfOffice(?IOutOfOfficeData $data): void {
+ $user = $this->createMock(IUser::class);
+
+ $mailAccount = new MailAccount();
+ $mailAccount->setId(1);
+ $mailAccount->setOutOfOfficeFollowsSystem(false);
+ $account = new Account($mailAccount);
+
+ $userSession = $this->serviceMock->getParameter('userSession');
+ $userSession->expects(self::once())
+ ->method('getUser')
+ ->willReturn($user);
+ $accountService = $this->serviceMock->getParameter('accountService');
+ $accountService->expects(self::once())
+ ->method('findById')
+ ->with(1)
+ ->willReturn($account);
+ $timeFactory = $this->serviceMock->getParameter('timeFactory');
+ $timeFactory->expects(self::once())
+ ->method('getTime')
+ ->willReturn(1500);
+ $this->availabilityCoordinator->expects(self::once())
+ ->method('getCurrentOutOfOfficeData')
+ ->with($user)
+ ->willReturn($data);
+ $outOfOfficeService = $this->serviceMock->getParameter('outOfOfficeService');
+ $outOfOfficeService->expects(self::once())
+ ->method('disable')
+ ->with($mailAccount);
+ $outOfOfficeService->expects(self::never())
+ ->method('update');
+
+ $this->outOfOfficeController->followSystem(1);
+
+ self::assertTrue($mailAccount->getOutOfOfficeFollowsSystem());
+ }
+
+ public function enabledOutOfOfficeDataProvider(): array {
+ $user = $this->createMock(IUser::class);
+ return [
+ [$this->createOutOfOfficeData('2', $user, 1000, 2000, 'Subject', 'Message')],
+ [$this->createOutOfOfficeData('2', $user, 1500, 2000, 'Subject', 'Message')],
+ ];
+ }
+
+ /**
+ * @dataProvider enabledOutOfOfficeDataProvider
+ */
+ public function testFollowSystemWithEnabledOutOfOffice(?IOutOfOfficeData $data): void {
+ $startDate = $data->getStartDate();
+ $endDate = $data->getEndDate();
+
+ $user = $this->createMock(IUser::class);
+
+ $mailAccount = new MailAccount();
+ $mailAccount->setId(1);
+ $mailAccount->setOutOfOfficeFollowsSystem(false);
+ $account = new Account($mailAccount);
+
+ $userSession = $this->serviceMock->getParameter('userSession');
+ $userSession->expects(self::once())
+ ->method('getUser')
+ ->willReturn($user);
+ $accountService = $this->serviceMock->getParameter('accountService');
+ $accountService->expects(self::once())
+ ->method('findById')
+ ->with(1)
+ ->willReturn($account);
+ $timeFactory = $this->serviceMock->getParameter('timeFactory');
+ $timeFactory->expects(self::once())
+ ->method('getTime')
+ ->willReturn(1500);
+ $this->availabilityCoordinator->expects(self::once())
+ ->method('getCurrentOutOfOfficeData')
+ ->with($user)
+ ->willReturn($data);
+ $outOfOfficeService = $this->serviceMock->getParameter('outOfOfficeService');
+ $outOfOfficeService->expects(self::never())
+ ->method('disable');
+ $outOfOfficeService->expects(self::once())
+ ->method('update')
+ ->with($mailAccount, self::callback(function (OutOfOfficeState $state) use ($endDate, $startDate): bool {
+ self::assertTrue($state->isEnabled());
+ self::assertEquals(new DateTimeImmutable("@$startDate"), $state->getStart());
+ self::assertEquals(new DateTimeImmutable("@$endDate"), $state->getEnd());
+ self::assertEquals('Subject', $state->getSubject());
+ self::assertEquals('Message', $state->getMessage());
+ return true;
+ }));
+
+ $this->outOfOfficeController->followSystem(1);
+
+ self::assertTrue($mailAccount->getOutOfOfficeFollowsSystem());
+ }
+}
diff --git a/tests/Unit/Controller/PageControllerTest.php b/tests/Unit/Controller/PageControllerTest.php
index 937559f109..5bd1acf1f0 100644
--- a/tests/Unit/Controller/PageControllerTest.php
+++ b/tests/Unit/Controller/PageControllerTest.php
@@ -50,6 +50,7 @@
use OCP\IUserManager;
use OCP\IUserSession;
use PHPUnit\Framework\MockObject\MockObject;
+use Psr\Container\ContainerInterface;
use Psr\Log\LoggerInterface;
class PageControllerTest extends TestCase {
@@ -112,6 +113,9 @@ class PageControllerTest extends TestCase {
private SmimeService $smimeService;
+ /** @var ContainerInterface|MockObject */
+ private $container;
+
protected function setUp(): void {
parent::setUp();
@@ -134,6 +138,7 @@ protected function setUp(): void {
$this->credentialStore = $this->createMock(ICredentialStore::class);
$this->smimeService = $this->createMock(SmimeService::class);
$this->userManager = $this->createMock(IUserManager::class);
+ $this->container = $this->createMock(ContainerInterface::class);
$this->controller = new PageController(
$this->appName,
@@ -155,6 +160,7 @@ protected function setUp(): void {
$this->smimeService,
$this->aiIntegrationsService,
$this->userManager,
+ $this->container,
);
}
diff --git a/tests/Unit/Controller/SieveControllerTest.php b/tests/Unit/Controller/SieveControllerTest.php
index eec9f238fc..2f3c5693d9 100644
--- a/tests/Unit/Controller/SieveControllerTest.php
+++ b/tests/Unit/Controller/SieveControllerTest.php
@@ -4,6 +4,7 @@
/**
* @author Daniel Kesselberg
+ * @author Richard Steinmetz
*
* Mail
*
@@ -25,11 +26,10 @@
use ChristophWurst\Nextcloud\Testing\ServiceMockObject;
use Horde\ManageSieve\Exception;
-use OCA\Mail\Account;
use OCA\Mail\Controller\SieveController;
use OCA\Mail\Db\MailAccount;
-use OCA\Mail\Exception\ClientException;
use OCA\Mail\Exception\CouldNotConnectException;
+use OCA\Mail\Sieve\NamedSieveScript;
use OCA\Mail\Tests\Integration\TestCase;
use OCP\Security\IRemoteHostValidator;
use PHPUnit\Framework\MockObject\MockObject;
@@ -129,56 +129,23 @@ public function testUpdateAccountEnableNoConnection(): void {
}
public function testGetActiveScript(): void {
- $mailAccount = new MailAccount();
- $mailAccount->setSieveEnabled(true);
- $mailAccount->setSieveHost('localhost');
- $mailAccount->setSievePort(4190);
- $mailAccount->setSieveUser('user');
- $mailAccount->setSievePassword('password');
- $mailAccount->setSieveSslMode('');
-
- $accountService = $this->serviceMock->getParameter('accountService');
- $accountService->expects($this->once())
- ->method('find')
+ $sieveService = $this->serviceMock->getParameter('sieveService');
+ $sieveService->expects($this->once())
+ ->method('getActiveScript')
->with('1', 2)
- ->willReturn(new Account($mailAccount));
+ ->willReturn(new NamedSieveScript('nextcloud', '# foo bar'));
$response = $this->sieveController->getActiveScript(2);
$this->assertEquals(200, $response->getStatus());
- $this->assertEquals(['scriptName' => '', 'script' => ''], $response->getData());
- }
-
- public function testGetActiveScriptNoSieve(): void {
- $this->expectException(ClientException::class);
- $this->expectExceptionMessage('ManageSieve is disabled');
-
- $mailAccount = new MailAccount();
- $mailAccount->setSieveEnabled(false);
-
- $accountService = $this->serviceMock->getParameter('accountService');
- $accountService->expects($this->once())
- ->method('find')
- ->with('1', 2)
- ->willReturn(new Account($mailAccount));
-
- $this->sieveController->getActiveScript(2);
+ $this->assertEquals(['scriptName' => 'nextcloud', 'script' => '# foo bar'], $response->getData());
}
public function testUpdateActiveScript(): void {
- $mailAccount = new MailAccount();
- $mailAccount->setSieveEnabled(true);
- $mailAccount->setSieveHost('localhost');
- $mailAccount->setSievePort(4190);
- $mailAccount->setSieveUser('user');
- $mailAccount->setSievePassword('password');
- $mailAccount->setSieveSslMode('');
-
- $accountService = $this->serviceMock->getParameter('accountService');
- $accountService->expects($this->once())
- ->method('find')
- ->with('1', 2)
- ->willReturn(new Account($mailAccount));
+ $sieveService = $this->serviceMock->getParameter('sieveService');
+ $sieveService->expects($this->once())
+ ->method('updateActiveScript')
+ ->with('1', 2, 'sieve script');
$response = $this->sieveController->updateActiveScript(2, 'sieve script');
diff --git a/tests/Unit/Service/OutOfOffice/OutOfOfficeParserTest.php b/tests/Unit/Service/OutOfOffice/OutOfOfficeParserTest.php
new file mode 100644
index 0000000000..98a2f05ba4
--- /dev/null
+++ b/tests/Unit/Service/OutOfOffice/OutOfOfficeParserTest.php
@@ -0,0 +1,189 @@
+
+ *
+ * @author Richard Steinmetz
+ *
+ * @license AGPL-3.0-or-later
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+namespace Unit\Service\OutOfOffice;
+
+use ChristophWurst\Nextcloud\Testing\TestCase;
+use DateTimeImmutable;
+use OCA\Mail\Service\OutOfOffice\OutOfOfficeParser;
+use OCA\Mail\Service\OutOfOffice\OutOfOfficeState;
+
+class OutOfOfficeParserTest extends TestCase {
+ private OutOfOfficeParser $outOfOfficeParser;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->outOfOfficeParser = new OutOfOfficeParser();
+ }
+
+ public function testParseEnabledResponder(): void {
+ $script = file_get_contents(__DIR__ . "/../../../data/sieve-vacation-on.txt");
+ $cleanedScript = file_get_contents(__DIR__ . "/../../../data/sieve-vacation-cleaned.txt");
+
+ $actual = $this->outOfOfficeParser->parseOutOfOfficeState($script);
+ self::assertEquals($script, $actual->getSieveScript());
+ self::assertEquals($cleanedScript, $actual->getUntouchedSieveScript());
+ self::assertEquals(1, $actual->getState()->getVersion());
+ self::assertEquals(true, $actual->getState()->isEnabled());
+ self::assertEquals(new DateTimeImmutable("2022-09-02"), $actual->getState()->getStart());
+ self::assertEquals(new DateTimeImmutable("2022-09-08"), $actual->getState()->getEnd());
+ self::assertEquals("On vacation", $actual->getState()->getSubject());
+ self::assertEquals("I'm on vacation.", $actual->getState()->getMessage());
+ }
+
+ public function testParseDisabledResponder(): void {
+ $script = file_get_contents(__DIR__ . "/../../../data/sieve-vacation-off.txt");
+ $cleanedScript = file_get_contents(__DIR__ . "/../../../data/sieve-vacation-cleaned.txt");
+
+ $actual = $this->outOfOfficeParser->parseOutOfOfficeState($script);
+ self::assertEquals($script, $actual->getSieveScript());
+ self::assertEquals($cleanedScript, $actual->getUntouchedSieveScript());
+ self::assertEquals(1, $actual->getState()->getVersion());
+ self::assertEquals(false, $actual->getState()->isEnabled());
+ self::assertEquals(null, $actual->getState()->getStart());
+ self::assertEquals(null, $actual->getState()->getEnd());
+ self::assertEquals("On vacation", $actual->getState()->getSubject());
+ self::assertEquals("I'm on vacation.", $actual->getState()->getMessage());
+ }
+
+ public function testParseLeaveForeignScriptUntouched(): void {
+ $script = file_get_contents(__DIR__ . "/../../../data/sieve-vacation-cleaned.txt");
+
+ $actual = $this->outOfOfficeParser->parseOutOfOfficeState($script);
+ self::assertEquals($script, $actual->getSieveScript());
+ self::assertEquals($script, $actual->getUntouchedSieveScript());
+ self::assertEquals(null, $actual->getState());
+ }
+
+ public function testBuildEnabledResponder(): void {
+ $script = file_get_contents(__DIR__ . "/../../../data/sieve-vacation-cleaned.txt");
+ $expected = file_get_contents(__DIR__ . "/../../../data/sieve-vacation-on.txt");
+
+ $actual = $this->outOfOfficeParser->buildSieveScript(
+ new OutOfOfficeState(
+ true,
+ new DateTimeImmutable("2022-09-02"),
+ new DateTimeImmutable("2022-09-08"),
+ "On vacation",
+ "I'm on vacation.",
+ ),
+ $script,
+ ["Test Test ", "Test Alias "],
+ );
+ self::assertEquals($expected, $actual);
+ }
+
+ public function testBuildEnabledResponderWithoutEndDate(): void {
+ $script = file_get_contents(__DIR__ . "/../../../data/sieve-vacation-cleaned.txt");
+ $expected = file_get_contents(__DIR__ . "/../../../data/sieve-vacation-on-no-end-date.txt");
+
+ $actual = $this->outOfOfficeParser->buildSieveScript(
+ new OutOfOfficeState(
+ true,
+ new DateTimeImmutable("2022-09-02"),
+ null,
+ "On vacation",
+ "I'm on vacation.",
+ ),
+ $script,
+ ["Test Test ", "Test Alias "],
+ );
+ self::assertEquals($expected, $actual);
+ }
+
+ public function testBuildEnabledResponderWithSpecialCharsInMessage(): void {
+ $script = file_get_contents(__DIR__ . "/../../../data/sieve-vacation-cleaned.txt");
+ $expected = file_get_contents(__DIR__ . "/../../../data/sieve-vacation-on-special-chars-message.txt");
+
+ $actual = $this->outOfOfficeParser->buildSieveScript(
+ new OutOfOfficeState(
+ true,
+ new DateTimeImmutable("2022-09-02"),
+ null,
+ "On vacation",
+ "I'm on vacation.\n\"Hello, World!\"\n\\ escaped backslash",
+ ),
+ $script,
+ ["Test Test ", "Test Alias "],
+ );
+ self::assertEquals($expected, $actual);
+ }
+
+ public function testBuildEnabledResponderWithSpecialCharsInSubject(): void {
+ $script = file_get_contents(__DIR__ . "/../../../data/sieve-vacation-cleaned.txt");
+ $expected = file_get_contents(__DIR__ . "/../../../data/sieve-vacation-on-special-chars-subject.txt");
+
+ $actual = $this->outOfOfficeParser->buildSieveScript(
+ new OutOfOfficeState(
+ true,
+ new DateTimeImmutable("2022-09-02"),
+ null,
+ "On vacation, \"Hello, World!\", \\ escaped backslash",
+ "I'm on vacation.",
+ ),
+ $script,
+ ["Test Test ", "Test Alias "],
+ );
+ self::assertEquals($expected, $actual);
+ }
+
+ public function testBuildEnabledResponderWithSubjectPlaceholder(): void {
+ $script = file_get_contents(__DIR__ . "/../../../data/sieve-vacation-cleaned.txt");
+ $expected = file_get_contents(__DIR__ . "/../../../data/sieve-vacation-on-subject-placeholder.txt");
+
+ $actual = $this->outOfOfficeParser->buildSieveScript(
+ new OutOfOfficeState(
+ true,
+ new DateTimeImmutable("2022-09-02"),
+ new DateTimeImmutable("2022-09-08"),
+ 'Re: ${subject}',
+ "I'm on vacation.",
+ ),
+ $script,
+ ["Test Test ", "Test Alias "],
+ );
+ self::assertEquals($expected, $actual);
+ }
+
+ public function testBuildDisabledResponder(): void {
+ $script = file_get_contents(__DIR__ . "/../../../data/sieve-vacation-cleaned.txt");
+ $expected = file_get_contents(__DIR__ . "/../../../data/sieve-vacation-off.txt");
+
+ $actual = $this->outOfOfficeParser->buildSieveScript(
+ new OutOfOfficeState(
+ false,
+ null,
+ null,
+ "On vacation",
+ "I'm on vacation.",
+ ),
+ $script,
+ ["Test Test ", "Test Alias "],
+ );
+ self::assertEquals($expected, $actual);
+ }
+}
diff --git a/tests/Unit/Service/SieveServiceTest.php b/tests/Unit/Service/SieveServiceTest.php
new file mode 100644
index 0000000000..4dba30495c
--- /dev/null
+++ b/tests/Unit/Service/SieveServiceTest.php
@@ -0,0 +1,258 @@
+
+ *
+ * @author Richard Steinmetz
+ *
+ * @license AGPL-3.0-or-later
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+namespace Unit\Service;
+
+use ChristophWurst\Nextcloud\Testing\TestCase;
+use OCA\Mail\Account;
+use OCA\Mail\Db\MailAccount;
+use OCA\Mail\Exception\ClientException;
+use OCA\Mail\Service\AccountService;
+use OCA\Mail\Service\SieveService;
+use OCA\Mail\Sieve\SieveClientFactory;
+use PHPUnit\Framework\MockObject\MockObject;
+
+class SieveServiceTest extends TestCase {
+ private SieveService $sieveService;
+
+ /** @var SieveClientFactory|MockObject */
+ private $sieveClientFactory;
+
+ /** @var AccountService|MockObject */
+ private $accountService;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->sieveClientFactory = $this->createMock(SieveClientFactory::class);
+ $this->accountService = $this->createMock(AccountService::class);
+
+ $this->sieveService = new SieveService(
+ $this->sieveClientFactory,
+ $this->accountService,
+ );
+ }
+
+ public function testGetActiveScript(): void {
+ $mailAccount = new MailAccount();
+ $mailAccount->setSieveEnabled(true);
+ $mailAccount->setSieveHost('localhost');
+ $mailAccount->setSievePort(4190);
+ $mailAccount->setSieveUser('user');
+ $mailAccount->setSievePassword('password');
+ $mailAccount->setSieveSslMode('');
+ $account = new Account($mailAccount);
+
+ $client = $this->createMock(\Horde\ManageSieve::class);
+ $client->expects(self::once())
+ ->method('getActive')
+ ->willReturn('nextcloud');
+ $client->expects(self::once())
+ ->method('getScript')
+ ->with('nextcloud')
+ ->willReturn('# foo bar');
+
+ $this->accountService->expects(self::once())
+ ->method('find')
+ ->with('1', 2)
+ ->willReturn($account);
+ $this->sieveClientFactory->expects(self::once())
+ ->method('getClient')
+ ->with($account)
+ ->willReturn($client);
+
+ $actual = $this->sieveService->getActiveScript('1', 2);
+ $this->assertEquals('nextcloud', $actual->getName());
+ $this->assertEquals('# foo bar', $actual->getScript());
+ }
+
+ public function testGetActiveScriptNoName(): void {
+ $mailAccount = new MailAccount();
+ $mailAccount->setSieveEnabled(true);
+ $mailAccount->setSieveHost('localhost');
+ $mailAccount->setSievePort(4190);
+ $mailAccount->setSieveUser('user');
+ $mailAccount->setSievePassword('password');
+ $mailAccount->setSieveSslMode('');
+ $account = new Account($mailAccount);
+
+ $client = $this->createMock(\Horde\ManageSieve::class);
+ $client->expects(self::once())
+ ->method('getActive')
+ ->willReturn(null);
+ $client->expects(self::never())
+ ->method('getScript');
+
+ $this->accountService->expects(self::once())
+ ->method('find')
+ ->with('1', 2)
+ ->willReturn($account);
+ $this->sieveClientFactory->expects(self::once())
+ ->method('getClient')
+ ->with($account)
+ ->willReturn($client);
+
+ $actual = $this->sieveService->getActiveScript('1', 2);
+ $this->assertEquals('', $actual->getName());
+ $this->assertEquals('', $actual->getScript());
+ }
+
+ public function scriptTrimDataProvider(): array {
+ return [
+ ["# foo bar", "# foo bar"],
+ ["# foo bar\r\n", "# foo bar"],
+ ["# foo bar\r\n\r\n", "# foo bar"],
+ ["\r\n# foo bar", "\r\n# foo bar"],
+ ["# foo bar ", "# foo bar "],
+ ];
+ }
+
+ /**
+ * @dataProvider scriptTrimDataProvider
+ */
+ public function testGetActiveScriptTrimsTrailingLineFeeds(string $script, string $expected): void {
+ $mailAccount = new MailAccount();
+ $mailAccount->setSieveEnabled(true);
+ $mailAccount->setSieveHost('localhost');
+ $mailAccount->setSievePort(4190);
+ $mailAccount->setSieveUser('user');
+ $mailAccount->setSievePassword('password');
+ $mailAccount->setSieveSslMode('');
+ $account = new Account($mailAccount);
+
+ $client = $this->createMock(\Horde\ManageSieve::class);
+ $client->expects(self::once())
+ ->method('getActive')
+ ->willReturn('nextcloud');
+ $client->expects(self::once())
+ ->method('getScript')
+ ->with('nextcloud')
+ ->willReturn($script);
+
+ $this->accountService->expects(self::once())
+ ->method('find')
+ ->with('1', 2)
+ ->willReturn($account);
+ $this->sieveClientFactory->expects(self::once())
+ ->method('getClient')
+ ->with($account)
+ ->willReturn($client);
+
+ $actual = $this->sieveService->getActiveScript('1', 2);
+ $this->assertEquals('nextcloud', $actual->getName());
+ $this->assertEquals($expected, $actual->getScript());
+ }
+
+ public function testGetActiveScriptNoSieve(): void {
+ $this->expectException(ClientException::class);
+ $this->expectExceptionMessage('ManageSieve is disabled');
+
+ $mailAccount = new MailAccount();
+ $mailAccount->setSieveEnabled(false);
+
+ $this->accountService->expects(self::once())
+ ->method('find')
+ ->with('1', 2)
+ ->willReturn(new Account($mailAccount));
+
+ $this->sieveService->getActiveScript('1', 2);
+ }
+
+ public function testUpdateActiveScript(): void {
+ $mailAccount = new MailAccount();
+ $mailAccount->setSieveEnabled(true);
+ $mailAccount->setSieveHost('localhost');
+ $mailAccount->setSievePort(4190);
+ $mailAccount->setSieveUser('user');
+ $mailAccount->setSievePassword('password');
+ $mailAccount->setSieveSslMode('');
+ $account = new Account($mailAccount);
+
+ $client = $this->createMock(\Horde\ManageSieve::class);
+ $client->expects(self::once())
+ ->method('getActive')
+ ->willReturn('nextcloud');
+ $client->expects(self::once())
+ ->method('installScript')
+ ->with('nextcloud', '# foo bar', true);
+
+ $this->accountService->expects(self::once())
+ ->method('find')
+ ->with('1', 2)
+ ->willReturn($account);
+ $this->sieveClientFactory->expects(self::once())
+ ->method('getClient')
+ ->with($account)
+ ->willReturn($client);
+
+ $this->sieveService->updateActiveScript('1', 2, '# foo bar');
+ }
+
+ public function testUpdateActiveScriptWithNoName(): void {
+ $mailAccount = new MailAccount();
+ $mailAccount->setSieveEnabled(true);
+ $mailAccount->setSieveHost('localhost');
+ $mailAccount->setSievePort(4190);
+ $mailAccount->setSieveUser('user');
+ $mailAccount->setSievePassword('password');
+ $mailAccount->setSieveSslMode('');
+ $account = new Account($mailAccount);
+
+ $client = $this->createMock(\Horde\ManageSieve::class);
+ $client->expects(self::once())
+ ->method('getActive')
+ ->willReturn(null);
+ $client->expects(self::once())
+ ->method('installScript')
+ ->with('nextcloud', '# foo bar', true);
+
+ $this->accountService->expects(self::once())
+ ->method('find')
+ ->with('1', 2)
+ ->willReturn($account);
+ $this->sieveClientFactory->expects(self::once())
+ ->method('getClient')
+ ->with($account)
+ ->willReturn($client);
+
+ $this->sieveService->updateActiveScript('1', 2, '# foo bar');
+ }
+
+ public function testUpdateActiveScriptNoSieve(): void {
+ $this->expectException(ClientException::class);
+ $this->expectExceptionMessage('ManageSieve is disabled');
+
+ $mailAccount = new MailAccount();
+ $mailAccount->setSieveEnabled(false);
+
+ $this->accountService->expects(self::once())
+ ->method('find')
+ ->with('1', 2)
+ ->willReturn(new Account($mailAccount));
+
+ $this->sieveService->updateActiveScript('1', 2, '# foo bar');
+ }
+}
diff --git a/src/tests/data/sieve-vacation-cleaned.txt b/tests/data/sieve-vacation-cleaned.txt
similarity index 100%
rename from src/tests/data/sieve-vacation-cleaned.txt
rename to tests/data/sieve-vacation-cleaned.txt
diff --git a/src/tests/data/sieve-vacation-off.txt b/tests/data/sieve-vacation-off.txt
similarity index 100%
rename from src/tests/data/sieve-vacation-off.txt
rename to tests/data/sieve-vacation-off.txt
diff --git a/src/tests/data/sieve-vacation-on-no-end-date.txt b/tests/data/sieve-vacation-on-no-end-date.txt
similarity index 100%
rename from src/tests/data/sieve-vacation-on-no-end-date.txt
rename to tests/data/sieve-vacation-on-no-end-date.txt
diff --git a/src/tests/data/sieve-vacation-on-special-chars-message.txt b/tests/data/sieve-vacation-on-special-chars-message.txt
similarity index 100%
rename from src/tests/data/sieve-vacation-on-special-chars-message.txt
rename to tests/data/sieve-vacation-on-special-chars-message.txt
diff --git a/src/tests/data/sieve-vacation-on-special-chars-subject.txt b/tests/data/sieve-vacation-on-special-chars-subject.txt
similarity index 100%
rename from src/tests/data/sieve-vacation-on-special-chars-subject.txt
rename to tests/data/sieve-vacation-on-special-chars-subject.txt
diff --git a/src/tests/data/sieve-vacation-on-subject-placeholder.txt b/tests/data/sieve-vacation-on-subject-placeholder.txt
similarity index 100%
rename from src/tests/data/sieve-vacation-on-subject-placeholder.txt
rename to tests/data/sieve-vacation-on-subject-placeholder.txt
diff --git a/src/tests/data/sieve-vacation-on.txt b/tests/data/sieve-vacation-on.txt
similarity index 100%
rename from src/tests/data/sieve-vacation-on.txt
rename to tests/data/sieve-vacation-on.txt
diff --git a/tests/psalm-baseline.xml b/tests/psalm-baseline.xml
index f39a3adb99..d306724e2c 100644
--- a/tests/psalm-baseline.xml
+++ b/tests/psalm-baseline.xml
@@ -1,6 +1,10 @@
+
+ OutOfOfficeListener::class
+ OutOfOfficeListener::class
+
FilteringProvider
ImportantMailWidgetV2
@@ -38,4 +42,16 @@
MailWidgetV2
+
+
+ IEventListener
+
+
+ $event
+
+
+ OutOfOfficeListener
+ OutOfOfficeListener
+
+