More improvements

This commit is contained in:
Antoine Nguyen
2025-05-01 18:00:00 +02:00
parent ce2569f39d
commit 6ee4ea1662
17 changed files with 1159 additions and 74 deletions
+1
View File
@@ -29,6 +29,7 @@ services:
- 8000:8000
env_file:
- test_project/oidc_key
- test_project/envfile
environment:
DB: "SQLITE"
DEBUG: "True"
+5
View File
@@ -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",
+10
View File
@@ -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>
+16
View File
@@ -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>
+49 -9
View File
@@ -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
View File
@@ -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"
+35 -29
View File
@@ -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()
+1
View File
@@ -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
View File
@@ -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
+23
View File
@@ -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")
+39 -6
View File
@@ -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)