From f79d8224c0dd635327031b05b1d466ae9b69a5d0 Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Sat, 14 Feb 2026 10:20:52 +0100 Subject: [PATCH 1/5] Webmail: save message as draft --- docker-compose.yml | 2 - frontend/src/api/webmail.js | 14 ++- .../components/webmail/ComposeEmailForm.vue | 100 +++++++++++++----- frontend/src/views/webmail/EmailView.vue | 16 +++ modoboa/lib/email_utils.py | 6 +- modoboa/webmail/lib/attachments.py | 56 ++++++++-- modoboa/webmail/lib/imapemail.py | 15 ++- modoboa/webmail/lib/imaputils.py | 10 +- modoboa/webmail/lib/utils.py | 2 +- modoboa/webmail/serializers.py | 47 ++++++-- modoboa/webmail/tests/test_viewsets.py | 33 ++++++ modoboa/webmail/viewsets.py | 57 +++++++--- 12 files changed, 290 insertions(+), 68 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 721a41ebe..fbd6168a7 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -87,8 +87,6 @@ services: - redis volumes: - .:/code - networks: - - default api-unsecured: container_name: modoboa_api_unsecured diff --git a/frontend/src/api/webmail.js b/frontend/src/api/webmail.js index 8f1191fa4..d4f3ab1fe 100644 --- a/frontend/src/api/webmail.js +++ b/frontend/src/api/webmail.js @@ -108,8 +108,12 @@ export default { getComposeSession(uid) { return repository.get(`/webmail/compose-sessions/${uid}/`) }, - createComposeSession() { - return repository.post('/webmail/compose-sessions/') + createComposeSession(fromDraftMessageUid) { + const data = {} + if (fromDraftMessageUid) { + data.from_draft_message = fromDraftMessageUid + } + return repository.post('/webmail/compose-sessions/', data) }, getAllowedSenders() { return repository.get('/webmail/compose-sessions/allowed_senders/') @@ -123,6 +127,12 @@ export default { } ) }, + saveComposeSession(sessionUid, data) { + return repository.post( + `/webmail/compose-sessions/${sessionUid}/save/`, + data + ) + }, removeAttachment(sessionUid, name) { return repository.delete( `/webmail/compose-sessions/${sessionUid}/attachments/${name}/` diff --git a/frontend/src/components/webmail/ComposeEmailForm.vue b/frontend/src/components/webmail/ComposeEmailForm.vue index 19b4e6a35..cc483b04f 100644 --- a/frontend/src/components/webmail/ComposeEmailForm.vue +++ b/frontend/src/components/webmail/ComposeEmailForm.vue @@ -40,6 +40,14 @@ :text="$gettext('Attachments') + ` (${attachmentCount})`" @click="openAttachmentsDialog" /> +
@@ -266,33 +274,43 @@ const initForm = () => { } } +const prepareMessage = () => { + const result = { ...form.value } + + if (result.to?.length) { + const to = [] + for (const rcpt of result.to) { + to.push(typeof rcpt === 'string' ? rcpt : rcpt.emails[0].address) + } + result.to = to + } + if (result.cc?.length) { + const cc = [] + for (const rcpt of result.cc) { + cc.push(typeof rcpt === 'string' ? rcpt : rcpt.emails[0].address) + } + result.cc = cc + } + if (result.bcc?.length) { + const bcc = [] + for (const rcpt of result.bcc) { + bcc.push(typeof rcpt === 'string' ? rcpt : rcpt.emails[0].address) + } + result.bcc = bcc + } + if (route.query.mailid) { + result.mailid = route.query.mailid + } + return result +} + const submit = async (reload) => { const { valid } = await formRef.value.validate() if (!valid) { return } working.value = true - const body = { ...form.value } - - const to = [] - for (const rcpt of body.to) { - to.push(typeof rcpt === 'string' ? rcpt : rcpt.emails[0].address) - } - body.to = to - if (body.cc?.length) { - const cc = [] - for (const rcpt of body.cc) { - cc.push(typeof rcpt === 'string' ? rcpt : rcpt.emails[0].address) - } - body.cc = cc - } - if (body.bcc?.length) { - const bcc = [] - for (const rcpt of body.bcc) { - bcc.push(typeof rcpt === 'string' ? rcpt : rcpt.emails[0].address) - } - body.bcc = bcc - } + const body = prepareMessage() try { await api.sendEmailFromComposeSession(route.query.uid, body) router.push({ name: 'MailboxView' }) @@ -337,8 +355,29 @@ const lookForContacts = debounce(async (search) => { } }, 500) -const initialize = (body) => { - if (body.signature) { +const initialize = async (body) => { + if (route.query.mailid) { + // Load draft + const draft = await api.getEmailContent('Drafts', route.query.mailid) + form.value.sender = draft.data.from_address.address + if (draft.data.to?.length) { + form.value.to = draft.data.to.map((rcpt) => rcpt.address) + } + if (draft.data.cc?.length) { + form.value.cc = draft.data.cc.map((rcpt) => rcpt.address) + showCcField.value = true + } + if (draft.data.bcc?.length) { + form.value.bcc = draft.data.bcc.map((rcpt) => rcpt.address) + showBccField.value = true + } + if (draft.data.subject) { + form.value.subject = draft.data.subject + } + if (draft.data.body) { + form.value.body = draft.data.body + } + } else if (body.signature) { if (form.value.body) { form.value.body += body.signature } else { @@ -363,6 +402,18 @@ const closeSchedulingForm = () => { showSchedulingForm.value = false } +const saveDraft = async () => { + working.value = true + const body = prepareMessage() + try { + await api.saveComposeSession(route.query.uid, body) + } catch (error) { + console.log(error) + } finally { + working.value = false + } +} + const getItemTitle = (item) => { if (typeof item === 'string') { return item @@ -379,8 +430,9 @@ watch( ) if (!route.query.uid) { - api.createComposeSession().then((resp) => { + api.createComposeSession(route.query.mailid).then((resp) => { const query = { ...route.query, uid: resp.data.uid } + attachmentCount.value = resp.data.attachments?.length || 0 router.push({ name: route.name, query }) initialize(resp.data) }) diff --git a/frontend/src/views/webmail/EmailView.vue b/frontend/src/views/webmail/EmailView.vue index e516ab6c2..0281bcc3a 100644 --- a/frontend/src/views/webmail/EmailView.vue +++ b/frontend/src/views/webmail/EmailView.vue @@ -53,6 +53,15 @@ @click="markEmailAsNotJunk" > + + @@ -318,6 +327,13 @@ const replyToEmail = (all) => { const forwardEmail = () => { router.push({ name: 'ForwardEmailView', query: route.query }) } + +const editDraft = () => { + router.push({ + name: 'ComposeEmailView', + query: { mailid: route.query.mailid }, + }) +}