Initial commit

This commit is contained in:
2026-02-10 19:39:08 -05:00
commit 2a251f9b3f
32 changed files with 6135 additions and 0 deletions

View File

@@ -0,0 +1,98 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useServicesStore } from '@MailManager/stores/servicesStore'
import AddAccountDialog from '@MailManager/components/AddAccountDialog.vue'
import EditAccountDialog from '@MailManager/components/EditAccountDialog.vue'
const servicesStore = useServicesStore()
// Dialog state
const showAddDialog = ref(false)
const showEditDialog = ref(false)
const editServiceProvider = ref<string>('')
const editServiceIdentifier = ref<string | number>('')
// Load services on mount
onMounted(async () => {
if (!servicesStore.has) {
await servicesStore.list()
}
})
const handleAddAccount = () => {
showAddDialog.value = true
}
const handleConfigureAccount = (serviceKey: string) => {
// Service key is in format "provider:identifier"
const [provider, identifier] = serviceKey.split(':')
editServiceProvider.value = provider
editServiceIdentifier.value = identifier
showEditDialog.value = true
}
const handleAccountSaved = async () => {
// Refresh the services list
await servicesStore.list()
}
</script>
<template>
<div class="pa-4">
<h3 class="text-h6 mb-4">Email Accounts</h3>
<v-list>
<v-list-item
v-for="service in servicesStore.services"
:key="`${service.provider}:${service.identifier}`"
>
<template #prepend>
<v-avatar color="primary">
<v-icon>mdi-email</v-icon>
</v-avatar>
</template>
<v-list-item-title>{{ service.label || 'Unnamed Account' }}</v-list-item-title>
<v-list-item-subtitle>{{ service.primaryAddress || service.identifier }}</v-list-item-subtitle>
<template #append>
<v-btn
icon="mdi-cog"
variant="text"
size="small"
@click="handleConfigureAccount(`${service.provider}:${service.identifier}`)"
/>
</template>
</v-list-item>
<v-list-item v-if="servicesStore.services.length === 0">
<v-list-item-title class="text-medium-emphasis">No accounts configured</v-list-item-title>
</v-list-item>
</v-list>
<div class="mt-4">
<v-btn
prepend-icon="mdi-plus"
variant="outlined"
color="primary"
@click="handleAddAccount"
>
Add Account
</v-btn>
</div>
<!-- Add Account Dialog -->
<AddAccountDialog
v-model="showAddDialog"
@saved="handleAccountSaved"
/>
<!-- Edit Account Dialog -->
<EditAccountDialog
v-if="editServiceProvider && editServiceIdentifier"
v-model="showEditDialog"
:service-provider="editServiceProvider"
:service-identifier="editServiceIdentifier"
@saved="handleAccountSaved"
/>
</div>
</template>

View File

@@ -0,0 +1,77 @@
<script setup lang="ts">
import { ref, computed } from 'vue'
import { useUser } from '@KTXC/composables/useUser'
type FolderViewMode = 'tree' | 'page'
const { settings, setSetting } = useUser()
const theme = ref('Auto')
const showPreview = ref(true)
const compactMode = ref(false)
const folderViewMode = computed({
get: () => {
const allSettings = settings.value?.all || {}
const mailSettings = allSettings.mail || {}
return (mailSettings.folderViewMode as FolderViewMode) || 'tree'
},
set: (value: FolderViewMode) => setSetting('mail.folderViewMode', value)
})
</script>
<template>
<div class="pa-4">
<h3 class="text-h6 mb-4">Display Settings</h3>
<v-list>
<v-list-item>
<v-list-item-title>Theme</v-list-item-title>
<v-list-item-subtitle>Choose your preferred theme</v-list-item-subtitle>
<template #append>
<v-select
v-model="theme"
:items="['Light', 'Dark', 'Auto']"
density="compact"
variant="outlined"
style="width: 150px"
/>
</template>
</v-list-item>
<v-list-item>
<v-list-item-title>Message preview</v-list-item-title>
<v-list-item-subtitle>Show message preview in list</v-list-item-subtitle>
<template #append>
<v-switch v-model="showPreview" color="primary" hide-details />
</template>
</v-list-item>
<v-list-item>
<v-list-item-title>Compact mode</v-list-item-title>
<v-list-item-subtitle>Use compact message list layout</v-list-item-subtitle>
<template #append>
<v-switch v-model="compactMode" color="primary" hide-details />
</template>
</v-list-item>
<v-list-item>
<v-list-item-title>Folder navigation style</v-list-item-title>
<v-list-item-subtitle>Choose how folders are displayed</v-list-item-subtitle>
<template #append>
<v-select
v-model="folderViewMode"
:items="[
{ value: 'tree', title: 'Tree' },
{ value: 'page', title: 'Page' }
]"
item-value="value"
item-title="title"
density="compact"
variant="outlined"
style="width: 150px"
/>
</template>
</v-list-item>
</v-list>
</div>
</template>

View File

@@ -0,0 +1,117 @@
<script setup lang="ts">
import { computed } from 'vue'
import { useUser } from '@KTXC/composables/useUser'
import { SecurityLevel, EmailSanitizer } from '@/utile/emailSanitizer'
const { getSetting, setSetting } = useUser()
const securityLevel = computed({
get: () => (getSetting('mail.security.level') as SecurityLevel) || SecurityLevel.MODERATE,
set: (value: SecurityLevel) => setSetting('mail.security.level', value)
})
const allowImagesDefault = computed({
get: () => getSetting('mail.security.allowImagesDefault') as boolean || false,
set: (value: boolean) => setSetting('mail.security.allowImagesDefault', value)
})
const securityLevels = [
{ value: SecurityLevel.STRICT, title: 'Strict', icon: 'mdi-shield-lock', description: 'Maximum security, blocks most content' },
{ value: SecurityLevel.MODERATE, title: 'Moderate', icon: 'mdi-shield-check', description: 'Balanced security and functionality' },
{ value: SecurityLevel.RELAXED, title: 'Relaxed', icon: 'mdi-shield-off', description: 'Minimal restrictions' }
]
</script>
<template>
<div class="pa-4">
<h3 class="text-h6 mb-4">Security & Privacy</h3>
<v-list>
<v-list-subheader>Email Content Security</v-list-subheader>
<v-list-item>
<v-list-item-title>Security Level</v-list-item-title>
<v-list-item-subtitle>
Controls how email content is filtered and sanitized
</v-list-item-subtitle>
<div class="mt-3">
<v-btn-toggle
v-model="securityLevel"
mandatory
color="primary"
variant="outlined"
density="comfortable"
class="d-flex flex-column flex-sm-row"
divided
>
<v-btn
v-for="level in securityLevels"
:key="level.value"
:value="level.value"
class="flex-grow-1"
>
<v-icon start>{{ level.icon }}</v-icon>
{{ level.title }}
</v-btn>
</v-btn-toggle>
<v-alert
density="compact"
variant="tonal"
color="info"
class="text-caption mt-3"
>
{{ EmailSanitizer.getSecurityLevelDescription(securityLevel) }}
</v-alert>
</div>
</v-list-item>
<v-divider class="my-2" />
<v-list-item>
<v-list-item-title>External Images</v-list-item-title>
<v-list-item-subtitle>
Load images from external sources automatically
</v-list-item-subtitle>
<template #append>
<v-switch v-model="allowImagesDefault" color="primary" hide-details />
</template>
</v-list-item>
<v-list-item>
<v-alert
density="compact"
variant="tonal"
color="warning"
icon="mdi-alert"
class="text-caption"
>
<strong>Privacy Warning:</strong> Loading external images may expose your IP address
and confirm to senders that you've opened their email. You can still choose to load
images for individual messages.
</v-alert>
</v-list-item>
</v-list>
<v-list class="mt-4">
<v-list-subheader>Additional Security Options</v-list-subheader>
<v-list-item>
<v-list-item-title>Block tracking pixels</v-list-item-title>
<v-list-item-subtitle>Prevent email tracking (recommended)</v-list-item-subtitle>
<template #append>
<v-switch color="primary" hide-details model-value="true" disabled />
</template>
</v-list-item>
<v-list-item>
<v-list-item-title>Warn on external links</v-list-item-title>
<v-list-item-subtitle>Show confirmation before opening links</v-list-item-subtitle>
<template #append>
<v-switch color="primary" hide-details />
</template>
</v-list-item>
</v-list>
</div>
</template>

View File

@@ -0,0 +1,94 @@
<script setup lang="ts">
import { ref } from 'vue'
import DisplaySettings from './DisplaySettings.vue'
import AccountsSettings from './AccountsSettings.vue'
import SecuritySettings from './SecuritySettings.vue'
interface Props {
modelValue: boolean
}
interface Emits {
(e: 'update:modelValue', value: boolean): void
}
const props = defineProps<Props>()
const emit = defineEmits<Emits>()
const settingsTab = ref('display')
const handleClose = () => {
emit('update:modelValue', false)
}
</script>
<template>
<v-dialog
:model-value="modelValue"
@update:model-value="emit('update:modelValue', $event)"
max-width="800"
scrollable
>
<v-card>
<v-card-title class="d-flex align-center">
<span>Mail Settings</span>
<v-spacer />
<v-btn
icon="mdi-close"
variant="text"
@click="handleClose"
/>
</v-card-title>
<v-divider />
<v-card-text class="pa-0">
<v-tabs
v-model="settingsTab"
bg-color="transparent"
>
<v-tab value="display">
<v-icon start>mdi-palette</v-icon>
Display
</v-tab>
<v-tab value="security">
<v-icon start>mdi-shield-account</v-icon>
Security
</v-tab>
<v-tab value="accounts">
<v-icon start>mdi-account-multiple</v-icon>
Accounts
</v-tab>
</v-tabs>
<v-divider />
<v-window v-model="settingsTab">
<v-window-item value="display">
<DisplaySettings />
</v-window-item>
<v-window-item value="security">
<SecuritySettings />
</v-window-item>
<v-window-item value="accounts">
<AccountsSettings />
</v-window-item>
</v-window>
</v-card-text>
<v-divider />
<v-card-actions>
<v-spacer />
<v-btn
variant="text"
@click="handleClose"
>
Close
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</template>