mirror of
https://github.com/jwinterm/modoboa.git
synced 2026-10-17 05:41:25 +02:00
More improvements
This commit is contained in:
@@ -29,6 +29,7 @@ services:
|
||||
- 8000:8000
|
||||
env_file:
|
||||
- test_project/oidc_key
|
||||
- test_project/envfile
|
||||
environment:
|
||||
DB: "SQLITE"
|
||||
DEBUG: "True"
|
||||
|
||||
@@ -22,6 +22,11 @@
|
||||
"@fullcalendar/vue3": "^6.1.15",
|
||||
"@github/webauthn-json": "^2.1.1",
|
||||
"@mdi/font": "^7.3.67",
|
||||
"@tiptap/extension-highlight": "^2.11.7",
|
||||
"@tiptap/extension-text-align": "^2.11.7",
|
||||
"@tiptap/pm": "^2.11.7",
|
||||
"@tiptap/starter-kit": "^2.11.7",
|
||||
"@tiptap/vue-3": "^2.11.7",
|
||||
"apexcharts": "^3.50.0",
|
||||
"axios": "^1.8.2",
|
||||
"core-js": "^3.37.1",
|
||||
|
||||
@@ -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>
|
||||
+427
-1
@@ -322,6 +322,16 @@
|
||||
resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33"
|
||||
integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==
|
||||
|
||||
"@popperjs/core@^2.9.0":
|
||||
version "2.11.8"
|
||||
resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.8.tgz#6b79032e760a0899cd4204710beede972a3a185f"
|
||||
integrity sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==
|
||||
|
||||
"@remirror/core-constants@3.0.0":
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@remirror/core-constants/-/core-constants-3.0.0.tgz#96fdb89d25c62e7b6a5d08caf0ce5114370e3b8f"
|
||||
integrity sha512-42aWfPrimMfDKDi4YegyS7x+/0tlzaqwPQCULLanv3DMIlu96KTJR0fM5isWX2UViOqlGnX6YFgqWepcX+XMNg==
|
||||
|
||||
"@rollup/rollup-android-arm-eabi@4.32.0":
|
||||
version "4.32.0"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.32.0.tgz#42a8e897c7b656adb4edebda3a8b83a57526452f"
|
||||
@@ -417,6 +427,189 @@
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.32.0.tgz#1a40b4792c08094b6479c48c90fe7f4b10ec2f54"
|
||||
integrity sha512-5hqO5S3PTEO2E5VjCePxv40gIgyS2KvO7E7/vvC/NbIW4SIRamkMr1hqj+5Y67fbBWv/bQLB6KelBQmXlyCjWA==
|
||||
|
||||
"@tiptap/core@^2.11.7":
|
||||
version "2.11.7"
|
||||
resolved "https://registry.yarnpkg.com/@tiptap/core/-/core-2.11.7.tgz#38600a7dabc42ea84e8dfb7a74c19df10db95d14"
|
||||
integrity sha512-zN+NFFxLsxNEL8Qioc+DL6b8+Tt2bmRbXH22Gk6F6nD30x83eaUSFlSv3wqvgyCq3I1i1NO394So+Agmayx6rQ==
|
||||
|
||||
"@tiptap/extension-blockquote@^2.11.7":
|
||||
version "2.11.7"
|
||||
resolved "https://registry.yarnpkg.com/@tiptap/extension-blockquote/-/extension-blockquote-2.11.7.tgz#ccbabedd2e19581424730613f00e971b527198ee"
|
||||
integrity sha512-liD8kWowl3CcYCG9JQlVx1eSNc/aHlt6JpVsuWvzq6J8APWX693i3+zFqyK2eCDn0k+vW62muhSBe3u09hA3Zw==
|
||||
|
||||
"@tiptap/extension-bold@^2.11.7":
|
||||
version "2.11.7"
|
||||
resolved "https://registry.yarnpkg.com/@tiptap/extension-bold/-/extension-bold-2.11.7.tgz#de45d9ab47b7342d83bb38823e57d1d4de3f3ae0"
|
||||
integrity sha512-VTR3JlldBixXbjpLTFme/Bxf1xeUgZZY3LTlt5JDlCW3CxO7k05CIa+kEZ8LXpog5annytZDUVtWqxrNjmsuHQ==
|
||||
|
||||
"@tiptap/extension-bubble-menu@^2.11.7":
|
||||
version "2.11.7"
|
||||
resolved "https://registry.yarnpkg.com/@tiptap/extension-bubble-menu/-/extension-bubble-menu-2.11.7.tgz#12f4d10e340cbd32f3319bda18d2518aa92fd02e"
|
||||
integrity sha512-0vYqSUSSap3kk3/VT4tFE1/6StX70I3/NKQ4J68ZSFgkgyB3ZVlYv7/dY3AkEukjsEp3yN7m8Gw8ei2eEwyzwg==
|
||||
dependencies:
|
||||
tippy.js "^6.3.7"
|
||||
|
||||
"@tiptap/extension-bullet-list@^2.11.7":
|
||||
version "2.11.7"
|
||||
resolved "https://registry.yarnpkg.com/@tiptap/extension-bullet-list/-/extension-bullet-list-2.11.7.tgz#33d965074108ea8dd1ba558b525bb25e12e54876"
|
||||
integrity sha512-WbPogE2/Q3e3/QYgbT1Sj4KQUfGAJNc5pvb7GrUbvRQsAh7HhtuO8hqdDwH8dEdD/cNUehgt17TO7u8qV6qeBw==
|
||||
|
||||
"@tiptap/extension-code-block@^2.11.7":
|
||||
version "2.11.7"
|
||||
resolved "https://registry.yarnpkg.com/@tiptap/extension-code-block/-/extension-code-block-2.11.7.tgz#1aaaadd231317a0e1277aa987854a077cb83f0f7"
|
||||
integrity sha512-To/y/2H04VWqiANy53aXjV7S6fA86c2759RsH1hTIe57jA1KyE7I5tlAofljOLZK/covkGmPeBddSPHGJbz++Q==
|
||||
|
||||
"@tiptap/extension-code@^2.11.7":
|
||||
version "2.11.7"
|
||||
resolved "https://registry.yarnpkg.com/@tiptap/extension-code/-/extension-code-2.11.7.tgz#d9cb080992080d3a576482d8beee280d29d8a9df"
|
||||
integrity sha512-VpPO1Uy/eF4hYOpohS/yMOcE1C07xmMj0/D989D9aS1x95jWwUVrSkwC+PlWMUBx9PbY2NRsg1ZDwVvlNKZ6yQ==
|
||||
|
||||
"@tiptap/extension-document@^2.11.7":
|
||||
version "2.11.7"
|
||||
resolved "https://registry.yarnpkg.com/@tiptap/extension-document/-/extension-document-2.11.7.tgz#07e3fcf42069fda02b63758b2f236822a7a14acc"
|
||||
integrity sha512-95ouJXPjdAm9+VBRgFo4lhDoMcHovyl/awORDI8gyEn0Rdglt+ZRZYoySFzbVzer9h0cre+QdIwr9AIzFFbfdA==
|
||||
|
||||
"@tiptap/extension-dropcursor@^2.11.7":
|
||||
version "2.11.7"
|
||||
resolved "https://registry.yarnpkg.com/@tiptap/extension-dropcursor/-/extension-dropcursor-2.11.7.tgz#389229b91ccce4d8ab2139acd9dafadbfedac7c0"
|
||||
integrity sha512-63mL+nxQILizsr5NbmgDeOjFEWi34BLt7evwL6UUZEVM15K8V1G8pD9Y0kCXrZYpHWz0tqFRXdrhDz0Ppu8oVw==
|
||||
|
||||
"@tiptap/extension-floating-menu@^2.11.7":
|
||||
version "2.11.7"
|
||||
resolved "https://registry.yarnpkg.com/@tiptap/extension-floating-menu/-/extension-floating-menu-2.11.7.tgz#cf674103730391dabd947c05c38f4e4b73d59842"
|
||||
integrity sha512-DG54WoUu2vxHRVzKZiR5I5RMOYj45IlxQMkBAx1wjS0ch41W8DUYEeipvMMjCeKtEI+emz03xYUcOAP9LRmg+w==
|
||||
dependencies:
|
||||
tippy.js "^6.3.7"
|
||||
|
||||
"@tiptap/extension-gapcursor@^2.11.7":
|
||||
version "2.11.7"
|
||||
resolved "https://registry.yarnpkg.com/@tiptap/extension-gapcursor/-/extension-gapcursor-2.11.7.tgz#82110b499ba7ae9a76cd57f42d535af3989abd98"
|
||||
integrity sha512-EceesmPG7FyjXZ8EgeJPUov9G1mAf2AwdypxBNH275g6xd5dmU/KvjoFZjmQ0X1ve7mS+wNupVlGxAEUYoveew==
|
||||
|
||||
"@tiptap/extension-hard-break@^2.11.7":
|
||||
version "2.11.7"
|
||||
resolved "https://registry.yarnpkg.com/@tiptap/extension-hard-break/-/extension-hard-break-2.11.7.tgz#6344893e9ac938684cecabad4ea037cc729f6cd7"
|
||||
integrity sha512-zTkZSA6q+F5sLOdCkiC2+RqJQN0zdsJqvFIOVFL/IDVOnq6PZO5THzwRRLvOSnJJl3edRQCl/hUgS0L5sTInGQ==
|
||||
|
||||
"@tiptap/extension-heading@^2.11.7":
|
||||
version "2.11.7"
|
||||
resolved "https://registry.yarnpkg.com/@tiptap/extension-heading/-/extension-heading-2.11.7.tgz#0e29db2a5dcd3456c7ab1a40817b56b926a073ec"
|
||||
integrity sha512-8kWh7y4Rd2fwxfWOhFFWncHdkDkMC1Z60yzIZWjIu72+6yQxvo8w3yeb7LI7jER4kffbMmadgcfhCHC/fkObBA==
|
||||
|
||||
"@tiptap/extension-highlight@^2.11.7":
|
||||
version "2.11.7"
|
||||
resolved "https://registry.yarnpkg.com/@tiptap/extension-highlight/-/extension-highlight-2.11.7.tgz#2c92a6b8de96edfbdb2d3cdbd9eb8cf9c8011da8"
|
||||
integrity sha512-c/NH4kIpNOWCUQv8RkFNDyOcgt+2pYFpDf0QBJmzhAuv4BIeS2bDmDtuNS7VgoWRZH+xxCNXfvm2BG+kjtipEg==
|
||||
|
||||
"@tiptap/extension-history@^2.11.7":
|
||||
version "2.11.7"
|
||||
resolved "https://registry.yarnpkg.com/@tiptap/extension-history/-/extension-history-2.11.7.tgz#428d3a4e11f1c261ec34b68c2d3d84f1377ed81e"
|
||||
integrity sha512-Cu5x3aS13I040QSRoLdd+w09G4OCVfU+azpUqxufZxeNs9BIJC+0jowPLeOxKDh6D5GGT2A8sQtxc6a/ssbs8g==
|
||||
|
||||
"@tiptap/extension-horizontal-rule@^2.11.7":
|
||||
version "2.11.7"
|
||||
resolved "https://registry.yarnpkg.com/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-2.11.7.tgz#36e33184064f844aeb3950011c346643926e682b"
|
||||
integrity sha512-uVmQwD2dzZ5xwmvUlciy0ItxOdOfQjH6VLmu80zyJf8Yu7mvwP8JyxoXUX0vd1xHpwAhgQ9/ozjIWYGIw79DPQ==
|
||||
|
||||
"@tiptap/extension-italic@^2.11.7":
|
||||
version "2.11.7"
|
||||
resolved "https://registry.yarnpkg.com/@tiptap/extension-italic/-/extension-italic-2.11.7.tgz#f3830c6ec8c7a12e20c64f9b7f100eff1a5382d9"
|
||||
integrity sha512-r985bkQfG0HMpmCU0X0p/Xe7U1qgRm2mxvcp6iPCuts2FqxaCoyfNZ8YnMsgVK1mRhM7+CQ5SEg2NOmQNtHvPw==
|
||||
|
||||
"@tiptap/extension-list-item@^2.11.7":
|
||||
version "2.11.7"
|
||||
resolved "https://registry.yarnpkg.com/@tiptap/extension-list-item/-/extension-list-item-2.11.7.tgz#06d9ea69dadaa09d260cfde81d5c77b56d8b0937"
|
||||
integrity sha512-6ikh7Y+qAbkSuIHXPIINqfzmWs5uIGrylihdZ9adaIyvrN1KSnWIqrZIk/NcZTg5YFIJlXrnGSRSjb/QM3WUhw==
|
||||
|
||||
"@tiptap/extension-ordered-list@^2.11.7":
|
||||
version "2.11.7"
|
||||
resolved "https://registry.yarnpkg.com/@tiptap/extension-ordered-list/-/extension-ordered-list-2.11.7.tgz#3310176a22ed6010b646ccf9c4fe6c25d05553dc"
|
||||
integrity sha512-bLGCHDMB0vbJk7uu8bRg8vES3GsvxkX7Cgjgm/6xysHFbK98y0asDtNxkW1VvuRreNGz4tyB6vkcVCfrxl4jKw==
|
||||
|
||||
"@tiptap/extension-paragraph@^2.11.7":
|
||||
version "2.11.7"
|
||||
resolved "https://registry.yarnpkg.com/@tiptap/extension-paragraph/-/extension-paragraph-2.11.7.tgz#de7fba01ebd35bddf4d6225fff708eb223276bc5"
|
||||
integrity sha512-Pl3B4q6DJqTvvAdraqZaNP9Hh0UWEHL5nNdxhaRNuhKaUo7lq8wbDSIxIW3lvV0lyCs0NfyunkUvSm1CXb6d4Q==
|
||||
|
||||
"@tiptap/extension-strike@^2.11.7":
|
||||
version "2.11.7"
|
||||
resolved "https://registry.yarnpkg.com/@tiptap/extension-strike/-/extension-strike-2.11.7.tgz#311b55f06bb3d1f93c02b809844cc8d27d110422"
|
||||
integrity sha512-D6GYiW9F24bvAY7XMOARNZbC8YGPzdzWdXd8VOOJABhf4ynMi/oW4NNiko+kZ67jn3EGaKoz32VMJzNQgYi1HA==
|
||||
|
||||
"@tiptap/extension-text-align@^2.11.7":
|
||||
version "2.11.7"
|
||||
resolved "https://registry.yarnpkg.com/@tiptap/extension-text-align/-/extension-text-align-2.11.7.tgz#a8e7058eae4f31b1741c615c383946fad377e7cd"
|
||||
integrity sha512-3M8zd9ROADXazVNpgR6Ejs1evSvBveN36qN4GgV71GqrNlTcjqYgQcXFLQrsd2hnE+aXir8/8bLJ+aaJXDninA==
|
||||
|
||||
"@tiptap/extension-text-style@^2.11.7":
|
||||
version "2.11.7"
|
||||
resolved "https://registry.yarnpkg.com/@tiptap/extension-text-style/-/extension-text-style-2.11.7.tgz#1af9c23ed6154680eac1c0621d1112764c3fafb9"
|
||||
integrity sha512-LHO6DBg/9SkCQFdWlVfw9nolUmw+Cid94WkTY+7IwrpyG2+ZGQxnKpCJCKyeaFNbDoYAtvu0vuTsSXeCkgShcA==
|
||||
|
||||
"@tiptap/extension-text@^2.11.7":
|
||||
version "2.11.7"
|
||||
resolved "https://registry.yarnpkg.com/@tiptap/extension-text/-/extension-text-2.11.7.tgz#bbd32763a5db1e6c0ad4239233a9f86e27d15179"
|
||||
integrity sha512-wObCn8qZkIFnXTLvBP+X8KgaEvTap/FJ/i4hBMfHBCKPGDx99KiJU6VIbDXG8d5ZcFZE0tOetK1pP5oI7qgMlQ==
|
||||
|
||||
"@tiptap/pm@^2.11.7":
|
||||
version "2.11.7"
|
||||
resolved "https://registry.yarnpkg.com/@tiptap/pm/-/pm-2.11.7.tgz#34e1dbe1f27ea978bc740c9144ae8195948609e3"
|
||||
integrity sha512-7gEEfz2Q6bYKXM07vzLUD0vqXFhC5geWRA6LCozTiLdVFDdHWiBrvb2rtkL5T7mfLq03zc1QhH7rI3F6VntOEA==
|
||||
dependencies:
|
||||
prosemirror-changeset "^2.2.1"
|
||||
prosemirror-collab "^1.3.1"
|
||||
prosemirror-commands "^1.6.2"
|
||||
prosemirror-dropcursor "^1.8.1"
|
||||
prosemirror-gapcursor "^1.3.2"
|
||||
prosemirror-history "^1.4.1"
|
||||
prosemirror-inputrules "^1.4.0"
|
||||
prosemirror-keymap "^1.2.2"
|
||||
prosemirror-markdown "^1.13.1"
|
||||
prosemirror-menu "^1.2.4"
|
||||
prosemirror-model "^1.23.0"
|
||||
prosemirror-schema-basic "^1.2.3"
|
||||
prosemirror-schema-list "^1.4.1"
|
||||
prosemirror-state "^1.4.3"
|
||||
prosemirror-tables "^1.6.4"
|
||||
prosemirror-trailing-node "^3.0.0"
|
||||
prosemirror-transform "^1.10.2"
|
||||
prosemirror-view "^1.37.0"
|
||||
|
||||
"@tiptap/starter-kit@^2.11.7":
|
||||
version "2.11.7"
|
||||
resolved "https://registry.yarnpkg.com/@tiptap/starter-kit/-/starter-kit-2.11.7.tgz#fd7f2c0021d5cd913afe289eb211b196cf509caf"
|
||||
integrity sha512-K+q51KwNU/l0kqRuV5e1824yOLVftj6kGplGQLvJG56P7Rb2dPbM/JeaDbxQhnHT/KDGamG0s0Po0M3pPY163A==
|
||||
dependencies:
|
||||
"@tiptap/core" "^2.11.7"
|
||||
"@tiptap/extension-blockquote" "^2.11.7"
|
||||
"@tiptap/extension-bold" "^2.11.7"
|
||||
"@tiptap/extension-bullet-list" "^2.11.7"
|
||||
"@tiptap/extension-code" "^2.11.7"
|
||||
"@tiptap/extension-code-block" "^2.11.7"
|
||||
"@tiptap/extension-document" "^2.11.7"
|
||||
"@tiptap/extension-dropcursor" "^2.11.7"
|
||||
"@tiptap/extension-gapcursor" "^2.11.7"
|
||||
"@tiptap/extension-hard-break" "^2.11.7"
|
||||
"@tiptap/extension-heading" "^2.11.7"
|
||||
"@tiptap/extension-history" "^2.11.7"
|
||||
"@tiptap/extension-horizontal-rule" "^2.11.7"
|
||||
"@tiptap/extension-italic" "^2.11.7"
|
||||
"@tiptap/extension-list-item" "^2.11.7"
|
||||
"@tiptap/extension-ordered-list" "^2.11.7"
|
||||
"@tiptap/extension-paragraph" "^2.11.7"
|
||||
"@tiptap/extension-strike" "^2.11.7"
|
||||
"@tiptap/extension-text" "^2.11.7"
|
||||
"@tiptap/extension-text-style" "^2.11.7"
|
||||
"@tiptap/pm" "^2.11.7"
|
||||
|
||||
"@tiptap/vue-3@^2.11.7":
|
||||
version "2.11.7"
|
||||
resolved "https://registry.yarnpkg.com/@tiptap/vue-3/-/vue-3-2.11.7.tgz#b5d3293ce5621876a38e1e22b7403f265a4e508a"
|
||||
integrity sha512-P4Dyi7Uvi+l2ubsVTibZU3XVLT15eWP0W3mPiQwT0IVI0+FjGyBa83TXgMh5Kb53nxABgIK7FiIMBtQPSkjqfg==
|
||||
dependencies:
|
||||
"@tiptap/extension-bubble-menu" "^2.11.7"
|
||||
"@tiptap/extension-floating-menu" "^2.11.7"
|
||||
|
||||
"@types/estree@1.0.6", "@types/estree@^1.0.6":
|
||||
version "1.0.6"
|
||||
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.6.tgz#628effeeae2064a1b4e79f78e81d87b7e5fc7b50"
|
||||
@@ -435,6 +628,24 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841"
|
||||
integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==
|
||||
|
||||
"@types/linkify-it@^5":
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/linkify-it/-/linkify-it-5.0.0.tgz#21413001973106cda1c3a9b91eedd4ccd5469d76"
|
||||
integrity sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==
|
||||
|
||||
"@types/markdown-it@^14.0.0":
|
||||
version "14.1.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/markdown-it/-/markdown-it-14.1.2.tgz#57f2532a0800067d9b934f3521429a2e8bfb4c61"
|
||||
integrity sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==
|
||||
dependencies:
|
||||
"@types/linkify-it" "^5"
|
||||
"@types/mdurl" "^2"
|
||||
|
||||
"@types/mdurl@^2":
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/mdurl/-/mdurl-2.0.0.tgz#d43878b5b20222682163ae6f897b20447233bdfd"
|
||||
integrity sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==
|
||||
|
||||
"@types/minimatch@*":
|
||||
version "5.1.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-5.1.2.tgz#07508b45797cb81ec3f273011b054cd0755eddca"
|
||||
@@ -760,6 +971,11 @@ cosmiconfig@^9.0.0:
|
||||
js-yaml "^4.1.0"
|
||||
parse-json "^5.2.0"
|
||||
|
||||
crelt@^1.0.0:
|
||||
version "1.0.6"
|
||||
resolved "https://registry.yarnpkg.com/crelt/-/crelt-1.0.6.tgz#7cc898ea74e190fb6ef9dae57f8f81cf7302df72"
|
||||
integrity sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==
|
||||
|
||||
cross-spawn@^7.0.0, cross-spawn@^7.0.6:
|
||||
version "7.0.6"
|
||||
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f"
|
||||
@@ -821,7 +1037,7 @@ emoji-regex@^9.2.2:
|
||||
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.2.2.tgz#840c8803b0d8047f4ff0cf963176b32d4ef3ed72"
|
||||
integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==
|
||||
|
||||
entities@^4.5.0:
|
||||
entities@^4.4.0, entities@^4.5.0:
|
||||
version "4.5.0"
|
||||
resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48"
|
||||
integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==
|
||||
@@ -1330,6 +1546,13 @@ lines-and-columns@^1.1.6:
|
||||
resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632"
|
||||
integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==
|
||||
|
||||
linkify-it@^5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/linkify-it/-/linkify-it-5.0.0.tgz#9ef238bfa6dc70bd8e7f9572b52d369af569b421"
|
||||
integrity sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==
|
||||
dependencies:
|
||||
uc.micro "^2.0.0"
|
||||
|
||||
locate-path@^6.0.0:
|
||||
version "6.0.0"
|
||||
resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286"
|
||||
@@ -1369,6 +1592,23 @@ magic-string@^0.30.11:
|
||||
dependencies:
|
||||
"@jridgewell/sourcemap-codec" "^1.5.0"
|
||||
|
||||
markdown-it@^14.0.0:
|
||||
version "14.1.0"
|
||||
resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-14.1.0.tgz#3c3c5992883c633db4714ccb4d7b5935d98b7d45"
|
||||
integrity sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==
|
||||
dependencies:
|
||||
argparse "^2.0.1"
|
||||
entities "^4.4.0"
|
||||
linkify-it "^5.0.0"
|
||||
mdurl "^2.0.0"
|
||||
punycode.js "^2.3.1"
|
||||
uc.micro "^2.1.0"
|
||||
|
||||
mdurl@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-2.0.0.tgz#80676ec0433025dd3e17ee983d0fe8de5a2237e0"
|
||||
integrity sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==
|
||||
|
||||
merge2@^1.3.0:
|
||||
version "1.4.1"
|
||||
resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae"
|
||||
@@ -1466,6 +1706,11 @@ optionator@^0.9.3:
|
||||
type-check "^0.4.0"
|
||||
word-wrap "^1.2.5"
|
||||
|
||||
orderedmap@^2.0.0:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/orderedmap/-/orderedmap-2.1.1.tgz#61481269c44031c449915497bf5a4ad273c512d2"
|
||||
integrity sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==
|
||||
|
||||
p-limit@^3.0.2:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b"
|
||||
@@ -1597,11 +1842,170 @@ prettier@^3.3.2:
|
||||
resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.4.2.tgz#a5ce1fb522a588bf2b78ca44c6e6fe5aa5a2b13f"
|
||||
integrity sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==
|
||||
|
||||
prosemirror-changeset@^2.2.1:
|
||||
version "2.2.1"
|
||||
resolved "https://registry.yarnpkg.com/prosemirror-changeset/-/prosemirror-changeset-2.2.1.tgz#dae94b63aec618fac7bb9061648e6e2a79988383"
|
||||
integrity sha512-J7msc6wbxB4ekDFj+n9gTW/jav/p53kdlivvuppHsrZXCaQdVgRghoZbSS3kwrRyAstRVQ4/+u5k7YfLgkkQvQ==
|
||||
dependencies:
|
||||
prosemirror-transform "^1.0.0"
|
||||
|
||||
prosemirror-collab@^1.3.1:
|
||||
version "1.3.1"
|
||||
resolved "https://registry.yarnpkg.com/prosemirror-collab/-/prosemirror-collab-1.3.1.tgz#0e8c91e76e009b53457eb3b3051fb68dad029a33"
|
||||
integrity sha512-4SnynYR9TTYaQVXd/ieUvsVV4PDMBzrq2xPUWutHivDuOshZXqQ5rGbZM84HEaXKbLdItse7weMGOUdDVcLKEQ==
|
||||
dependencies:
|
||||
prosemirror-state "^1.0.0"
|
||||
|
||||
prosemirror-commands@^1.0.0, prosemirror-commands@^1.6.2:
|
||||
version "1.7.0"
|
||||
resolved "https://registry.yarnpkg.com/prosemirror-commands/-/prosemirror-commands-1.7.0.tgz#c0a60c808f51157caa146922494fc59fe257f27c"
|
||||
integrity sha512-6toodS4R/Aah5pdsrIwnTYPEjW70SlO5a66oo5Kk+CIrgJz3ukOoS+FYDGqvQlAX5PxoGWDX1oD++tn5X3pyRA==
|
||||
dependencies:
|
||||
prosemirror-model "^1.0.0"
|
||||
prosemirror-state "^1.0.0"
|
||||
prosemirror-transform "^1.10.2"
|
||||
|
||||
prosemirror-dropcursor@^1.8.1:
|
||||
version "1.8.1"
|
||||
resolved "https://registry.yarnpkg.com/prosemirror-dropcursor/-/prosemirror-dropcursor-1.8.1.tgz#49b9fb2f583e0d0f4021ff87db825faa2be2832d"
|
||||
integrity sha512-M30WJdJZLyXHi3N8vxN6Zh5O8ZBbQCz0gURTfPmTIBNQ5pxrdU7A58QkNqfa98YEjSAL1HUyyU34f6Pm5xBSGw==
|
||||
dependencies:
|
||||
prosemirror-state "^1.0.0"
|
||||
prosemirror-transform "^1.1.0"
|
||||
prosemirror-view "^1.1.0"
|
||||
|
||||
prosemirror-gapcursor@^1.3.2:
|
||||
version "1.3.2"
|
||||
resolved "https://registry.yarnpkg.com/prosemirror-gapcursor/-/prosemirror-gapcursor-1.3.2.tgz#5fa336b83789c6199a7341c9493587e249215cb4"
|
||||
integrity sha512-wtjswVBd2vaQRrnYZaBCbyDqr232Ed4p2QPtRIUK5FuqHYKGWkEwl08oQM4Tw7DOR0FsasARV5uJFvMZWxdNxQ==
|
||||
dependencies:
|
||||
prosemirror-keymap "^1.0.0"
|
||||
prosemirror-model "^1.0.0"
|
||||
prosemirror-state "^1.0.0"
|
||||
prosemirror-view "^1.0.0"
|
||||
|
||||
prosemirror-history@^1.0.0, prosemirror-history@^1.4.1:
|
||||
version "1.4.1"
|
||||
resolved "https://registry.yarnpkg.com/prosemirror-history/-/prosemirror-history-1.4.1.tgz#cc370a46fb629e83a33946a0e12612e934ab8b98"
|
||||
integrity sha512-2JZD8z2JviJrboD9cPuX/Sv/1ChFng+xh2tChQ2X4bB2HeK+rra/bmJ3xGntCcjhOqIzSDG6Id7e8RJ9QPXLEQ==
|
||||
dependencies:
|
||||
prosemirror-state "^1.2.2"
|
||||
prosemirror-transform "^1.0.0"
|
||||
prosemirror-view "^1.31.0"
|
||||
rope-sequence "^1.3.0"
|
||||
|
||||
prosemirror-inputrules@^1.4.0:
|
||||
version "1.5.0"
|
||||
resolved "https://registry.yarnpkg.com/prosemirror-inputrules/-/prosemirror-inputrules-1.5.0.tgz#e22bfaf1d6ea4fe240ad447c184af3d520d43c37"
|
||||
integrity sha512-K0xJRCmt+uSw7xesnHmcn72yBGTbY45vm8gXI4LZXbx2Z0jwh5aF9xrGQgrVPu0WbyFVFF3E/o9VhJYz6SQWnA==
|
||||
dependencies:
|
||||
prosemirror-state "^1.0.0"
|
||||
prosemirror-transform "^1.0.0"
|
||||
|
||||
prosemirror-keymap@^1.0.0, prosemirror-keymap@^1.2.2:
|
||||
version "1.2.2"
|
||||
resolved "https://registry.yarnpkg.com/prosemirror-keymap/-/prosemirror-keymap-1.2.2.tgz#14a54763a29c7b2704f561088ccf3384d14eb77e"
|
||||
integrity sha512-EAlXoksqC6Vbocqc0GtzCruZEzYgrn+iiGnNjsJsH4mrnIGex4qbLdWWNza3AW5W36ZRrlBID0eM6bdKH4OStQ==
|
||||
dependencies:
|
||||
prosemirror-state "^1.0.0"
|
||||
w3c-keyname "^2.2.0"
|
||||
|
||||
prosemirror-markdown@^1.13.1:
|
||||
version "1.13.2"
|
||||
resolved "https://registry.yarnpkg.com/prosemirror-markdown/-/prosemirror-markdown-1.13.2.tgz#863eb3fd5f57a444e4378174622b562735b1c503"
|
||||
integrity sha512-FPD9rHPdA9fqzNmIIDhhnYQ6WgNoSWX9StUZ8LEKapaXU9i6XgykaHKhp6XMyXlOWetmaFgGDS/nu/w9/vUc5g==
|
||||
dependencies:
|
||||
"@types/markdown-it" "^14.0.0"
|
||||
markdown-it "^14.0.0"
|
||||
prosemirror-model "^1.25.0"
|
||||
|
||||
prosemirror-menu@^1.2.4:
|
||||
version "1.2.4"
|
||||
resolved "https://registry.yarnpkg.com/prosemirror-menu/-/prosemirror-menu-1.2.4.tgz#3cfdc7c06d10f9fbd1bce29082c498bd11a0a79a"
|
||||
integrity sha512-S/bXlc0ODQup6aiBbWVsX/eM+xJgCTAfMq/nLqaO5ID/am4wS0tTCIkzwytmao7ypEtjj39i7YbJjAgO20mIqA==
|
||||
dependencies:
|
||||
crelt "^1.0.0"
|
||||
prosemirror-commands "^1.0.0"
|
||||
prosemirror-history "^1.0.0"
|
||||
prosemirror-state "^1.0.0"
|
||||
|
||||
prosemirror-model@^1.0.0, prosemirror-model@^1.20.0, prosemirror-model@^1.21.0, prosemirror-model@^1.23.0, prosemirror-model@^1.24.1, prosemirror-model@^1.25.0:
|
||||
version "1.25.0"
|
||||
resolved "https://registry.yarnpkg.com/prosemirror-model/-/prosemirror-model-1.25.0.tgz#c147113edc0718a14f03881e4c20367d0221f7af"
|
||||
integrity sha512-/8XUmxWf0pkj2BmtqZHYJipTBMHIdVjuvFzMvEoxrtyGNmfvdhBiRwYt/eFwy2wA9DtBW3RLqvZnjurEkHaFCw==
|
||||
dependencies:
|
||||
orderedmap "^2.0.0"
|
||||
|
||||
prosemirror-schema-basic@^1.2.3:
|
||||
version "1.2.4"
|
||||
resolved "https://registry.yarnpkg.com/prosemirror-schema-basic/-/prosemirror-schema-basic-1.2.4.tgz#389ce1ec09b8a30ea9bbb92c58569cb690c2d695"
|
||||
integrity sha512-ELxP4TlX3yr2v5rM7Sb70SqStq5NvI15c0j9j/gjsrO5vaw+fnnpovCLEGIcpeGfifkuqJwl4fon6b+KdrODYQ==
|
||||
dependencies:
|
||||
prosemirror-model "^1.25.0"
|
||||
|
||||
prosemirror-schema-list@^1.4.1:
|
||||
version "1.5.1"
|
||||
resolved "https://registry.yarnpkg.com/prosemirror-schema-list/-/prosemirror-schema-list-1.5.1.tgz#5869c8f749e8745c394548bb11820b0feb1e32f5"
|
||||
integrity sha512-927lFx/uwyQaGwJxLWCZRkjXG0p48KpMj6ueoYiu4JX05GGuGcgzAy62dfiV8eFZftgyBUvLx76RsMe20fJl+Q==
|
||||
dependencies:
|
||||
prosemirror-model "^1.0.0"
|
||||
prosemirror-state "^1.0.0"
|
||||
prosemirror-transform "^1.7.3"
|
||||
|
||||
prosemirror-state@^1.0.0, prosemirror-state@^1.2.2, prosemirror-state@^1.4.3:
|
||||
version "1.4.3"
|
||||
resolved "https://registry.yarnpkg.com/prosemirror-state/-/prosemirror-state-1.4.3.tgz#94aecf3ffd54ec37e87aa7179d13508da181a080"
|
||||
integrity sha512-goFKORVbvPuAQaXhpbemJFRKJ2aixr+AZMGiquiqKxaucC6hlpHNZHWgz5R7dS4roHiwq9vDctE//CZ++o0W1Q==
|
||||
dependencies:
|
||||
prosemirror-model "^1.0.0"
|
||||
prosemirror-transform "^1.0.0"
|
||||
prosemirror-view "^1.27.0"
|
||||
|
||||
prosemirror-tables@^1.6.4:
|
||||
version "1.6.4"
|
||||
resolved "https://registry.yarnpkg.com/prosemirror-tables/-/prosemirror-tables-1.6.4.tgz#e36ebca70d9e398c4a3b99b122ba86bfc985293d"
|
||||
integrity sha512-TkDY3Gw52gRFRfRn2f4wJv5WOgAOXLJA2CQJYIJ5+kdFbfj3acR4JUW6LX2e1hiEBiUwvEhzH5a3cZ5YSztpIA==
|
||||
dependencies:
|
||||
prosemirror-keymap "^1.2.2"
|
||||
prosemirror-model "^1.24.1"
|
||||
prosemirror-state "^1.4.3"
|
||||
prosemirror-transform "^1.10.2"
|
||||
prosemirror-view "^1.37.2"
|
||||
|
||||
prosemirror-trailing-node@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/prosemirror-trailing-node/-/prosemirror-trailing-node-3.0.0.tgz#5bc223d4fc1e8d9145e4079ec77a932b54e19e04"
|
||||
integrity sha512-xiun5/3q0w5eRnGYfNlW1uU9W6x5MoFKWwq/0TIRgt09lv7Hcser2QYV8t4muXbEr+Fwo0geYn79Xs4GKywrRQ==
|
||||
dependencies:
|
||||
"@remirror/core-constants" "3.0.0"
|
||||
escape-string-regexp "^4.0.0"
|
||||
|
||||
prosemirror-transform@^1.0.0, prosemirror-transform@^1.1.0, prosemirror-transform@^1.10.2, prosemirror-transform@^1.7.3:
|
||||
version "1.10.3"
|
||||
resolved "https://registry.yarnpkg.com/prosemirror-transform/-/prosemirror-transform-1.10.3.tgz#fae660bd7ffef3159aff44bc21e9e044aa31b67d"
|
||||
integrity sha512-Nhh/+1kZGRINbEHmVu39oynhcap4hWTs/BlU7NnxWj3+l0qi8I1mu67v6mMdEe/ltD8hHvU4FV6PHiCw2VSpMw==
|
||||
dependencies:
|
||||
prosemirror-model "^1.21.0"
|
||||
|
||||
prosemirror-view@^1.0.0, prosemirror-view@^1.1.0, prosemirror-view@^1.27.0, prosemirror-view@^1.31.0, prosemirror-view@^1.37.0, prosemirror-view@^1.37.2:
|
||||
version "1.38.1"
|
||||
resolved "https://registry.yarnpkg.com/prosemirror-view/-/prosemirror-view-1.38.1.tgz#566d30cc8b00a68d6b4c60f5d8a6ab97c82990b3"
|
||||
integrity sha512-4FH/uM1A4PNyrxXbD+RAbAsf0d/mM0D/wAKSVVWK7o0A9Q/oOXJBrw786mBf2Vnrs/Edly6dH6Z2gsb7zWwaUw==
|
||||
dependencies:
|
||||
prosemirror-model "^1.20.0"
|
||||
prosemirror-state "^1.0.0"
|
||||
prosemirror-transform "^1.1.0"
|
||||
|
||||
proxy-from-env@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2"
|
||||
integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==
|
||||
|
||||
punycode.js@^2.3.1:
|
||||
version "2.3.1"
|
||||
resolved "https://registry.yarnpkg.com/punycode.js/-/punycode.js-2.3.1.tgz#6b53e56ad75588234e79f4affa90972c7dd8cdb7"
|
||||
integrity sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==
|
||||
|
||||
punycode@^2.1.0:
|
||||
version "2.3.1"
|
||||
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5"
|
||||
@@ -1667,6 +2071,11 @@ rollup@^4.20.0:
|
||||
"@rollup/rollup-win32-x64-msvc" "4.32.0"
|
||||
fsevents "~2.3.2"
|
||||
|
||||
rope-sequence@^1.3.0:
|
||||
version "1.3.4"
|
||||
resolved "https://registry.yarnpkg.com/rope-sequence/-/rope-sequence-1.3.4.tgz#df85711aaecd32f1e756f76e43a415171235d425"
|
||||
integrity sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==
|
||||
|
||||
run-parallel@^1.1.9:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee"
|
||||
@@ -1825,6 +2234,13 @@ svg.select.js@^3.0.1:
|
||||
dependencies:
|
||||
svg.js "^2.6.5"
|
||||
|
||||
tippy.js@^6.3.7:
|
||||
version "6.3.7"
|
||||
resolved "https://registry.yarnpkg.com/tippy.js/-/tippy.js-6.3.7.tgz#8ccfb651d642010ed9a32ff29b0e9e19c5b8c61c"
|
||||
integrity sha512-E1d3oP2emgJ9dRQZdf3Kkn0qJgI6ZLpyS5z6ZkY1DF3kaQaBsGZsndEpHwx+eC+tYM41HaSNvNtLx8tU57FzTQ==
|
||||
dependencies:
|
||||
"@popperjs/core" "^2.9.0"
|
||||
|
||||
to-regex-range@^5.0.1:
|
||||
version "5.0.1"
|
||||
resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4"
|
||||
@@ -1854,6 +2270,11 @@ typical@^4.0.0:
|
||||
resolved "https://registry.yarnpkg.com/typical/-/typical-4.0.0.tgz#cbeaff3b9d7ae1e2bbfaf5a4e6f11eccfde94fc4"
|
||||
integrity sha512-VAH4IvQ7BDFYglMd7BPRDfLgxZZX4O4TFcRDA6EN5X7erNJJq+McIEp8np9aVtxrCJ6qx4GTYVfOWNjcqwZgRw==
|
||||
|
||||
uc.micro@^2.0.0, uc.micro@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-2.1.0.tgz#f8d3f7d0ec4c3dea35a7e3c8efa4cb8b45c9e7ee"
|
||||
integrity sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==
|
||||
|
||||
undici-types@~6.20.0:
|
||||
version "6.20.0"
|
||||
resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.20.0.tgz#8171bf22c1f588d1554d55bf204bc624af388433"
|
||||
@@ -1972,6 +2393,11 @@ vuetify@^3.6.12:
|
||||
resolved "https://registry.yarnpkg.com/vuetify/-/vuetify-3.7.7.tgz#f40e06abf81c426a29aee53e166fb7fcb05e98cd"
|
||||
integrity sha512-YqKnRo2+BZWQRzfxHVA/5reQo1eLvbS9Z6N+Lvaot/5lpdi7JWooMr/hWoCr7/QPBGRcXArHppqIB+hMfPlsXw==
|
||||
|
||||
w3c-keyname@^2.2.0:
|
||||
version "2.2.8"
|
||||
resolved "https://registry.yarnpkg.com/w3c-keyname/-/w3c-keyname-2.2.8.tgz#7b17c8c6883d4e8b86ac8aba79d39e880f8869c5"
|
||||
integrity sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==
|
||||
|
||||
webpack-virtual-modules@^0.6.2:
|
||||
version "0.6.2"
|
||||
resolved "https://registry.yarnpkg.com/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz#057faa9065c8acf48f24cb57ac0e77739ab9a7e8"
|
||||
|
||||
@@ -18,7 +18,7 @@ from django.utils.html import conditional_escape
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from modoboa.lib import u2u_decode
|
||||
from modoboa.lib.email_utils import Email, EmailAddress
|
||||
from modoboa.lib.email_utils import Email
|
||||
|
||||
from . import imapheader
|
||||
from .attachments import get_storage_path
|
||||
@@ -39,7 +39,7 @@ class ImapEmail(Email):
|
||||
("Subject", True),
|
||||
]
|
||||
|
||||
def __init__(self, request, *args, **kwargs):
|
||||
def __init__(self, request, *args, **kwargs) -> None:
|
||||
super().__init__(*args, **kwargs)
|
||||
self.request = request
|
||||
self.imapc = get_imapconnector(request)
|
||||
@@ -160,7 +160,9 @@ class ImapEmail(Email):
|
||||
@property
|
||||
def source(self):
|
||||
"""Retrieve email source."""
|
||||
return self.imapc.fetchmail(self.mbox, self.mailid, what="source")["BODY[]"]
|
||||
return self.imapc.fetchmail(
|
||||
self.mbox, self.mailid, readonly=True, what="source"
|
||||
)["BODY[]"]
|
||||
|
||||
def _find_content_charset(self, part):
|
||||
for pos, elem in enumerate(part["params"]):
|
||||
@@ -226,11 +228,11 @@ class ImapEmail(Email):
|
||||
class Modifier(ImapEmail):
|
||||
"""Message modifier."""
|
||||
|
||||
def __init__(self, form, request, *args, **kwargs):
|
||||
def __init__(self, request, *args, **kwargs):
|
||||
kwargs["dformat"] = request.user.parameters.get_value("editor")
|
||||
super().__init__(request, *args, **kwargs)
|
||||
self.form = form
|
||||
self.fetch_headers(raw_addresses=True)
|
||||
self._inject_textheader()
|
||||
getattr(self, f"_modify_{self.dformat}")()
|
||||
|
||||
def _modify_plain(self):
|
||||
@@ -255,32 +257,36 @@ class ReplyModifier(Modifier):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.textheader = f"{self.Form} {_('wrote:')}"
|
||||
if self.dformat == "html":
|
||||
self.textheader = f"<p>{self.textheader}</p>"
|
||||
if hasattr(self, "Message_ID"):
|
||||
self.form.fields["origmsgid"].initial = self.Message_ID
|
||||
if not hasattr(self, "Reply_To"):
|
||||
self.form.fields["to"].initial = self.original_From
|
||||
else:
|
||||
self.form.fields["to"].initial = self.original_ReplyTo
|
||||
if self.request.GET.get("all", "0") == "1": # reply-all
|
||||
self.form.fields["cc"].initial = ""
|
||||
toparse = self.To.split(",")
|
||||
if hasattr(self, "Cc"):
|
||||
toparse += self.Cc.split(",")
|
||||
for addr in toparse:
|
||||
tmp = EmailAddress(addr)
|
||||
if tmp.address and tmp.address == self.request.user.username:
|
||||
continue
|
||||
if self.form.fields["cc"].initial != "":
|
||||
self.form.fields["cc"].initial += ", "
|
||||
self.form.fields["cc"].initial += tmp.fulladdress
|
||||
# if hasattr(self, "Message_ID"):
|
||||
# self.form.fields["origmsgid"].initial = self.Message_ID
|
||||
# if not hasattr(self, "Reply_To"):
|
||||
# self.form.fields["to"].initial = self.original_From
|
||||
# else:
|
||||
# self.form.fields["to"].initial = self.original_ReplyTo
|
||||
# if self.request.GET.get("all", "0") == "1": # reply-all
|
||||
# self.form.fields["cc"].initial = ""
|
||||
# toparse = self.To.split(",")
|
||||
# if hasattr(self, "Cc"):
|
||||
# toparse += self.Cc.split(",")
|
||||
# for addr in toparse:
|
||||
# tmp = EmailAddress(addr)
|
||||
# if tmp.address and tmp.address == self.request.user.username:
|
||||
# continue
|
||||
# if self.form.fields["cc"].initial != "":
|
||||
# self.form.fields["cc"].initial += ", "
|
||||
# self.form.fields["cc"].initial += tmp.fulladdress
|
||||
m = re.match(r"re\s*:\s*.+", self.subject.lower())
|
||||
if m:
|
||||
self.form.fields["subject"].initial = self.subject
|
||||
if not m:
|
||||
self.subject = f"Re: {self.subject}"
|
||||
|
||||
def _inject_textheader(self):
|
||||
sender = self.From.get("name", self.From["address"])
|
||||
textheader = f"{sender} {_('wrote:')}"
|
||||
if self.dformat == "html":
|
||||
textheader = f"<p>{textheader}</p>"
|
||||
else:
|
||||
self.form.fields["subject"].initial = f"Re: {self.subject}"
|
||||
textheader = f"{textheader}\n"
|
||||
self.body = textheader + self.body
|
||||
|
||||
def _modify_plain(self):
|
||||
super()._modify_plain()
|
||||
|
||||
@@ -376,6 +376,7 @@ class IMAPconnector:
|
||||
name = self._encode_mbox_name(name)
|
||||
if readonly:
|
||||
self._cmd("EXAMINE", name)
|
||||
self.m.is_readonly = True
|
||||
else:
|
||||
self._cmd("SELECT", name)
|
||||
self.m.state = "SELECTED"
|
||||
|
||||
+145
-28
@@ -1,15 +1,139 @@
|
||||
from email.header import Header
|
||||
from email.mime.image import MIMEImage
|
||||
import os
|
||||
import pkg_resources
|
||||
import smtplib
|
||||
from typing import Optional, Tuple
|
||||
from urllib.parse import unquote, urlparse
|
||||
|
||||
import lxml
|
||||
|
||||
from django.conf import settings
|
||||
from django.core import mail
|
||||
from django.template.loader import render_to_string
|
||||
from django.core.mail import EmailMessage, EmailMultiAlternatives
|
||||
|
||||
from modoboa.lib.cryptutils import get_password
|
||||
from modoboa.core import models as core_models
|
||||
from modoboa.parameters import tools as param_tools
|
||||
|
||||
from . import get_imapconnector, clean_attachments
|
||||
from . import get_imapconnector
|
||||
|
||||
|
||||
def send_mail(request, form, posturl=None):
|
||||
def html2plaintext(content: str) -> str:
|
||||
"""HTML to plain text translation.
|
||||
|
||||
:param content: some HTML content
|
||||
"""
|
||||
if not content:
|
||||
return ""
|
||||
html = lxml.html.fromstring(content)
|
||||
plaintext = ""
|
||||
for ch in html.iter():
|
||||
p = None
|
||||
if ch.text is not None:
|
||||
p = ch.text.strip("\r\t\n")
|
||||
if ch.tag == "img":
|
||||
p = ch.get("alt")
|
||||
if p is None:
|
||||
continue
|
||||
plaintext += p + "\n"
|
||||
return plaintext
|
||||
|
||||
|
||||
def make_body_images_inline(body: str) -> Tuple[str, list]:
|
||||
"""Look for images inside the body and make them inline.
|
||||
|
||||
Before sending a message in HTML format, it is necessary to find
|
||||
all img tags contained in the body in order to rewrite them. For
|
||||
example, icons provided by CKeditor are stored on the server
|
||||
filesystem and not accessible from the outside. We must embark
|
||||
them as parts of the MIME message if we want recipients to
|
||||
display them correctly.
|
||||
|
||||
:param body: the HTML body to parse
|
||||
"""
|
||||
html = lxml.html.fromstring(body)
|
||||
parts = []
|
||||
for tag in html.iter("img"):
|
||||
src = tag.get("src")
|
||||
if src is None:
|
||||
continue
|
||||
o = urlparse(src)
|
||||
path = unquote(os.path.join(settings.BASE_DIR, o.path[1:]))
|
||||
if not os.path.exists(path):
|
||||
continue
|
||||
fname = os.path.basename(path)
|
||||
cid = f"{os.path.splitext(fname)[0]}@modoboa"
|
||||
tag.set("src", f"cid:{cid}")
|
||||
with open(path, "rb") as fp:
|
||||
part = MIMEImage(fp.read())
|
||||
part["Content-ID"] = f"<{cid}>"
|
||||
part.replace_header("Content-Type", f'{part["Content-Type"]}; name="{fname}"')
|
||||
part["Content-Disposition"] = "inline"
|
||||
parts.append(part)
|
||||
return lxml.html.tostring(html, encoding="unicode"), parts
|
||||
|
||||
|
||||
def html_msg(attributes: dict) -> EmailMultiAlternatives:
|
||||
"""Create a multipart message.
|
||||
|
||||
We attach two alternatives:
|
||||
* text/html
|
||||
* text/plain
|
||||
"""
|
||||
body = attributes.get("body")
|
||||
if body:
|
||||
tbody = html2plaintext(body)
|
||||
body, images = make_body_images_inline(body)
|
||||
else:
|
||||
tbody = ""
|
||||
images = []
|
||||
msg = EmailMultiAlternatives()
|
||||
msg.body = tbody
|
||||
msg.attach_alternative(body, "text/html")
|
||||
for img in images:
|
||||
msg.attach(img)
|
||||
return msg
|
||||
|
||||
|
||||
def plain_msg(attributes: dict) -> EmailMessage:
|
||||
"""Create a simple text message."""
|
||||
msg = EmailMessage()
|
||||
msg.body = attributes.get("body", "")
|
||||
return msg
|
||||
|
||||
|
||||
def format_sender_address(user: core_models.User, address: str) -> str:
|
||||
"""Format address before message is sent."""
|
||||
if user.first_name != "" or user.last_name != "":
|
||||
return f'"{Header(user.fullname, "utf8")}" <{address}>'
|
||||
return address
|
||||
|
||||
|
||||
def create_message(user: core_models.User, attributes: dict):
|
||||
headers = {
|
||||
"User-Agent": "Modoboa {}".format(
|
||||
pkg_resources.get_distribution("modoboa").version
|
||||
)
|
||||
}
|
||||
origmsgid = attributes.get("origmsgid")
|
||||
if origmsgid:
|
||||
headers.update({"References": origmsgid, "In-Reply-To": origmsgid})
|
||||
mode = user.parameters.get_value("editor")
|
||||
sender = format_sender_address(user, attributes["sender"])
|
||||
if mode == "html":
|
||||
msg = html_msg(attributes)
|
||||
else:
|
||||
msg = plain_msg(attributes)
|
||||
msg.from_email = sender
|
||||
msg.to = attributes["to"]
|
||||
msg.headers = headers
|
||||
for hdr in ["subject", "cc", "bcc"]:
|
||||
if hdr in attributes:
|
||||
setattr(msg, hdr, attributes[hdr])
|
||||
return msg
|
||||
|
||||
|
||||
def send_mail(request, attributes: dict) -> Tuple[bool, Optional[str]]:
|
||||
"""Email verification and sending.
|
||||
|
||||
If the form does not present any error, a new MIME message is
|
||||
@@ -20,47 +144,40 @@ def send_mail(request, form, posturl=None):
|
||||
:param posturl: the url to post the message form to
|
||||
:return: a 2-uple (True|False, HttpResponse)
|
||||
"""
|
||||
if not form.is_valid():
|
||||
editormode = request.user.parameters.get_value("editor")
|
||||
listing = render_to_string(
|
||||
"modoboa_webmail/compose.html",
|
||||
{
|
||||
"form": form,
|
||||
"noerrors": True,
|
||||
"body": form.cleaned_data.get("body", "").strip(),
|
||||
"posturl": posturl,
|
||||
},
|
||||
request,
|
||||
)
|
||||
return False, {"status": "ko", "listing": listing, "editor": editormode}
|
||||
|
||||
msg = form.to_msg(request)
|
||||
conf = dict(param_tools.get_global_parameters("modoboa_webmail"))
|
||||
msg = create_message(request.user, attributes)
|
||||
conf = dict(param_tools.get_global_parameters("webmail"))
|
||||
options = {"host": conf["smtp_server"], "port": conf["smtp_port"]}
|
||||
if conf["smtp_secured_mode"] == "ssl":
|
||||
options.update({"use_ssl": True})
|
||||
elif conf["smtp_secured_mode"] == "starttls":
|
||||
options.update({"use_tls": True})
|
||||
if conf["smtp_authentication"]:
|
||||
options.update(
|
||||
{"username": request.user.username, "password": get_password(request)}
|
||||
)
|
||||
if not settings.WEBMAIL_DEV_MODE:
|
||||
# TODO: oauth2 auth
|
||||
pass
|
||||
else:
|
||||
options.update(
|
||||
{
|
||||
"username": settings.WEBMAIL_DEV_USERNAME,
|
||||
"password": settings.WEBMAIL_DEV_PASSWORD,
|
||||
}
|
||||
)
|
||||
try:
|
||||
with mail.get_connection(**options) as connection:
|
||||
msg.connection = connection
|
||||
msg.send()
|
||||
except smtplib.SMTPResponseException as inst:
|
||||
return False, {"status": "ko", "error": inst.smtp_error}
|
||||
return False, str(inst.smtp_error)
|
||||
except smtplib.SMTPRecipientsRefused as inst:
|
||||
error = ", ".join(
|
||||
[f"{rcpt}: {error}" for rcpt, error in inst.recipients.items()]
|
||||
)
|
||||
return False, {"status": "ko", "error": error}
|
||||
return False, error
|
||||
|
||||
# Copy message to sent folder
|
||||
sentfolder = request.user.parameters.get_value("sent_folder")
|
||||
get_imapconnector(request).push_mail(sentfolder, msg.message())
|
||||
clean_attachments(request.session["compose_mail"]["attachments"])
|
||||
del request.session["compose_mail"]
|
||||
# clean_attachments(request.session["compose_mail"]["attachments"])
|
||||
# del request.session["compose_mail"]
|
||||
|
||||
return True, {}
|
||||
return True, None
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
from rest_framework import serializers
|
||||
|
||||
from modoboa.lib import email_utils
|
||||
from modoboa.webmail import constants
|
||||
from modoboa.webmail.lib import imapheader
|
||||
|
||||
@@ -144,3 +145,25 @@ class FlagSelectionSerializer(serializers.Serializer):
|
||||
|
||||
def validate_selection(self, value):
|
||||
return [item for item in value if item.isdigit()]
|
||||
|
||||
|
||||
class SendEmailSerializer(serializers.Serializer):
|
||||
|
||||
sender = serializers.EmailField()
|
||||
to = serializers.ListField(child=serializers.EmailField())
|
||||
cc = serializers.ListField(child=serializers.EmailField(), required=False)
|
||||
bcc = serializers.ListField(child=serializers.EmailField(), required=False)
|
||||
subject = serializers.CharField(required=False)
|
||||
body = serializers.CharField(required=False)
|
||||
|
||||
def validate_sender(self, value):
|
||||
return value
|
||||
|
||||
def validate_to(self, value):
|
||||
return email_utils.prepare_addresses(value, "envelope")
|
||||
|
||||
def validate_cc(self, value):
|
||||
return email_utils.prepare_addresses(value, "envelope")
|
||||
|
||||
def validate_bcc(self, value):
|
||||
return email_utils.prepare_addresses(value, "envelope")
|
||||
|
||||
@@ -31,6 +31,8 @@ class UserEmailViewSet(viewsets.GenericViewSet):
|
||||
return serializers.MoveSelectionSerializer
|
||||
if self.action == "flag":
|
||||
return serializers.FlagSelectionSerializer
|
||||
if self.action == "send":
|
||||
return serializers.SendEmailSerializer
|
||||
return serializers.EmailSerializer
|
||||
|
||||
def list(self, request):
|
||||
@@ -118,16 +120,38 @@ class UserEmailViewSet(viewsets.GenericViewSet):
|
||||
mailid = request.GET.get("mailid")
|
||||
if not mailbox or not mailid:
|
||||
raise Http404
|
||||
dformat = self.request.GET.get("dformat", "plain")
|
||||
dformat = request.user.parameters.get_value("displaymode")
|
||||
context = self.request.GET.get("context")
|
||||
if context and context in ["reply", "forward"]:
|
||||
modclass = getattr(lib, f"{context.capitalize()}Modifier")
|
||||
email = modclass(
|
||||
request,
|
||||
f"{mailbox}:{mailid}",
|
||||
dformat=dformat,
|
||||
links=request.GET.get("links", "0") == "1",
|
||||
)
|
||||
else:
|
||||
email = lib.ImapEmail(
|
||||
request,
|
||||
f"{mailbox}:{mailid}",
|
||||
dformat=dformat,
|
||||
links=request.GET.get("links", "0") == "1",
|
||||
)
|
||||
email.fetch_headers()
|
||||
serializer = serializers.EmailSerializer(email)
|
||||
return response.Response(serializer.data)
|
||||
|
||||
@action(methods=["get"], detail=False)
|
||||
def source(self, request):
|
||||
mailbox = request.GET.get("mailbox", "INBOX")
|
||||
mailid = request.GET.get("mailid")
|
||||
if not mailbox or not mailid:
|
||||
raise Http404
|
||||
email = lib.ImapEmail(
|
||||
request,
|
||||
f"{mailbox}:{mailid}",
|
||||
dformat=dformat,
|
||||
links=request.GET.get("links", "0") == "1",
|
||||
)
|
||||
email.fetch_headers()
|
||||
serializer = serializers.EmailSerializer(email)
|
||||
return response.Response(serializer.data)
|
||||
return response.Response({"source": email.source})
|
||||
|
||||
@action(methods=["get"], detail=False)
|
||||
def attachment(self, request):
|
||||
@@ -145,3 +169,12 @@ class UserEmailViewSet(viewsets.GenericViewSet):
|
||||
if int(partdef["size"]) < 200:
|
||||
resp["Content-Length"] = partdef["size"]
|
||||
return resp
|
||||
|
||||
@action(methods=["post"], detail=False)
|
||||
def send(self, request):
|
||||
serializer = self.get_serializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
status, error = lib.send_mail(request, serializer.validated_data)
|
||||
if status:
|
||||
return response.Response(status=204)
|
||||
return response.Response({"error": error}, status=400)
|
||||
|
||||
Reference in New Issue
Block a user