From 6df2d69eb856ba1cdcbab5aa2ef70405c17c742f Mon Sep 17 00:00:00 2001
From: Sergii Leonenko <sleonenko@sipwise.com>
Date: Mon, 7 Dec 2020 22:59:05 +0200
Subject: [PATCH] TT#96352 - CSC: As a Customer, I want change my Mail2Fax
 settings

AC:
If not already exists:
Can see a separate main menu item "Fax Settings"
Can click the separate main menu item and land on a page "Fax Settings" (route=/user/fax-settings)

Can see settings if the feature enabled
Can decide either to use SecretKey or ACL to manage authentication

Can set a custom secret key/token
Can set the renew interval (never, daily, weekly, monthly)
Can see/read the "Last Secret Key Modify Time"
Can add email addresses to get notified about expired key (secret_renew_notify)
Can remove email addresses

Can add ACL Rule (email, ip, destination, use-regexp flag)
Can edit ACL Rule (email, ip, destination, use-regexp flag)
Can remove ACL Rule
Change-Id: I6bc25ab2f73d0dfae3fab224b11396ecdd17ab39
---
 src/api/fax.js                                |  27 ++
 src/boot/i18n.js                              |   1 +
 src/components/CscMainMenuTop.vue             |   5 +-
 src/components/CscTooltip.vue                 |  55 +++
 src/components/form/CscInputSaveable.vue      |   1 +
 .../CallForward/CscAddDestinationForm.vue     |   4 +-
 ...nation.vue => CscFaxToMailDestination.vue} |   8 +-
 ...rm.vue => CscFaxToMailDestinationForm.vue} |   4 +-
 .../FaxSettings/CscFaxToMailSettings.vue      | 272 +++++++++++++
 .../pages/FaxSettings/CscMailToFaxACL.vue     | 109 +++++
 .../pages/FaxSettings/CscMailToFaxACLForm.vue | 177 +++++++++
 .../CscMailToFaxRenewNotifyEmail.vue          | 152 +++++++
 .../CscMailToFaxRenewNotifyEmailForm.vue      | 129 ++++++
 .../FaxSettings/CscMailToFaxSettings.vue      | 373 ++++++++++++++++++
 src/css/app.styl                              |   6 +
 src/i18n/en.json                              |  15 +-
 src/layouts/CscLayoutMain.vue                 |   6 +-
 src/pages/CscPageFaxSettings.vue              | 291 ++------------
 src/plugins/rtc-engine.js                     |  28 +-
 src/router/routes.js                          |   8 +
 src/store/call-forward.js                     |   4 +-
 src/store/fax.js                              |  33 +-
 src/store/user.js                             |   6 +-
 23 files changed, 1422 insertions(+), 292 deletions(-)
 create mode 100644 src/components/CscTooltip.vue
 rename src/components/pages/FaxSettings/{CscFax2MailDestination.vue => CscFaxToMailDestination.vue} (91%)
 rename src/components/pages/FaxSettings/{CscFax2MailDestinationForm.vue => CscFaxToMailDestinationForm.vue} (98%)
 create mode 100644 src/components/pages/FaxSettings/CscFaxToMailSettings.vue
 create mode 100644 src/components/pages/FaxSettings/CscMailToFaxACL.vue
 create mode 100644 src/components/pages/FaxSettings/CscMailToFaxACLForm.vue
 create mode 100644 src/components/pages/FaxSettings/CscMailToFaxRenewNotifyEmail.vue
 create mode 100644 src/components/pages/FaxSettings/CscMailToFaxRenewNotifyEmailForm.vue
 create mode 100644 src/components/pages/FaxSettings/CscMailToFaxSettings.vue

diff --git a/src/api/fax.js b/src/api/fax.js
index 10215509..946569b2 100644
--- a/src/api/fax.js
+++ b/src/api/fax.js
@@ -32,3 +32,30 @@ export async function setFaxServerField (options) {
 		value: options.value
 	})
 }
+
+export async function getMailToFaxSettings (subscriberId) {
+	const result = await get({
+		path: `api/mailtofaxsettings/${subscriberId}`
+	})
+	const settings = _.clone(result)
+	delete settings._links
+	return settings
+}
+
+export async function setMailToFaxSettingField (options) {
+	if (!['active', 'secret_key', 'secret_key_renew', 'secret_renew_notify', 'acl'].includes(options.field)) {
+		throw Error(`setMailToFaxSettingField: unknown field name ${options.field}`)
+	}
+	if (options.field === 'secret_renew_notify') {
+		// searching for duplicates
+		const destinationsIds = options.value.map(d => d.destination)
+		if ((new Set(destinationsIds)).size !== destinationsIds.length) {
+			throw Error(i18n.t('faxSettings.notifyEmailExists'))
+		}
+	}
+	return patchReplaceFull({
+		path: `api/mailtofaxsettings/${options.subscriberId}`,
+		fieldPath: options.field,
+		value: options.value
+	})
+}
diff --git a/src/boot/i18n.js b/src/boot/i18n.js
index e8e7a3fa..61b5dccf 100644
--- a/src/boot/i18n.js
+++ b/src/boot/i18n.js
@@ -17,6 +17,7 @@ export const defaultLocale = 'en-US'
 export const i18n = new VueI18n({
 	locale: defaultLocale,
 	fallbackLocale: defaultLocale,
+	formatFallbackMessages: true,
 	messages
 })
 
diff --git a/src/components/CscMainMenuTop.vue b/src/components/CscMainMenuTop.vue
index 2b8077d3..cebe6413 100644
--- a/src/components/CscMainMenuTop.vue
+++ b/src/components/CscMainMenuTop.vue
@@ -46,7 +46,8 @@ export default {
 	computed: {
 		...mapGetters('user', [
 			'isRtcEngineUiVisible',
-			'isPbxEnabled'
+			'isPbxEnabled',
+			'hasFaxCapability'
 		]),
 		items () {
 			return [
@@ -129,7 +130,7 @@ export default {
 					to: '/user/fax-settings',
 					icon: 'fas fa-fax',
 					label: this.$t('navigation.faxSettings.title'),
-					visible: true
+					visible: this.hasFaxCapability
 				},
 				{
 					icon: 'miscellaneous_services',
diff --git a/src/components/CscTooltip.vue b/src/components/CscTooltip.vue
new file mode 100644
index 00000000..a6a2e80f
--- /dev/null
+++ b/src/components/CscTooltip.vue
@@ -0,0 +1,55 @@
+<template>
+	<q-tooltip
+		ref="tooltip"
+		:delay="delay"
+		:content-class="contentClass"
+		v-bind="$attrs"
+		v-on="$listeners"
+		@show="autoHide"
+		@hide="cancelAutoHide"
+	>
+		<slot />
+	</q-tooltip>
+</template>
+
+<script>
+export default {
+	name: 'CscTooltip',
+	props: {
+		autoHideDelay: {
+			type: Number,
+			default: 5000
+		},
+		delay: {
+			type: Number,
+			default: 500
+		},
+		contentClass: {
+			type: String,
+			default: 'text-dark'
+		}
+	},
+	data () {
+		return {
+			autoHideHandler: undefined
+		}
+	},
+	beforeDestroy () {
+		this.cancelAutoHide()
+	},
+	methods: {
+		autoHide () {
+			this.cancelAutoHide()
+			this.autoHideHandler = setTimeout(() => {
+					this.autoHideHandler = undefined
+					this.$refs.tooltip.hide()
+				},
+				this.autoHideDelay
+			)
+		},
+		cancelAutoHide () {
+			clearTimeout(this.autoHideHandler)
+		}
+	}
+}
+</script>
diff --git a/src/components/form/CscInputSaveable.vue b/src/components/form/CscInputSaveable.vue
index 4c78f4e8..c407819f 100644
--- a/src/components/form/CscInputSaveable.vue
+++ b/src/components/form/CscInputSaveable.vue
@@ -7,6 +7,7 @@
 		@input="$emit('input', $event)"
 		@keyup.enter="$emit('save', $event)"
 	>
+		<slot />
 		<template
 			v-if="icon !== undefined && icon !== null"
 			v-slot:prepend
diff --git a/src/components/pages/CallForward/CscAddDestinationForm.vue b/src/components/pages/CallForward/CscAddDestinationForm.vue
index da262aed..1cbdc4ec 100644
--- a/src/components/pages/CallForward/CscAddDestinationForm.vue
+++ b/src/components/pages/CallForward/CscAddDestinationForm.vue
@@ -18,7 +18,7 @@
 					@click="addDestinationByType('voicebox')"
 				/>
 				<csc-popup-menu-item
-					v-if="hasFaxCapability && hasSendFaxFeature"
+					v-if="hasFaxCapabilityAndFaxActive && hasSendFaxFeature"
 					:label="$t('pages.callForward.buttons.addFax2Mail')"
 					@click="addDestinationByType('fax2mail')"
 				/>
@@ -150,7 +150,7 @@ export default {
 		]),
 		...mapGetters('user', [
 			'hasSendFaxFeature',
-			'hasFaxCapability'
+			'hasFaxCapabilityAndFaxActive'
 		]),
 		timeoutInputError () {
 			if (!this.$v.destinationForm.timeout.required) {
diff --git a/src/components/pages/FaxSettings/CscFax2MailDestination.vue b/src/components/pages/FaxSettings/CscFaxToMailDestination.vue
similarity index 91%
rename from src/components/pages/FaxSettings/CscFax2MailDestination.vue
rename to src/components/pages/FaxSettings/CscFaxToMailDestination.vue
index b9ba9cc1..a8f79570 100644
--- a/src/components/pages/FaxSettings/CscFax2MailDestination.vue
+++ b/src/components/pages/FaxSettings/CscFaxToMailDestination.vue
@@ -43,7 +43,7 @@
 			</csc-list-menu-item>
 		</template>
 		<template slot="body">
-			<csc-fax2-mail-destination-form
+			<csc-fax-to-mail-destination-form
 				:is-add-new-mode="false"
 				:initial-data="destination"
 				:loading="loading"
@@ -58,12 +58,12 @@ import CscListItem from 'components/CscListItem'
 import CscListItemTitle from 'components/CscListItemTitle'
 import CscListMenuItem from 'components/CscListMenuItem'
 import CscListItemSubtitle from 'components/CscListItemSubtitle'
-import CscFax2MailDestinationForm from 'components/pages/FaxSettings/CscFax2MailDestinationForm'
+import CscFaxToMailDestinationForm from 'components/pages/FaxSettings/CscFaxToMailDestinationForm'
 
 export default {
-	name: 'CscFax2MailDestination',
+	name: 'CscFaxToMailDestination',
 	components: {
-		CscFax2MailDestinationForm,
+		CscFaxToMailDestinationForm,
 		CscListItemSubtitle,
 		CscListMenuItem,
 		CscListItemTitle,
diff --git a/src/components/pages/FaxSettings/CscFax2MailDestinationForm.vue b/src/components/pages/FaxSettings/CscFaxToMailDestinationForm.vue
similarity index 98%
rename from src/components/pages/FaxSettings/CscFax2MailDestinationForm.vue
rename to src/components/pages/FaxSettings/CscFaxToMailDestinationForm.vue
index 4a39406d..7ae18398 100644
--- a/src/components/pages/FaxSettings/CscFax2MailDestinationForm.vue
+++ b/src/components/pages/FaxSettings/CscFaxToMailDestinationForm.vue
@@ -70,7 +70,7 @@
 			<q-btn
 				flat
 				color="primary"
-				icon="person"
+				icon="done"
 				:loading="loading"
 				:disable="$v.data.$invalid || loading"
 				:label="$t('faxSettings.createDestination')"
@@ -85,7 +85,7 @@ import { email, required } from 'vuelidate/lib/validators'
 import CscInputSaveable from 'components/form/CscInputSaveable'
 
 export default {
-	name: 'CscFax2MailDestinationForm',
+	name: 'CscFaxToMailDestinationForm',
 	components: {
 		CscInputSaveable
 	},
diff --git a/src/components/pages/FaxSettings/CscFaxToMailSettings.vue b/src/components/pages/FaxSettings/CscFaxToMailSettings.vue
new file mode 100644
index 00000000..6a85d590
--- /dev/null
+++ b/src/components/pages/FaxSettings/CscFaxToMailSettings.vue
@@ -0,0 +1,272 @@
+<template>
+	<div>
+		<q-list
+			class="col col-xs-12 col-md-6"
+			dense
+		>
+			<q-item>
+				<q-item-section>
+					<q-toggle
+						v-model="faxToMailSettings.active"
+						:label="$t('faxSettings.active')"
+						:disable="!dataLoaded"
+						@input="setChangedData('active', !faxServerSettings.active)"
+					/>
+				</q-item-section>
+				<q-item-section
+					side
+				>
+					<csc-spinner
+						v-if="loadingFaxServerSettings"
+						class="self-center"
+					/>
+				</q-item-section>
+			</q-item>
+			<q-item>
+				<q-item-section>
+					<csc-input-saveable
+						v-model.trim="faxToMailSettings.name"
+						:label="$t('faxSettings.sendfaxHeaderName')"
+						:disable="!dataLoaded"
+						:loading="loadingFaxServerSettings"
+						:value-changed="nameChanged"
+						@save="setChangedData('name', faxToMailSettings.name)"
+						@undo="restoreName"
+					/>
+				</q-item-section>
+			</q-item>
+			<q-item>
+				<q-item-section>
+					<q-toggle
+						v-model="faxToMailSettings.t38"
+						:label="$t('faxSettings.T38')"
+						:disable="!dataLoaded"
+						@input="setChangedData('t38', !faxServerSettings.t38)"
+					/>
+				</q-item-section>
+				<q-item-section
+					side
+				>
+					<csc-spinner
+						v-if="loadingFaxServerSettings"
+						class="self-center"
+					/>
+				</q-item-section>
+			</q-item>
+			<q-item>
+				<q-item-section>
+					<q-toggle
+						v-model="faxToMailSettings.ecm"
+						:label="$t('faxSettings.ECM')"
+						:disable="!dataLoaded"
+						@input="setChangedData('ecm', !faxServerSettings.ecm)"
+					/>
+				</q-item-section>
+				<q-item-section
+					side
+				>
+					<csc-spinner
+						v-if="loadingFaxServerSettings"
+						class="self-center"
+					/>
+				</q-item-section>
+			</q-item>
+			<q-item class="row">
+				<div class="col">
+					<span class="text-h6">{{ $t('faxSettings.destinations') }}:</span>
+				</div>
+				<div class="col text-center">
+					<csc-spinner
+						v-if="loadingFaxServerSettings"
+					/>
+				</div>
+				<div class="col text-right">
+					<q-btn
+						flat
+						color="primary"
+						icon="add"
+						:disable="!dataLoaded || showAddNewDestination"
+						@click="openAddNewDestination"
+					>
+						{{ $t('faxSettings.addDestination') }}
+					</q-btn>
+				</div>
+			</q-item>
+		</q-list>
+		<q-separator />
+		<div
+			class="row justify-center q-mb-lg"
+		>
+			<q-list
+				class="col-xs-12"
+			>
+				<q-item
+					v-if="showAddNewDestination"
+					class="row justify-center"
+				>
+					<csc-fax-to-mail-destination-form
+						v-if="showAddNewDestination"
+						ref="addNewDestination"
+						:loading="loadingFaxServerSettings"
+						:is-add-new-mode="true"
+						@save="addNewDestination"
+						@cancel="closeAddNewDestination"
+					/>
+				</q-item>
+				<q-item
+					v-if="!hasDestinations"
+					class="row justify-center"
+				>
+					{{ $t('faxSettings.noDestinationsCreatedYet') }}
+				</q-item>
+				<csc-fax-to-mail-destination
+					v-for="(destinationItem, index) in faxToMailSettings.destinations"
+					:key="destinationItem.destination"
+					:odd="(index % 2) === 0"
+					:expanded="expandedDestinationId === destinationItem.destination"
+					:destination="destinationItem"
+					:loading="loadingFaxServerSettings"
+					@collapse="expandedDestinationId = null"
+					@expand="expandedDestinationId = destinationItem.destination"
+					@remove="openDeleteDestinationDialog(destinationItem.destination)"
+					@update-property="updateDestinationItemProperty(destinationItem.destination, ...arguments)"
+				/>
+			</q-list>
+		</div>
+	</div>
+</template>
+
+<script>
+import _ from 'lodash'
+import { mapState } from 'vuex'
+import CscInputSaveable from 'components/form/CscInputSaveable'
+import CscSpinner from 'components/CscSpinner'
+import { mapWaitingActions, mapWaitingGetters } from 'vue-wait'
+import CscFaxToMailDestinationForm from 'components/pages/FaxSettings/CscFaxToMailDestinationForm'
+import CscFaxToMailDestination from 'components/pages/FaxSettings/CscFaxToMailDestination'
+import CscRemoveDialog from 'components/CscRemoveDialog'
+import { showGlobalError } from 'src/helpers/ui'
+export default {
+	name: 'CscFaxToMailSettings',
+	components: {
+		CscFaxToMailDestination,
+		CscFaxToMailDestinationForm,
+		CscSpinner,
+		CscInputSaveable
+	},
+	data () {
+		return {
+			faxToMailSettings: {},
+			showAddNewDestination: false,
+			expandedDestinationId: null
+		}
+	},
+	computed: {
+		...mapState('fax', [
+			'faxServerSettings',
+			'faxServerSettingsInitialized'
+		]),
+		...mapWaitingGetters({
+			loadingFaxServerSettings: 'loading faxServerSettings'
+		}),
+		dataLoaded () {
+			return this.faxServerSettingsInitialized && !this.loadingFaxServerSettings
+		},
+		hasDestinations () {
+			return this.faxToMailSettings?.destinations?.length
+		},
+		nameChanged () {
+			return this.faxToMailSettings.name !== this.faxServerSettings.name
+		}
+
+	},
+	mounted () {
+		this.loadFaxServerSettings()
+	},
+	methods: {
+		...mapWaitingActions('fax', {
+			loadFaxSettingsAction: 'loading faxServerSettings',
+			faxServerSettingsUpdateAction: 'loading faxServerSettings'
+		}),
+		async loadFaxServerSettings () {
+			try {
+				await this.loadFaxSettingsAction()
+				this.updateDataFromStore()
+			} catch (err) {
+				showGlobalError(err?.message)
+			}
+		},
+		updateDataFromStore () {
+			this.faxToMailSettings = _.cloneDeep(this.faxServerSettings)
+		},
+		async setChangedData (field, value) {
+			try {
+				await this.faxServerSettingsUpdateAction({ field, value })
+				this.updateDataFromStore()
+			} catch (err) {
+				showGlobalError(err?.message)
+			}
+		},
+		restoreName () {
+			this.faxToMailSettings.name = this.faxServerSettings.name
+		},
+		async updateDestinations (destinationItems, beforeUpdateUI = () => {}) {
+			try {
+				await this.faxServerSettingsUpdateAction({
+					field: 'destinations',
+					value: destinationItems
+				})
+				beforeUpdateUI()
+				this.updateDataFromStore()
+			} catch (err) {
+				showGlobalError(err?.message)
+			}
+		},
+		openAddNewDestination () {
+			this.showAddNewDestination = true
+		},
+		closeAddNewDestination () {
+			this.showAddNewDestination = false
+			this.$refs.addNewDestination.reset()
+		},
+		addNewDestination (destination) {
+			const destinationItems = [...this.faxToMailSettings.destinations, destination]
+
+			this.updateDestinations(destinationItems, () => {
+				this.closeAddNewDestination()
+			})
+		},
+		deleteDestination (destinationId) {
+			const destinationItems = this.faxToMailSettings.destinations.filter(d => d.destination !== destinationId)
+			this.faxServerSettingsUpdateAction({
+				field: 'destinations',
+				value: destinationItems
+			}).then(() => {
+				if (this.expandedDestinationId === destinationId) {
+					this.expandedDestinationId = null
+				}
+				this.updateDataFromStore()
+			})
+		},
+		openDeleteDestinationDialog (destinationId) {
+			this.$q.dialog({
+				component: CscRemoveDialog,
+				parent: this,
+				title: this.$t('faxSettings.deleteDestinationTitle'),
+				message: this.$t('faxSettings.deleteDestinationText', { destination: destinationId })
+			}).onOk(() => {
+				this.deleteDestination(destinationId)
+			})
+		},
+		updateDestinationItemProperty (destinationId, data) {
+			const destinationItems = _.cloneDeep(this.faxToMailSettings.destinations)
+			const destinationItemIndex = destinationItems.findIndex(d => d.destination === destinationId)
+			if (destinationItemIndex >= 0) {
+				destinationItems[destinationItemIndex][data.name] = data.value
+			}
+
+			this.updateDestinations(destinationItems)
+		}
+	}
+}
+</script>
diff --git a/src/components/pages/FaxSettings/CscMailToFaxACL.vue b/src/components/pages/FaxSettings/CscMailToFaxACL.vue
new file mode 100644
index 00000000..f56097a0
--- /dev/null
+++ b/src/components/pages/FaxSettings/CscMailToFaxACL.vue
@@ -0,0 +1,109 @@
+<template>
+	<div>
+		<div class="q-item">
+			<div
+				class="csc-list-item-head row items-center"
+				@click="toggle"
+			>
+				<div
+					class="q-item__section column q-item__section--side justify-center"
+				>
+					<q-icon
+						name="fas fa-shield-alt"
+						size="24px"
+						:color="expanded ? 'primary' : ''"
+					/>
+				</div>
+				<div
+					class="q-item__section column q-item__section--main justify-center"
+					:class="expanded ? 'text-primary' : ''"
+				>
+					<div class="q-item__label text-caption">
+						<u>{{ acl.from_email }}</u> and
+						<u>{{ acl.received_from }} </u> <sup v-if="acl.use_regex">(.*) </sup> =>
+						<u>{{ acl.destination }} </u> <sup v-if="acl.use_regex">(.*) </sup>
+					</div>
+				</div>
+				<div
+					class="q-item__section column q-item__section--side justify-center"
+				>
+					<q-btn
+						flat
+						dense
+						icon="delete"
+						text-color="negative"
+						:title="$t('Remove')"
+						:disable="isChanged"
+						@click.stop="remove"
+					/>
+				</div>
+			</div>
+			<q-slide-transition>
+				<div
+					v-if="expanded"
+					class="csc-list-item-body"
+				>
+					<csc-mail-to-fax-a-c-l-form
+						:is-add-new-mode="false"
+						:initial-data="acl"
+						:loading="loading"
+						@update-property="updateProperty"
+					/>
+				</div>
+			</q-slide-transition>
+		</div>
+	</div>
+</template>
+
+<script>
+import CscMailToFaxACLForm from 'components/pages/FaxSettings/CscMailToFaxACLForm'
+
+export default {
+	name: 'CscMailToFaxACL',
+	components: {
+		CscMailToFaxACLForm
+	},
+	props: {
+		acl: {
+			type: Object,
+			required: true
+		},
+		expanded: {
+			type: Boolean,
+			default: false
+		},
+		loading: {
+			type: Boolean,
+			default: false
+		}
+	},
+	computed: {
+		isChanged () {
+			return false
+		}
+	},
+	methods: {
+		toggle () {
+			if (this.expanded) {
+				this.$emit('collapse')
+			} else {
+				this.$emit('expand')
+			}
+		},
+		updateProperty () {
+			this.$emit('update-property', ...arguments)
+		},
+		remove () {
+			this.$q.dialog({
+				title: this.$t('Remove ACL'),
+				message: this.$t('You are about to remove ACL: From email <{from_email}>', { from_email: this.acl.from_email }),
+				color: 'primary',
+				cancel: true,
+				persistent: true
+			}).onOk(() => {
+				this.$emit('remove', this.key)
+			})
+		}
+	}
+}
+</script>
diff --git a/src/components/pages/FaxSettings/CscMailToFaxACLForm.vue b/src/components/pages/FaxSettings/CscMailToFaxACLForm.vue
new file mode 100644
index 00000000..ab84293b
--- /dev/null
+++ b/src/components/pages/FaxSettings/CscMailToFaxACLForm.vue
@@ -0,0 +1,177 @@
+<template>
+	<div>
+		<div class="row">
+			<div
+				class="col"
+			>
+				<csc-input-saveable
+					v-model="data.from_email"
+					icon="email"
+					:label="$t('From email')"
+					:disable="disabled"
+					:readonly="loading"
+					:error="$v.data.from_email.$error"
+					:error-message="fromEmailErrorMessage"
+					:value-changed="!isAddNewMode && data.from_email !== initialData.from_email"
+					@input="$v.data.from_email.$touch"
+					@keypress.space.prevent
+					@keydown.space.prevent
+					@keyup.space.prevent
+					@undo="data.from_email = initialData.from_email"
+					@save="updatePropertyData('from_email')"
+				>
+					<csc-tooltip>
+						{{ $t('Accepted email address to allow mail2fax transmission.') }}
+					</csc-tooltip>
+				</csc-input-saveable>
+				<csc-input-saveable
+					v-model="data.received_from"
+					:label="$t('Received from IP')"
+					:disable="disabled"
+					:readonly="loading"
+					:value-changed="!isAddNewMode && data.received_from !== initialData.received_from"
+					@keypress.space.prevent
+					@keydown.space.prevent
+					@keyup.space.prevent
+					@undo="data.received_from = initialData.received_from"
+					@save="updatePropertyData('received_from')"
+				>
+					<csc-tooltip>
+						{{ $t('Allow mail2fax emails only to this IP (the IP or hostname is present in the &quot;Received&quot; header).') }}
+					</csc-tooltip>
+				</csc-input-saveable>
+				<csc-input-saveable
+					v-model="data.destination"
+					:label="$t('Destination')"
+					:disable="disabled"
+					:readonly="loading"
+					:value-changed="!isAddNewMode && data.destination !== initialData.destination"
+					@keypress.space.prevent
+					@keydown.space.prevent
+					@keyup.space.prevent
+					@undo="data.destination = initialData.destination"
+					@save="updatePropertyData('destination')"
+				>
+					<csc-tooltip>
+						{{ $t('Allow mail2fax destination only to this number.') }}
+					</csc-tooltip>
+				</csc-input-saveable>
+
+				<q-toggle
+					v-model="data.use_regex"
+					:label="$t('Use RegExp')"
+					:hint="$t('Enable regex matching for &quot;Received from IP&quot; and &quot;Destination&quot; fields.')"
+					:disable="loading"
+					@input="updatePropertyData('use_regex')"
+				>
+					<csc-tooltip>
+						{{ $t('Enable regex matching for &quot;Received from IP&quot; and &quot;Destination&quot; fields.') }}
+					</csc-tooltip>
+				</q-toggle>
+			</div>
+		</div>
+		<div
+			v-if="isAddNewMode"
+			class="row justify-center"
+		>
+			<q-btn
+				flat
+				color="default"
+				icon="clear"
+				:disable="loading"
+				:label="$t('buttons.cancel')"
+				@click="cancel()"
+			/>
+			<q-btn
+				flat
+				color="primary"
+				icon="person"
+				:loading="loading"
+				:disable="$v.data.$invalid || loading"
+				label="Create ACL"
+				@click="save()"
+			/>
+		</div>
+	</div>
+</template>
+
+<script>
+import { email } from 'vuelidate/lib/validators'
+import CscInputSaveable from 'components/form/CscInputSaveable'
+import CscTooltip from 'components/CscTooltip'
+
+export default {
+	name: 'CscMailToFaxACLForm',
+	components: {
+		CscTooltip,
+		CscInputSaveable
+	},
+	props: {
+		loading: {
+			type: Boolean,
+			default: false
+		},
+		disabled: {
+			type: Boolean,
+			default: false
+		},
+		initialData: {
+			type: Object,
+			default: () => ({
+				destination: '',
+				from_email: '',
+				received_from: '',
+				use_regex: false
+			})
+		},
+		isAddNewMode: {
+			type: Boolean,
+			default: false
+		}
+	},
+	data () {
+		return {
+			data: this.getDefaults()
+		}
+	},
+	validations: {
+		data: {
+			from_email: {
+				email
+			}
+		}
+	},
+	computed: {
+		fromEmailErrorMessage () {
+			if (!this.$v.data.from_email.email) {
+				return this.$t('validationErrors.email')
+			} else {
+				return ''
+			}
+		}
+	},
+	methods: {
+		getDefaults () {
+			return { ...this.initialData }
+		},
+		cancel () {
+			this.$emit('cancel')
+		},
+		save () {
+			this.$emit('save', {
+				...this.data
+			})
+		},
+		reset () {
+			this.data = this.getDefaults()
+			this.$v.$reset()
+		},
+		updatePropertyData (propertyName) {
+			this.$emit('update-property', {
+				name: propertyName,
+				value: this.data[propertyName]
+			})
+		}
+	}
+}
+</script>
diff --git a/src/components/pages/FaxSettings/CscMailToFaxRenewNotifyEmail.vue b/src/components/pages/FaxSettings/CscMailToFaxRenewNotifyEmail.vue
new file mode 100644
index 00000000..19a8936c
--- /dev/null
+++ b/src/components/pages/FaxSettings/CscMailToFaxRenewNotifyEmail.vue
@@ -0,0 +1,152 @@
+<template>
+	<q-item>
+		<q-item-section
+			v-if="!editing"
+			side
+			@click="activateEditing"
+		>
+			<q-icon name="email" />
+		</q-item-section>
+		<q-item-section
+			@click="activateEditing"
+		>
+			<q-item-label
+				v-if="!editing"
+			>
+				{{ value }}
+			</q-item-label>
+			<csc-input-saveable
+				v-else
+				ref="emailInput"
+				v-model="newEmail"
+				:label="$t('Renew Notify Email')"
+				:value-changed="isChanged"
+				:error="$v.newEmail.$error"
+				:error-message="newEmailErrorMessage"
+				dense
+				@keypress.space.prevent
+				@keydown.space.prevent
+				@keyup.space.prevent
+				@input="$v.newEmail.$touch"
+				@save="save"
+				@undo="undo"
+				@focusout="focusOutEditing"
+				@focusin="cancelTimer"
+			/>
+		</q-item-section>
+		<q-item-section
+			side
+		>
+			<q-btn
+				flat
+				dense
+				icon="delete"
+				text-color="negative"
+				:title="$t('Remove')"
+				:disable="isChanged"
+				@click="remove"
+			/>
+		</q-item-section>
+	</q-item>
+</template>
+
+<script>
+import CscInputSaveable from 'components/form/CscInputSaveable'
+import { email, required } from 'vuelidate/lib/validators'
+export default {
+	name: 'CscMailToFaxRenewNotifyEmail',
+	components: {
+		CscInputSaveable
+	},
+	props: {
+		value: {
+			type: String,
+			required: true
+		}
+	},
+	data () {
+		return {
+			newEmail: this.value,
+			editing: false,
+			timerHandler: undefined
+		}
+	},
+	validations: {
+		newEmail: {
+			required,
+			email
+		}
+	},
+	computed: {
+		isChanged () {
+			return this.newEmail !== this.value
+		},
+		newEmailErrorMessage () {
+			if (!this.$v.newEmail.required) {
+				return this.$t('validationErrors.fieldRequired', {
+					field: this.$t('Renew Notify Email')
+				})
+			} else if (!this.$v.newEmail.email) {
+				return this.$t('validationErrors.email')
+			} else {
+				return ''
+			}
+		}
+	},
+	beforeDestroy () {
+		this.cancelTimer()
+	},
+	methods: {
+		activateEditing () {
+			if (!this.editing) {
+				this.newEmail = this.value
+				this.editing = true
+				this.focusEmailInput()
+			}
+		},
+		deactivateEditing () {
+			this.timerHandler = setTimeout(() => {
+				this.editing = false
+			}, 1000)
+		},
+		cancelTimer () {
+			clearTimeout(this.timerHandler)
+		},
+		focusOutEditing () {
+			if (!this.isChanged) {
+				this.deactivateEditing()
+			}
+		},
+		focusEmailInput () {
+			this.$nextTick(() => {
+				const emailInput = this.$refs.emailInput?.$el
+				if (emailInput) {
+					emailInput.focus()
+				}
+			})
+		},
+		undo () {
+			this.newEmail = this.value
+			this.$v.$reset()
+			this.focusEmailInput()
+		},
+		save () {
+			this.$emit('save', {
+				id: this.key,
+				value: this.newEmail
+			})
+		},
+		remove () {
+			this.$q.dialog({
+				title: this.$t('faxSettings.deleteRenewNotifyEmailTitle'),
+				message: this.$t('faxSettings.deleteRenewNotifyEmailText', { email: this.value }),
+				color: 'primary',
+				cancel: true,
+				persistent: true
+			}).onOk(() => {
+				this.$emit('remove', this.key)
+			})
+		}
+	}
+}
+</script>
diff --git a/src/components/pages/FaxSettings/CscMailToFaxRenewNotifyEmailForm.vue b/src/components/pages/FaxSettings/CscMailToFaxRenewNotifyEmailForm.vue
new file mode 100644
index 00000000..ad0be762
--- /dev/null
+++ b/src/components/pages/FaxSettings/CscMailToFaxRenewNotifyEmailForm.vue
@@ -0,0 +1,129 @@
+<template>
+	<div class="csc-form">
+		<csc-input-saveable
+			v-model="data.destination"
+			icon="email"
+			:label="$t('Renew Notify Email')"
+			:disable="disabled"
+			:readonly="loading"
+			:error="$v.data.destination.$error"
+			:error-message="destinationErrorMessage"
+			:value-changed="!isAddNewMode && data.destination !== initialData.destination"
+			@input="$v.data.destination.$touch"
+			@keypress.space.prevent
+			@keydown.space.prevent
+			@keyup.space.prevent
+			@undo="data.destination = initialData.destination"
+			@save="updatePropertyData('destination')"
+		>
+			<csc-tooltip>
+				{{ $t('Destination email to send the secret key renew notification to.') }}
+			</csc-tooltip>
+		</csc-input-saveable>
+		<div
+			v-if="isAddNewMode"
+			class="csc-form-actions row justify-center"
+		>
+			<q-btn
+				flat
+				color="default"
+				icon="clear"
+				:disable="loading"
+				:label="$t('Cancel')"
+				@click="cancel()"
+			/>
+			<q-btn
+				flat
+				color="primary"
+				icon="done"
+				:loading="loading"
+				:disable="$v.data.$invalid || loading"
+				:label="$t('Add email')"
+				@click="save()"
+			/>
+		</div>
+	</div>
+</template>
+
+<script>
+import { email, required } from 'vuelidate/lib/validators'
+import CscInputSaveable from 'components/form/CscInputSaveable'
+import CscTooltip from 'components/CscTooltip'
+
+export default {
+	name: 'CscMailToFaxRenewNotifyEmailForm',
+	components: {
+		CscTooltip,
+		CscInputSaveable
+	},
+	props: {
+		loading: {
+			type: Boolean,
+			default: false
+		},
+		disabled: {
+			type: Boolean,
+			default: false
+		},
+		initialData: {
+			type: Object,
+			default: () => ({
+				destination: ''
+			})
+		},
+		isAddNewMode: {
+			type: Boolean,
+			default: false
+		}
+	},
+	data () {
+		return {
+			data: this.getDefaults()
+		}
+	},
+	validations: {
+		data: {
+			destination: {
+				required,
+				email
+			}
+		}
+	},
+	computed: {
+		destinationErrorMessage () {
+			if (!this.$v.data.destination.required) {
+				return this.$t('validationErrors.fieldRequired', {
+					field: this.$t('Email')
+				})
+			} else if (!this.$v.data.destination.email) {
+				return this.$t('validationErrors.email')
+			} else {
+				return ''
+			}
+		}
+	},
+	methods: {
+		getDefaults () {
+			return { ...this.initialData }
+		},
+		cancel () {
+			this.$emit('cancel')
+		},
+		save () {
+			this.$emit('save', {
+				...this.data
+			})
+		},
+		reset () {
+			this.data = this.getDefaults()
+			this.$v.$reset()
+		},
+		updatePropertyData (propertyName) {
+			this.$emit('update-property', {
+				name: propertyName,
+				value: this.data[propertyName]
+			})
+		}
+	}
+}
+</script>
diff --git a/src/components/pages/FaxSettings/CscMailToFaxSettings.vue b/src/components/pages/FaxSettings/CscMailToFaxSettings.vue
new file mode 100644
index 00000000..a98cdc65
--- /dev/null
+++ b/src/components/pages/FaxSettings/CscMailToFaxSettings.vue
@@ -0,0 +1,373 @@
+<template>
+	<div
+		v-if="!mailToFaxSettingsModel.active"
+		class="q-pa-md"
+	>
+		<csc-spinner
+			v-if="loadingMail2FaxSettings"
+			class="self-center"
+		/>
+		<div v-else>
+			{{ $t('faxSettings.featureIsNotActive') }}
+		</div>
+	</div>
+	<div v-else>
+		<q-list
+			class="col col-xs-12 col-md-6"
+			dense
+		>
+			<q-item>
+				<q-item-section>
+					<q-toggle
+						:value="mailToFaxSettingsModel.active"
+						:label="$t('faxSettings.active')"
+						:disable="true"
+					/>
+				</q-item-section>
+				<q-item-section
+					side
+				>
+					<csc-spinner
+						v-if="loadingMail2FaxSettings"
+						class="self-center"
+					/>
+				</q-item-section>
+			</q-item>
+			<q-item>
+				<q-item-section>
+					<csc-input-saveable
+						v-model.trim="mailToFaxSettingsModel.secret_key"
+						:label="secretKeyFieldLabel"
+						:disable="!dataLoaded"
+						:loading="loadingMail2FaxSettings"
+						:value-changed="mailToFaxSettingsModel.secret_key !== mailToFaxSettings.secret_key"
+						@save="setChangedData('secret_key', mailToFaxSettingsModel.secret_key)"
+						@undo="mailToFaxSettingsModel.secret_key = mailToFaxSettings.secret_key"
+					>
+						<csc-tooltip>
+							{{ $t('Enable strict mode that requires all mail2fax emails to have the secret key as the very first line of the email + an empty line. The key is removed from the email once matched.') }}
+						</csc-tooltip>
+					</csc-input-saveable>
+				</q-item-section>
+			</q-item>
+			<q-item>
+				<q-item-section>
+					<q-select
+						v-model="mailToFaxSettingsModel.secret_key_renew"
+						emit-value
+						map-options
+						:disable="!dataLoaded"
+						:readonly="!dataLoaded"
+						:label="$t('faxSettings.secretKeyRenew')"
+						:options="secretKeyRenewOptions"
+						@input="setChangedData('secret_key_renew', mailToFaxSettingsModel.secret_key_renew)"
+					>
+						<csc-tooltip>
+							{{ $t('Interval when the secret key is automatically renewed.') }}
+						</csc-tooltip>
+					</q-select>
+				</q-item-section>
+				<q-item-section
+					side
+				>
+					<csc-spinner
+						v-if="loadingMail2FaxSettings"
+						class="self-center"
+					/>
+				</q-item-section>
+			</q-item>
+		</q-list>
+		<div class="row">
+			<div class="col q-py-md q-pl-md">
+				<div class="row q-pb-xs">
+					<div class="col vertical-bottom">
+						<span class="vertical-middle">{{ $t('faxSettings.secretKeyRenewNotify') }}:</span>
+					</div>
+					<div class="col text-right">
+						<q-btn
+							flat
+							color="primary"
+							icon="add"
+							:disable="!dataLoaded || showAddNewRenewEmail"
+							@click="openAddNewRenewEmail"
+						>
+							{{ $t('faxSettings.addEmail') }}
+						</q-btn>
+					</div>
+				</div>
+				<q-separator />
+				<div class="col relative-position">
+					<div
+						v-if="showAddNewRenewEmail"
+						class="row justify-center q-pa-md"
+					>
+						<csc-mail-to-fax-renew-notify-email-form
+							v-if="showAddNewRenewEmail"
+							ref="addNewRenewEmailForm"
+							class="col"
+							:loading="!dataLoaded"
+							:is-add-new-mode="true"
+							@save="addNewRenewEmail"
+							@cancel="closeAddNewRenewEmail"
+						/>
+					</div>
+					<div
+						v-if="!showAddNewRenewEmail && (!mailToFaxSettingsModel.secret_renew_notify || !mailToFaxSettingsModel.secret_renew_notify.length)"
+						class="row q-pa-md justify-center"
+					>
+						{{ $t('There are no Key Renew Notify Emails yet') }}
+					</div>
+					<div
+						v-else
+						class="row q-pa-xs"
+					>
+						<q-list class="col striped-list">
+							<csc-mail-to-fax-renew-notify-email
+								v-for="renewEmail in mailToFaxSettingsModel.secret_renew_notify"
+								:key="renewEmail.destination"
+								:value="renewEmail.destination"
+								@save="updateRenewEmailItem(renewEmail.destination, ...arguments)"
+								@remove="deleteRenewEmailItem(renewEmail.destination)"
+							/>
+						</q-list>
+					</div>
+
+					<q-inner-loading :showing="!dataLoaded">
+						<q-spinner-dots
+							size="50px"
+							color="primary"
+						/>
+					</q-inner-loading>
+				</div>
+			</div>
+			<div class="col q-pa-md">
+				<div class="row q-pb-xs">
+					<div class="col">
+						{{ $t('faxSettings.ACL') }}:
+					</div>
+					<div class="col text-right">
+						<q-btn
+							flat
+							color="primary"
+							icon="add"
+							:disable="!dataLoaded || showAddNewACL"
+							@click="openAddNewACL"
+						>
+							{{ $t('faxSettings.addACL') }}
+						</q-btn>
+					</div>
+				</div>
+				<q-separator />
+				<div class="col relative-position">
+					<div
+						v-if="showAddNewACL"
+						class="row justify-center q-pa-md"
+					>
+						<csc-mail-to-fax-a-c-l-form
+							v-if="showAddNewACL"
+							ref="addNewACLForm"
+							class="col"
+							:loading="!dataLoaded"
+							:is-add-new-mode="true"
+							@save="addNewACL"
+							@cancel="closeAddNewACL"
+						/>
+					</div>
+					<div
+						v-if="!showAddNewACL && (!mailToFaxSettingsModel.acl || !mailToFaxSettingsModel.acl.length)"
+						class="row q-pa-md justify-center"
+					>
+						{{ $t('There are no ACLs yet') }}
+					</div>
+					<div
+						v-else
+						class="row q-pa-xs"
+					>
+						<q-list class="col striped-list">
+							<csc-mail-to-fax-a-c-l
+								v-for="(acl, index) in mailToFaxSettingsModel.acl"
+								:key="index"
+								:acl="acl"
+								:expanded="index === expandedACLId"
+								@expand="expandedACLId = index"
+								@collapse="expandedACLId = null"
+								@update-property="updateACL(index, ...arguments)"
+								@remove="deleteACL(index)"
+							/>
+						</q-list>
+					</div>
+
+					<q-inner-loading :showing="!dataLoaded">
+						<q-spinner-dots
+							size="50px"
+							color="primary"
+						/>
+					</q-inner-loading>
+				</div>
+			</div>
+		</div>
+	</div>
+</template>
+
+<script>
+import _ from 'lodash'
+import { mapState } from 'vuex'
+import { mapWaitingActions, mapWaitingGetters } from 'vue-wait'
+import { showGlobalError } from 'src/helpers/ui'
+import CscSpinner from 'components/CscSpinner'
+import CscInputSaveable from 'components/form/CscInputSaveable'
+import CscMailToFaxRenewNotifyEmail from 'components/pages/FaxSettings/CscMailToFaxRenewNotifyEmail'
+import CscMailToFaxACL from 'components/pages/FaxSettings/CscMailToFaxACL'
+import CscMailToFaxRenewNotifyEmailForm from 'components/pages/FaxSettings/CscMailToFaxRenewNotifyEmailForm'
+import CscMailToFaxACLForm from 'components/pages/FaxSettings/CscMailToFaxACLForm'
+import CscTooltip from 'components/CscTooltip'
+
+export default {
+	name: 'CscMailToFaxSettings',
+	components: {
+		CscTooltip,
+		CscMailToFaxACLForm,
+		CscMailToFaxRenewNotifyEmailForm,
+		CscMailToFaxACL,
+		CscMailToFaxRenewNotifyEmail,
+		CscInputSaveable,
+		CscSpinner
+	},
+	data () {
+		return {
+			mailToFaxSettingsModel: {},
+			showAddNewRenewEmail: false,
+			showAddNewACL: false,
+			expandedACLId: null
+		}
+	},
+	computed: {
+		...mapState('fax', [
+			'mailToFaxSettings',
+			'mailToFaxSettingsInitialized'
+		]),
+		...mapWaitingGetters({
+			loadingMail2FaxSettings: 'loading mail2faxSettings'
+		}),
+		dataLoaded () {
+			return this.mailToFaxSettingsInitialized && !this.loadingMail2FaxSettings
+		},
+		secretKeyFieldLabel () {
+			let label = this.$t('faxSettings.secretKeyField')
+			label += ' (' + this.$t('faxSettings.lastModifyTime') + ': '
+			if (this.mailToFaxSettings.last_secret_key_modify) {
+				label += this.mailToFaxSettings.last_secret_key_modify + ')'
+			} else {
+				label += this.$t('faxSettings.notModifiedYet') + ')'
+			}
+			return label
+		},
+		secretKeyRenewOptions () {
+			return [
+				{ value: 'never', label: this.$t('Never') },
+				{ value: 'daily', label: this.$t('Daily') },
+				{ value: 'weekly', label: this.$t('Weekly') },
+				{ value: 'monthly', label: this.$t('Monthly') }
+			]
+		}
+	},
+	mounted () {
+		this.loadMailToFaxSettings()
+	},
+	methods: {
+		...mapWaitingActions('fax', {
+			loadMailToFaxSettingsAction: 'loading mail2faxSettings',
+			mailToFaxSettingsUpdateAction: 'loading mail2faxSettings'
+		}),
+		async loadMailToFaxSettings () {
+			try {
+				await this.loadMailToFaxSettingsAction()
+				this.updateDataFromStore()
+			} catch (err) {
+				if (String(err.code) === '403') {
+					this.mailToFaxSettingsModel = {
+						active: false
+					}
+				} else {
+					showGlobalError(err?.message || this.$t('Unknown error'))
+				}
+			}
+		},
+		updateDataFromStore () {
+			this.mailToFaxSettingsModel = {
+				active: true,
+				..._.cloneDeep(this.mailToFaxSettings)
+			}
+		},
+		async setChangedData (field, value, beforeUpdateUI = () => {}) {
+			try {
+				await this.mailToFaxSettingsUpdateAction({ field, value })
+				beforeUpdateUI()
+				this.updateDataFromStore()
+			} catch (err) {
+				showGlobalError(err?.message || this.$t('Unknown error'))
+			}
+		},
+		openAddNewRenewEmail () {
+			this.showAddNewRenewEmail = true
+		},
+		closeAddNewRenewEmail () {
+			this.showAddNewRenewEmail = false
+			this.$refs.addNewRenewEmailForm.reset()
+		},
+		addNewRenewEmail (newItemData) {
+			const renewEmailItems = [...this.mailToFaxSettingsModel.secret_renew_notify, newItemData]
+
+			this.setChangedData('secret_renew_notify', renewEmailItems, () => {
+				this.closeAddNewRenewEmail()
+			})
+		},
+		updateRenewEmailItem (itemId, data) {
+			const renewEmailItems = _.cloneDeep(this.mailToFaxSettingsModel.secret_renew_notify)
+			const renewEmailItemIndex = renewEmailItems.findIndex(d => d.destination === itemId)
+			if (renewEmailItemIndex >= 0) {
+				renewEmailItems[renewEmailItemIndex].destination = data.value
+			}
+
+			this.setChangedData('secret_renew_notify', renewEmailItems)
+		},
+		deleteRenewEmailItem (itemId) {
+			const renewEmailItems = this.mailToFaxSettingsModel.secret_renew_notify.filter(d => d.destination !== itemId)
+			this.setChangedData('secret_renew_notify', renewEmailItems)
+		},
+
+		openAddNewACL () {
+			this.showAddNewACL = true
+		},
+		closeAddNewACL () {
+			this.showAddNewACL = false
+			this.$refs.addNewACLForm.reset()
+		},
+		addNewACL (newItemData) {
+			const ACLItems = [...this.mailToFaxSettingsModel.acl, newItemData]
+
+			this.setChangedData('acl', ACLItems, () => {
+				this.closeAddNewACL()
+			})
+		},
+		updateACL (itemId, { name, value }) {
+			const ACLItems = _.cloneDeep(this.mailToFaxSettingsModel.acl)
+			if (itemId >= 0) {
+				ACLItems[itemId][name] = value
+			}
+
+			this.setChangedData('acl', ACLItems)
+		},
+		deleteACL (itemId) {
+			const ACLItems = this.mailToFaxSettingsModel.acl.filter((acl, index) => index !== itemId)
+			this.setChangedData('acl', ACLItems, () => {
+				this.expandedACLId = null
+			})
+		}
+	}
+}
+</script>
+
+<style lang="stylus" rel="stylesheet/stylus" scoped>
+
+</style>
diff --git a/src/css/app.styl b/src/css/app.styl
index c5823806..f216e03d 100644
--- a/src/css/app.styl
+++ b/src/css/app.styl
@@ -27,3 +27,9 @@ body.body--dark
 
 .csc-opt-center
 	margin-top ($header-height * -2)
+
+.striped-list
+	> :nth-of-type(2n+1)
+		background-color $item-stripe-color
+	> :nth-of-type(2n)
+		background-color alpha($main-menu-background, 0.2)
diff --git a/src/i18n/en.json b/src/i18n/en.json
index 6643c61c..a8e424f2 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -764,7 +764,20 @@
 		"deleteDestinationTitle": "Remove Destination",
 		"deleteDestinationText": "You are about to remove destination {destination}",
 		"destinationEmailExists": "The Destination Email is already used",
-		"destinationItemTitle": "<{destination}> as {filetype}"
+		"destinationItemTitle": "<{destination}> as {filetype}",
+		"notifyEmailExists": "The Notify Email is already used",
+		"featureIsNotActive": "Mail To Fax feature is not active",
+		"destinations": "Destinations",
+		"secretKeyField": "Secret Key (empty=disabled)",
+		"lastModifyTime": "Last Modify Time",
+		"notModifiedYet": "Not modified yet",
+		"secretKeyRenew": "Secret Key Renew",
+		"secretKeyRenewNotify": "Secret Key Renew Notify",
+		"addEmail": "Add email",
+		"deleteRenewNotifyEmailTitle": "Remove secret key renew notify email",
+		"deleteRenewNotifyEmailText": "You are about to remove secret key renew notify email: {email}",
+		"ACL": "ACL",
+		"addACL": "Add ACL"
 	},
 	"callSettings": {
 		"musicOnHold": "Music on Hold",
diff --git a/src/layouts/CscLayoutMain.vue b/src/layouts/CscLayoutMain.vue
index c419b2e1..6eeca715 100644
--- a/src/layouts/CscLayoutMain.vue
+++ b/src/layouts/CscLayoutMain.vue
@@ -21,7 +21,7 @@
 					@click="$refs.mainMenu.show()"
 				/>
 				<q-btn
-					v-if="hasFaxCapability && hasSendFaxFeature"
+					v-if="hasFaxCapabilityAndFaxActive && hasSendFaxFeature"
 					class="q-mr-sm"
 					flat
 					dense
@@ -297,7 +297,7 @@ export default {
 			'getUsername',
 			'isPbxAdmin',
 			'hasSmsCapability',
-			'hasFaxCapability',
+			'hasFaxCapabilityAndFaxActive',
 			'hasSendSmsFeature',
 			'hasSendFaxFeature',
 			'userDataRequesting',
@@ -315,7 +315,7 @@ export default {
 		]),
 		hasCommunicationCapabilities () {
 			return (this.hasSmsCapability && this.hasSendSmsFeature) ||
-				(this.hasFaxCapability && this.hasSendFaxFeature)
+				(this.hasFaxCapabilityAndFaxActive && this.hasSendFaxFeature)
 		},
 		isMenuClosed () {
 			return !this.sideStates.left
diff --git a/src/pages/CscPageFaxSettings.vue b/src/pages/CscPageFaxSettings.vue
index 5220e3fc..bc3a8bab 100644
--- a/src/pages/CscPageFaxSettings.vue
+++ b/src/pages/CscPageFaxSettings.vue
@@ -1,275 +1,46 @@
 <template>
-	<csc-page
-		class="q-pa-lg"
+	<csc-page-sticky-tabs
+		v-model="selectedTab"
 	>
-		<q-list
-			class="col col-xs-12 col-md-6"
-			dense
+		<template
+			v-slot:tabs
 		>
-			<q-item>
-				<q-item-section>
-					<q-toggle
-						v-model="faxToMailSettings.active"
-						:label="$t('faxSettings.active')"
-						:disable="!dataLoaded"
-						@input="setChangedData('active', !faxServerSettings.active)"
-					/>
-				</q-item-section>
-				<q-item-section
-					side
-				>
-					<csc-spinner
-						v-if="loadingFaxServerSettings"
-						class="self-center"
-					/>
-				</q-item-section>
-			</q-item>
-			<q-item>
-				<q-item-section>
-					<csc-input-saveable
-						v-model.trim="faxToMailSettings.name"
-						:label="$t('faxSettings.sendfaxHeaderName')"
-						:disable="!dataLoaded"
-						:loading="loadingFaxServerSettings"
-						:value-changed="nameChanged"
-						@save="setChangedData('name', faxToMailSettings.name)"
-						@undo="restoreName"
-					/>
-				</q-item-section>
-			</q-item>
-			<q-item>
-				<q-item-section>
-					<q-toggle
-						v-model="faxToMailSettings.t38"
-						:label="$t('faxSettings.T38')"
-						:disable="!dataLoaded"
-						@input="setChangedData('t38', !faxServerSettings.t38)"
-					/>
-				</q-item-section>
-				<q-item-section
-					side
-				>
-					<csc-spinner
-						v-if="loadingFaxServerSettings"
-						class="self-center"
-					/>
-				</q-item-section>
-			</q-item>
-			<q-item>
-				<q-item-section>
-					<q-toggle
-						v-model="faxToMailSettings.ecm"
-						:label="$t('faxSettings.ECM')"
-						:disable="!dataLoaded"
-						@input="setChangedData('ecm', !faxServerSettings.ecm)"
-					/>
-				</q-item-section>
-				<q-item-section
-					side
-				>
-					<csc-spinner
-						v-if="loadingFaxServerSettings"
-						class="self-center"
-					/>
-				</q-item-section>
-			</q-item>
-			<q-item class="row">
-				<div class="col">
-					<span class="text-h6">Destinations:</span>
-				</div>
-				<div class="col text-center">
-					<csc-spinner
-						v-if="loadingFaxServerSettings"
-					/>
-				</div>
-				<div class="col text-right">
-					<q-btn
-						flat
-						color="primary"
-						icon="add"
-						:disable="!dataLoaded || showAddNewDestination"
-						@click="openAddNewDestination"
-					>
-						{{ $t('faxSettings.addDestination') }}
-					</q-btn>
-				</div>
-			</q-item>
-		</q-list>
-		<q-separator />
-		<div
-			class="row justify-center q-mb-lg"
-		>
-			<q-list
-				class="col-xs-12"
-			>
-				<q-item
-					v-if="showAddNewDestination"
-					class="row justify-center"
-				>
-					<csc-fax2-mail-destination-form
-						v-if="showAddNewDestination"
-						ref="addNewDestination"
-						:loading="loadingFaxServerSettings"
-						:is-add-new-mode="true"
-						@save="addNewDestination"
-						@cancel="closeAddNewDestination"
-					/>
-				</q-item>
-				<q-item
-					v-if="!hasDestinations"
-					class="row justify-center"
-				>
-					{{ $t('faxSettings.noDestinationsCreatedYet') }}
-				</q-item>
-				<csc-fax2-mail-destination
-					v-for="(destinationItem, index) in faxToMailSettings.destinations"
-					:key="destinationItem.destination"
-					:odd="(index % 2) === 0"
-					:expanded="expandedDestinationId === destinationItem.destination"
-					:destination="destinationItem"
-					:loading="loadingFaxServerSettings"
-					@collapse="expandedDestinationId = null"
-					@expand="expandedDestinationId = destinationItem.destination"
-					@remove="openDeleteDestinationDialog(destinationItem.destination)"
-					@update-property="updateDestinationItemProperty(destinationItem.destination, ...arguments)"
-				/>
-			</q-list>
-		</div>
-	</csc-page>
+			<q-tab
+				name="fax2mail"
+				icon="perm_phone_msg"
+				:label="$t('Fax to Mail and Sendfax')"
+			/>
+			<q-tab
+				name="mail2fax"
+				icon="forward_to_inbox"
+				:label="$t('Mail to Fax')"
+			/>
+		</template>
+
+		<csc-fax-to-mail-settings
+			v-if="selectedTab === 'fax2mail'"
+		/>
+		<csc-mail-to-fax-settings
+			v-if="selectedTab === 'mail2fax'"
+		/>
+	</csc-page-sticky-tabs>
 </template>
 
 <script>
-import _ from 'lodash'
-import { mapState } from 'vuex'
-import CscInputSaveable from 'components/form/CscInputSaveable'
-import CscPage from 'components/CscPage'
-import CscSpinner from 'components/CscSpinner'
-import { mapWaitingActions, mapWaitingGetters } from 'vue-wait'
-import CscFax2MailDestinationForm from 'components/pages/FaxSettings/CscFax2MailDestinationForm'
-import CscFax2MailDestination from 'components/pages/FaxSettings/CscFax2MailDestination'
-import CscRemoveDialog from 'components/CscRemoveDialog'
-import { showGlobalError } from 'src/helpers/ui'
+import CscPageStickyTabs from 'components/CscPageStickyTabs'
+import CscFaxToMailSettings from 'components/pages/FaxSettings/CscFaxToMailSettings'
+import CscMailToFaxSettings from 'components/pages/FaxSettings/CscMailToFaxSettings'
+
 export default {
 	name: 'CscPageFaxSettings',
 	components: {
-		CscFax2MailDestination,
-		CscFax2MailDestinationForm,
-		CscSpinner,
-		CscPage,
-		CscInputSaveable
+		CscPageStickyTabs,
+		CscMailToFaxSettings,
+		CscFaxToMailSettings
 	},
 	data () {
 		return {
-			faxToMailSettings: {},
-			showAddNewDestination: false,
-			expandedDestinationId: null
-		}
-	},
-	computed: {
-		...mapState('fax', [
-			'faxServerSettings',
-			'faxServerSettingsInitialized'
-		]),
-		...mapWaitingGetters({
-			loadingFaxServerSettings: 'loading faxServerSettings'
-		}),
-		dataLoaded () {
-			return this.faxServerSettingsInitialized && !this.loadingFaxServerSettings
-		},
-		hasDestinations () {
-			return this.faxToMailSettings?.destinations?.length
-		},
-		nameChanged () {
-			return this.faxToMailSettings.name !== this.faxServerSettings.name
-		}
-
-	},
-	mounted () {
-		this.loadFaxServerSettings()
-	},
-	methods: {
-		...mapWaitingActions('fax', {
-			loadFaxSettingsAction: 'loading faxServerSettings',
-			fieldUpdateAction: 'loading faxServerSettings'
-		}),
-		async loadFaxServerSettings () {
-			try {
-				await this.loadFaxSettingsAction()
-				this.updateDataFromStore()
-			} catch (err) {
-				showGlobalError(err?.message)
-			}
-		},
-		updateDataFromStore () {
-			this.faxToMailSettings = _.cloneDeep(this.faxServerSettings)
-		},
-		async setChangedData (field, value) {
-			try {
-				await this.fieldUpdateAction({ field, value })
-				this.updateDataFromStore()
-			} catch (err) {
-				showGlobalError(err?.message)
-			}
-		},
-		restoreName () {
-			this.faxToMailSettings.name = this.faxServerSettings.name
-		},
-		async updateDestinations (destinationItems, beforeUpdateUI = () => {}) {
-			try {
-				await this.fieldUpdateAction({
-					field: 'destinations',
-					value: destinationItems
-				})
-				beforeUpdateUI()
-				this.updateDataFromStore()
-			} catch (err) {
-				showGlobalError(err?.message)
-			}
-		},
-		openAddNewDestination () {
-			this.showAddNewDestination = true
-		},
-		closeAddNewDestination () {
-			this.showAddNewDestination = false
-			this.$refs.addNewDestination.reset()
-		},
-		addNewDestination (destination) {
-			const destinationItems = [...this.faxToMailSettings.destinations, destination]
-
-			this.updateDestinations(destinationItems, () => {
-				this.closeAddNewDestination()
-			})
-		},
-		deleteDestination (destinationId) {
-			const destinationItems = this.faxToMailSettings.destinations.filter(d => d.destination !== destinationId)
-			this.fieldUpdateAction({
-				field: 'destinations',
-				value: destinationItems
-			}).then(() => {
-				if (this.expandedDestinationId === destinationId) {
-					this.expandedDestinationId = null
-				}
-				this.updateDataFromStore()
-			})
-		},
-		openDeleteDestinationDialog (destinationId) {
-			this.$q.dialog({
-				component: CscRemoveDialog,
-				parent: this,
-				title: this.$t('faxSettings.deleteDestinationTitle'),
-				message: this.$t('faxSettings.deleteDestinationText', { destination: destinationId })
-			}).onOk(() => {
-				this.deleteDestination(destinationId)
-			})
-		},
-		updateDestinationItemProperty (destinationId, data) {
-			const destinationItems = _.cloneDeep(this.faxToMailSettings.destinations)
-			const destinationItemIndex = destinationItems.findIndex(d => d.destination === destinationId)
-			if (destinationItemIndex >= 0) {
-				destinationItems[destinationItemIndex][data.name] = data.value
-			}
-
-			this.updateDestinations(destinationItems)
+			selectedTab: 'fax2mail'
 		}
 	}
 }
diff --git a/src/plugins/rtc-engine.js b/src/plugins/rtc-engine.js
index 9579d71d..b0b93b3a 100644
--- a/src/plugins/rtc-engine.js
+++ b/src/plugins/rtc-engine.js
@@ -107,18 +107,22 @@ export class RtcEnginePlugin {
 				})
 				this.client.onConnect(() => {
 					this.events.emit('connected')
-					const conferenceNetwork = this.client.getNetworkByTag('conference')
-					conferenceNetwork.onConnect(() => {
-						this.events.emit('conference-network-connected', conferenceNetwork)
-					}).onDisconnect(() => {
-						this.events.emit('conference-network-disconnected', conferenceNetwork)
-					})
-					const sipNetwork = this.client.getNetworkByTag('sip')
-					sipNetwork.onConnect(() => {
-						this.events.emit('sip-network-connected', sipNetwork)
-					}).onDisconnect(() => {
-						this.events.emit('sip-network-disconnected', sipNetwork)
-					})
+					try {
+						const conferenceNetwork = this.client.getNetworkByTag('conference')
+						conferenceNetwork.onConnect(() => {
+							this.events.emit('conference-network-connected', conferenceNetwork)
+						}).onDisconnect(() => {
+							this.events.emit('conference-network-disconnected', conferenceNetwork)
+						})
+						const sipNetwork = this.client.getNetworkByTag('sip')
+						sipNetwork.onConnect(() => {
+							this.events.emit('sip-network-connected', sipNetwork)
+						}).onDisconnect(() => {
+							this.events.emit('sip-network-disconnected', sipNetwork)
+						})
+					} catch (e) {
+						reject(new Error('Unable to connect to a specific network by RTCEngine client'))
+					}
 					resolve()
 				})
 				this.client.onDisconnect(() => {
diff --git a/src/router/routes.js b/src/router/routes.js
index 943561c0..239f0301 100644
--- a/src/router/routes.js
+++ b/src/router/routes.js
@@ -195,6 +195,14 @@ export default function routes (app) {
 					meta: {
 						title: i18n.t('navigation.faxSettings.title'),
 						subtitle: i18n.t('navigation.faxSettings.subTitle')
+					},
+					async beforeEnter (routeTo, routeFrom, next) {
+						await app.store.dispatch('user/initUser')
+						if (app.store.getters['user/hasFaxCapability']) {
+							next()
+						} else {
+							next('/')
+						}
 					}
 				},
 				{
diff --git a/src/store/call-forward.js b/src/store/call-forward.js
index f19b8f1d..22ea193c 100644
--- a/src/store/call-forward.js
+++ b/src/store/call-forward.js
@@ -99,8 +99,8 @@ export default {
 		updateOwnPhoneTimeoutError: null
 	},
 	getters: {
-		hasFaxCapability (state, getters, rootState, rootGetters) {
-			return rootGetters['user/hasFaxCapability']
+		hasFaxCapabilityAndFaxActive (state, getters, rootState, rootGetters) {
+			return rootGetters['user/hasFaxCapabilityAndFaxActive']
 		},
 		subscriberId (state, getters, rootState, rootGetters) {
 			return rootGetters['user/getSubscriberId']
diff --git a/src/store/fax.js b/src/store/fax.js
index cce527d5..567a83e6 100644
--- a/src/store/fax.js
+++ b/src/store/fax.js
@@ -1,14 +1,19 @@
 import _ from 'lodash'
 import {
 	getFaxServerSettings,
-	setFaxServerField
+	setFaxServerField,
+	getMailToFaxSettings,
+	setMailToFaxSettingField
 } from '../api/fax'
 
 export default {
 	namespaced: true,
 	state: {
 		faxServerSettingsInitialized: false,
-		faxServerSettings: {}
+		faxServerSettings: {},
+
+		mailToFaxSettingsInitialized: false,
+		mailToFaxSettings: {}
 	},
 	getters: {
 		subscriberId (state, getters, rootState, rootGetters) {
@@ -21,6 +26,11 @@ export default {
 				state.faxServerSettings = res.faxServerSettings
 				state.faxServerSettingsInitialized = true
 			}
+
+			if (_.has(res, 'mailToFaxSettings')) {
+				state.mailToFaxSettings = res.mailToFaxSettings
+				state.mailToFaxSettingsInitialized = true
+			}
 		}
 	},
 	actions: {
@@ -30,7 +40,7 @@ export default {
 				faxServerSettings
 			})
 		},
-		async fieldUpdateAction (context, options) {
+		async faxServerSettingsUpdateAction (context, options) {
 			const faxServerSettings = await setFaxServerField({
 				subscriberId: context.getters.subscriberId,
 				field: options.field,
@@ -40,6 +50,23 @@ export default {
 				faxServerSettings
 			})
 			context.commit('user/updateFaxActiveCapabilityState', faxServerSettings.active, { root: true })
+		},
+
+		async loadMailToFaxSettingsAction (context) {
+			const mailToFaxSettings = await getMailToFaxSettings(context.getters.subscriberId)
+			context.commit('settingsSucceeded', {
+				mailToFaxSettings
+			})
+		},
+		async mailToFaxSettingsUpdateAction (context, options) {
+			const mailToFaxSettings = await setMailToFaxSettingField({
+				subscriberId: context.getters.subscriberId,
+				field: options.field,
+				value: options.value
+			})
+			context.commit('settingsSucceeded', {
+				mailToFaxSettings
+			})
 		}
 	}
 }
diff --git a/src/store/user.js b/src/store/user.js
index 92fe9b11..e949b989 100644
--- a/src/store/user.js
+++ b/src/store/user.js
@@ -83,6 +83,10 @@ export default {
 			return state.features.sendFax
 		},
 		hasFaxCapability (state) {
+			return state.capabilities !== null &&
+				state.capabilities.faxserver
+		},
+		hasFaxCapabilityAndFaxActive (state) {
 			return state.capabilities !== null &&
                 state.capabilities.faxserver &&
                 state.capabilities.faxactive
@@ -360,7 +364,7 @@ export default {
 			}
 		},
 		async forwardHome (context) {
-			if (context.rootState.route.path === '/user/home' && !context.getters.isRtcEngineUiVisible) {
+			if (context.rootState.route?.path === '/user/home' && !context.getters.isRtcEngineUiVisible) {
 				await router.push({ path: '/user/conversations' })
 			}
 		},