mirror of
https://github.com/jwinterm/modoboa.git
synced 2026-10-20 06:08:35 +02:00
More improvements
This commit is contained in:
@@ -23,6 +23,13 @@ export default {
|
||||
}
|
||||
return repository.get(`/webmail/emails/content/`, { params })
|
||||
},
|
||||
getEmailSource(mailbox, mailid) {
|
||||
const params = {
|
||||
mailbox,
|
||||
mailid,
|
||||
}
|
||||
return repository.get(`/webmail/emails/source/`, { params })
|
||||
},
|
||||
getEmailAttachment(mailbox, mailid, partnum) {
|
||||
const params = {
|
||||
mailbox,
|
||||
@@ -63,4 +70,7 @@ export default {
|
||||
}
|
||||
return repository.post('/webmail/emails/flag/', body)
|
||||
},
|
||||
sendEmail(body) {
|
||||
return repository.post('/webmail/emails/send/', body)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
<template>
|
||||
<div>
|
||||
<div ref="toolbar" class="mt-4">
|
||||
<v-btn-toggle
|
||||
v-if="editor"
|
||||
v-model="selection"
|
||||
base-color="grey-lighten-3"
|
||||
color="primary"
|
||||
multiple
|
||||
>
|
||||
<v-btn
|
||||
icon="mdi-format-bold"
|
||||
:class="{ 'is-active': editor.isActive('bold') }"
|
||||
size="small"
|
||||
value="bold"
|
||||
@click="editor.chain().focus().toggleBold().run()"
|
||||
/>
|
||||
<v-btn
|
||||
icon="mdi-format-italic"
|
||||
:class="{ 'is-active': editor.isActive('italic') }"
|
||||
size="small"
|
||||
value="italic"
|
||||
@click="editor.chain().focus().toggleItalic().run()"
|
||||
/>
|
||||
<v-btn
|
||||
icon="mdi-format-strikethrough"
|
||||
:class="{ 'is-active': editor.isActive('strike') }"
|
||||
size="small"
|
||||
value="strike"
|
||||
@click="editor.chain().focus().toggleStrike().run()"
|
||||
/>
|
||||
<v-divider vertical />
|
||||
<v-btn
|
||||
icon="mdi-format-align-left"
|
||||
:class="{ 'is-active': editor.isActive({ textAlign: 'left' }) }"
|
||||
size="small"
|
||||
value="left"
|
||||
@click="editor.chain().focus().setTextAlign('left').run()"
|
||||
/>
|
||||
<v-btn
|
||||
icon="mdi-format-align-center"
|
||||
:class="{ 'is-active': editor.isActive({ textAlign: 'center' }) }"
|
||||
size="small"
|
||||
value="center"
|
||||
@click="editor.chain().focus().setTextAlign('center').run()"
|
||||
/>
|
||||
<v-btn
|
||||
icon="mdi-format-align-right"
|
||||
:class="{ 'is-active': editor.isActive({ textAlign: 'right' }) }"
|
||||
size="small"
|
||||
value="right"
|
||||
@click="editor.chain().focus().setTextAlign('right').run()"
|
||||
/>
|
||||
<v-btn
|
||||
icon="mdi-format-align-justify"
|
||||
:class="{ 'is-active': editor.isActive({ textAlign: 'justify' }) }"
|
||||
size="small"
|
||||
value="justify"
|
||||
@click="editor.chain().focus().setTextAlign('justify').run()"
|
||||
/>
|
||||
<v-divider vertical />
|
||||
<v-btn
|
||||
icon="mdi-format-list-bulleted"
|
||||
:class="{ 'is-active': editor.isActive('bulletList') }"
|
||||
size="small"
|
||||
value="bullet"
|
||||
@click="editor.chain().focus().toggleBulletList().run()"
|
||||
/>
|
||||
<v-btn
|
||||
icon="mdi-format-list-numbered"
|
||||
:class="{ 'is-active': editor.isActive('orderedList') }"
|
||||
size="small"
|
||||
value="ordered"
|
||||
@click="editor.chain().focus().toggleOrderedList().run()"
|
||||
/>
|
||||
</v-btn-toggle>
|
||||
<v-btn-group v-if="editor" class="ml-2" color="grey-lighten-3">
|
||||
<v-btn
|
||||
icon="mdi-undo"
|
||||
:disabled="!editor.can().chain().focus().undo().run()"
|
||||
size="small"
|
||||
value="undo"
|
||||
@click="editor.chain().focus().undo().run()"
|
||||
/>
|
||||
<v-btn
|
||||
icon="mdi-redo"
|
||||
:disabled="!editor.can().chain().focus().redo().run()"
|
||||
size="small"
|
||||
value="redo"
|
||||
@click="editor.chain().focus().redo().run()"
|
||||
/>
|
||||
</v-btn-group>
|
||||
</div>
|
||||
<EditorContent
|
||||
:editor="editor"
|
||||
class="border-sm d-flex flex-column flex-grow-1"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch } from 'vue'
|
||||
import Highlight from '@tiptap/extension-highlight'
|
||||
import TextAlign from '@tiptap/extension-text-align'
|
||||
import StarterKit from '@tiptap/starter-kit'
|
||||
import { useEditor, EditorContent } from '@tiptap/vue-3'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
})
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
const selection = ref([])
|
||||
const toolbar = ref()
|
||||
|
||||
const editor = useEditor({
|
||||
content: props.modelValue,
|
||||
extensions: [
|
||||
StarterKit,
|
||||
TextAlign.configure({
|
||||
types: ['heading', 'paragraph'],
|
||||
}),
|
||||
Highlight,
|
||||
],
|
||||
onUpdate: () => {
|
||||
emit('update:modelValue', editor.value.getHTML())
|
||||
},
|
||||
})
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(value) => {
|
||||
if (editor.value.getHTML() === value) {
|
||||
return
|
||||
}
|
||||
editor.value.commands.setContent(value, false)
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.tiptap {
|
||||
:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
ul,
|
||||
ol {
|
||||
padding: 0 1rem;
|
||||
margin: 1.25rem 1rem 1.25rem 0.4rem;
|
||||
|
||||
li p {
|
||||
margin-top: 0.25em;
|
||||
margin-bottom: 0.25em;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,205 @@
|
||||
<template>
|
||||
<div
|
||||
class="bg-white rounded-lg pa-4 position-relative h-100 d-flex flex-column"
|
||||
>
|
||||
<v-toolbar color="white">
|
||||
<v-btn icon="mdi-arrow-left" size="small" variant="flat" @click="close" />
|
||||
<v-btn
|
||||
class="ml-2"
|
||||
color="primary"
|
||||
variant="tonal"
|
||||
prepend-icon="mdi-send"
|
||||
:loading="working"
|
||||
:text="$gettext('Send')"
|
||||
@click="submit"
|
||||
>
|
||||
</v-btn>
|
||||
</v-toolbar>
|
||||
<v-form ref="formRef" class="flex-grow-1 d-flex flex-column">
|
||||
<div>
|
||||
<v-row class="align-center">
|
||||
<v-col cols="2">
|
||||
<span>{{ $gettext('From') }}</span>
|
||||
</v-col>
|
||||
<v-col cols="8">
|
||||
<v-select
|
||||
v-model="form.sender"
|
||||
:items="validSenderAddresses"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
hide-details="auto"
|
||||
:rules="[rules.required]"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row class="align-center">
|
||||
<v-col cols="2">
|
||||
<span>{{ $gettext('To') }}</span>
|
||||
</v-col>
|
||||
<v-col cols="8">
|
||||
<v-text-field
|
||||
v-model="form.to"
|
||||
:placeholder="$gettext('Provide one or more addresses')"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
hide-details="auto"
|
||||
:rules="[rules.required]"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="2">
|
||||
<v-btn
|
||||
v-if="!showCcField"
|
||||
:text="$gettext('Cc')"
|
||||
prepend-icon="mdi-plus"
|
||||
size="x-small"
|
||||
variant="flat"
|
||||
@click="showCcField = true"
|
||||
/>
|
||||
<v-btn
|
||||
v-if="!showBccField"
|
||||
:text="$gettext('Bcc')"
|
||||
prepend-icon="mdi-plus"
|
||||
size="x-small"
|
||||
variant="flat"
|
||||
@click="showBccField = true"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-if="showCcField" class="align-center">
|
||||
<v-col cols="2">
|
||||
<span>{{ $gettext('Cc') }}</span>
|
||||
<v-btn
|
||||
icon="mdi-close"
|
||||
variant="flat"
|
||||
size="x-small"
|
||||
@click="showCcField = false"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="8">
|
||||
<v-text-field
|
||||
v-model="form.cc"
|
||||
:placeholder="$gettext('Provide one or more addresses')"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
hide-details="auto"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-if="showBccField" class="align-center">
|
||||
<v-col cols="2">
|
||||
<span>{{ $gettext('Bcc') }}</span>
|
||||
<v-btn
|
||||
icon="mdi-close"
|
||||
variant="flat"
|
||||
size="x-small"
|
||||
@click="showBccField = false"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="8">
|
||||
<v-text-field
|
||||
v-model="form.bcc"
|
||||
:placeholder="$gettext('Provide one or more addresses')"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
hide-details="auto"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row class="align-center">
|
||||
<v-col cols="2">
|
||||
<span>{{ $gettext('Subject') }}</span>
|
||||
</v-col>
|
||||
<v-col cols="8">
|
||||
<v-text-field
|
||||
v-model="form.subject"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
hide-details="auto"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</div>
|
||||
<HtmlEditor
|
||||
v-model="form.body"
|
||||
class="d-flex flex-column flex-grow-1 mt-4"
|
||||
/>
|
||||
</v-form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useGettext } from 'vue3-gettext'
|
||||
import { useAuthStore, useBusStore } from '@/stores'
|
||||
import HtmlEditor from '@/components/tools/HtmlEditor'
|
||||
import rules from '@/plugins/rules'
|
||||
import api from '@/api/webmail'
|
||||
|
||||
const props = defineProps({
|
||||
originalEmail: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
})
|
||||
|
||||
const router = useRouter()
|
||||
const { $gettext } = useGettext()
|
||||
const { displayNotification } = useBusStore()
|
||||
const authStore = useAuthStore()
|
||||
|
||||
const form = ref({})
|
||||
const formRef = ref()
|
||||
const showCcField = ref(false)
|
||||
const showBccField = ref(false)
|
||||
const working = ref(false)
|
||||
|
||||
const validSenderAddresses = computed(() => {
|
||||
return [authStore.authUser.username]
|
||||
})
|
||||
|
||||
const close = () => {
|
||||
router.push({
|
||||
name: 'MailboxView',
|
||||
})
|
||||
}
|
||||
|
||||
const initForm = () => {
|
||||
form.value = {
|
||||
sender: authStore.authUser.username,
|
||||
}
|
||||
console.log(props.originalEmail)
|
||||
if (props.originalEmail) {
|
||||
form.value.to = props.originalEmail.from_address.address
|
||||
form.value.subject = props.originalEmail.subject
|
||||
form.value.body = props.originalEmail.body
|
||||
}
|
||||
}
|
||||
|
||||
const submit = async () => {
|
||||
const { valid } = await formRef.value.validate()
|
||||
if (!valid) {
|
||||
return
|
||||
}
|
||||
working.value = true
|
||||
const body = { ...form.value }
|
||||
body.to = [body.to]
|
||||
try {
|
||||
await api.sendEmail(body)
|
||||
router.push({ name: 'MailboxView' })
|
||||
displayNotification({ msg: $gettext('Email sent') })
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
} finally {
|
||||
working.value = false
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.originalEmail,
|
||||
() => {
|
||||
initForm()
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
</script>
|
||||
@@ -30,7 +30,7 @@
|
||||
>
|
||||
</v-btn>
|
||||
<v-btn
|
||||
v-if="$route.params.mailbox === 'Junk'"
|
||||
v-if="$route.params.mailbox !== 'Junk'"
|
||||
class="ml-2"
|
||||
color="warning"
|
||||
variant="tonal"
|
||||
|
||||
@@ -22,6 +22,15 @@
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-center mb-4">
|
||||
<v-btn
|
||||
:text="$gettext('Compose')"
|
||||
color="secondary"
|
||||
variant="flat"
|
||||
prepend-icon="mdi-pencil"
|
||||
@click="openComposeForm"
|
||||
/>
|
||||
</div>
|
||||
<MailboxList
|
||||
:mailboxes="userMailboxes"
|
||||
class="mr-2"
|
||||
@@ -56,4 +65,8 @@ function openMailbox(mailbox) {
|
||||
api.getUserMailboxes().then((resp) => {
|
||||
userMailboxes.value = resp.data
|
||||
})
|
||||
|
||||
const openComposeForm = () => {
|
||||
router.push({ name: 'ComposeEmailView' })
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -339,6 +339,22 @@ const routes = [
|
||||
requiresAuth: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'compose',
|
||||
name: 'ComposeEmailView',
|
||||
component: () => import('@/views/webmail/ComposeEmailView.vue'),
|
||||
meta: {
|
||||
requiresAuth: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'reply',
|
||||
name: 'ReplyEmailView',
|
||||
component: () => import('@/views/webmail/ReplyEmailView.vue'),
|
||||
meta: {
|
||||
requiresAuth: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<ComposeEmailForm />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import ComposeEmailForm from '@/components/webmail/ComposeEmailForm'
|
||||
</script>
|
||||
@@ -7,15 +7,20 @@
|
||||
color="primary"
|
||||
variant="tonal"
|
||||
prepend-icon="mdi-reply"
|
||||
append-icon="mdi-chevron-down"
|
||||
@click="replyToEmail"
|
||||
>
|
||||
{{ $gettext('Reply') }}
|
||||
<v-menu activator="parent">
|
||||
<v-list>
|
||||
<v-list-item :title="$gettext('Reply all')" />
|
||||
<v-list-item :title="$gettext('Forward')" />
|
||||
</v-list>
|
||||
</v-menu>
|
||||
<template #append>
|
||||
<v-btn size="x-small" variant="plain">
|
||||
<v-icon icon="mdi-chevron-down" />
|
||||
<v-menu activator="parent">
|
||||
<v-list>
|
||||
<v-list-item :title="$gettext('Reply all')" />
|
||||
<v-list-item :title="$gettext('Forward')" />
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-btn>
|
||||
<v-btn
|
||||
class="ml-2"
|
||||
@@ -28,7 +33,7 @@
|
||||
>
|
||||
</v-btn>
|
||||
<v-btn
|
||||
v-if="$route.params.mailbox === 'Junk'"
|
||||
v-if="route.params.mailbox !== 'Junk'"
|
||||
class="ml-2"
|
||||
color="warning"
|
||||
variant="tonal"
|
||||
@@ -52,7 +57,7 @@
|
||||
<v-btn class="ml-2" variant="tonal" icon size="small">
|
||||
<v-icon icon="mdi-cog" />
|
||||
<v-menu activator="parent">
|
||||
<v-list>
|
||||
<v-list density="compact">
|
||||
<v-list-item
|
||||
v-if="!enableLinks"
|
||||
:title="$gettext('Enable links')"
|
||||
@@ -63,6 +68,10 @@
|
||||
:title="$gettext('Disable links')"
|
||||
@click="enableLinks = false"
|
||||
/>
|
||||
<v-list-item
|
||||
:title="$gettext('Display source')"
|
||||
@click="openEmailSourceDialog"
|
||||
/>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</v-btn>
|
||||
@@ -100,6 +109,20 @@
|
||||
</div>
|
||||
<iframe class="email-frame" />
|
||||
</div>
|
||||
<v-dialog v-model="showEmailSource" max-width="1200">
|
||||
<v-card :title="$gettext('Message source')">
|
||||
<v-card-text class="text-caption overflow-x-auto">
|
||||
<pre>{{ emailSource }}</pre>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn
|
||||
:text="$gettext('Close')"
|
||||
@click="showEmailSource = false"
|
||||
></v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
@@ -116,8 +139,10 @@ const router = useRouter()
|
||||
|
||||
const enableLinks = ref(false)
|
||||
const email = ref(null)
|
||||
const emailSource = ref(null)
|
||||
const headers = ref(null)
|
||||
const loaded = ref(false)
|
||||
const showEmailSource = ref(false)
|
||||
const working = ref(false)
|
||||
|
||||
const recipients = computed(() => {
|
||||
@@ -231,6 +256,21 @@ const markEmailAsNotJunk = () => {
|
||||
displayNotification({ msg: $gettext('Message marked as not junk') })
|
||||
})
|
||||
}
|
||||
|
||||
const openEmailSourceDialog = async () => {
|
||||
if (!emailSource.value) {
|
||||
const resp = await api.getEmailSource(
|
||||
route.query.mailbox,
|
||||
route.query.mailid
|
||||
)
|
||||
emailSource.value = resp.data.source
|
||||
}
|
||||
showEmailSource.value = true
|
||||
}
|
||||
|
||||
const replyToEmail = () => {
|
||||
router.push({ name: 'ReplyEmailView', query: route.query })
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
<template>
|
||||
<ComposeEmailForm :original-email="email" />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import ComposeEmailForm from '@/components/webmail/ComposeEmailForm'
|
||||
import api from '@/api/webmail'
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
const email = ref()
|
||||
|
||||
api
|
||||
.getEmailContent(route.query.mailbox, route.query.mailid, {
|
||||
context: 'reply',
|
||||
})
|
||||
.then((resp) => {
|
||||
email.value = resp.data
|
||||
})
|
||||
</script>
|
||||
Reference in New Issue
Block a user