mirror of
https://github.com/ChrispyBacon-dev/DockFlare.git
synced 2026-07-09 17:19:34 +00:00
Merge pull request #366 from ChrispyBacon-dev/unstable
Display name in Email From Header - Fixed and added Profile Setting i…
This commit is contained in:
commit
452ebe9c80
9 changed files with 138 additions and 9 deletions
|
|
@ -3,6 +3,15 @@
|
|||
All notable changes to this project will be documented in this file.
|
||||
|
||||
|
||||
## [v3.1.2] - 2026-05-07
|
||||
|
||||
### Added
|
||||
- **Webmail - Profile Settings:** New Profile section in Settings lets users update their display name. The current formatted From address (`Name <email>`) is previewed live. Changes persist immediately and reflect across the session without re-login.
|
||||
|
||||
### Fixed
|
||||
- **Outbound - Display Name in From Header:** The display name set during mailbox creation was stored but never applied when sending. Outbound emails now correctly use `Display Name <address>` format in the `From` header. Reported by the community in [#363](https://github.com/ChrispyBacon-dev/DockFlare/issues/363).
|
||||
|
||||
|
||||
## [v3.1.1] - 2026-04-24
|
||||
|
||||
### Added
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ def _get_int_env(name, default, minimum=None):
|
|||
return default
|
||||
|
||||
# --- DockFlare Version ---
|
||||
APP_VERSION = "v3.1.1"
|
||||
APP_VERSION = "v3.1.2"
|
||||
# --- web: https://dockflare.app ---
|
||||
# --- github: https://github.com/ChrispyBacon-dev/DockFlare ---
|
||||
|
||||
|
|
|
|||
|
|
@ -72,10 +72,12 @@ export default {
|
|||
}
|
||||
|
||||
try {
|
||||
const fromMatch = typeof body.from === 'string' ? body.from.match(/<([^>]+)>/) : null;
|
||||
const fromEnvelope = fromMatch ? fromMatch[1] : (body.from || '').trim();
|
||||
for (const recipient of toList) {
|
||||
const addrMatch = typeof recipient === 'string' ? recipient.match(/<([^>]+)>/) : null;
|
||||
const toAddress = addrMatch ? addrMatch[1] : (typeof recipient === 'string' ? recipient.trim() : recipient);
|
||||
const message = new EmailMessage(body.from, toAddress, mimeMessage);
|
||||
const message = new EmailMessage(fromEnvelope, toAddress, mimeMessage);
|
||||
await env.SEND_EMAIL.send(message);
|
||||
}
|
||||
return new Response(JSON.stringify({ success: true, message_id: body.messageId }), {
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
test github action single container
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
import base64
|
||||
import email.utils
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
|
|
@ -299,12 +300,12 @@ def get_mailbox_preferences(address):
|
|||
if not _check_mailbox_access(address):
|
||||
return jsonify({"error": "forbidden"}), 403
|
||||
db = get_db()
|
||||
cur = db.execute("SELECT notification_preview FROM mailboxes WHERE address=?", (address,))
|
||||
cur = db.execute("SELECT notification_preview, display_name FROM mailboxes WHERE address=?", (address,))
|
||||
row = cur.fetchone()
|
||||
if not row:
|
||||
return jsonify({"error": "not found"}), 404
|
||||
preview = row['notification_preview'] if row['notification_preview'] is not None else 1
|
||||
return jsonify({"notification_preview": bool(preview)})
|
||||
return jsonify({"notification_preview": bool(preview), "display_name": row['display_name'] or ''})
|
||||
|
||||
|
||||
@api_bp.route('/mailboxes/<address>/preferences', methods=['PATCH'])
|
||||
|
|
@ -319,7 +320,9 @@ def patch_mailbox_preferences(address):
|
|||
"UPDATE mailboxes SET notification_preview=? WHERE address=?",
|
||||
(int(bool(data['notification_preview'])), address),
|
||||
)
|
||||
db.commit()
|
||||
if 'display_name' in data:
|
||||
db.execute("UPDATE mailboxes SET display_name=? WHERE address=?", (data['display_name'], address))
|
||||
db.commit()
|
||||
return jsonify({"status": "updated"})
|
||||
|
||||
|
||||
|
|
@ -789,6 +792,9 @@ def _dispatch_send(address, data, effective_from=None, via_alias=None):
|
|||
msg_id = f"<{uuid.uuid4()}@{address.split('@')[1]}>"
|
||||
|
||||
db = get_db()
|
||||
mb_row = db.execute("SELECT display_name FROM mailboxes WHERE address=?", (address,)).fetchone()
|
||||
display_name = (mb_row['display_name'] if mb_row else '') or ''
|
||||
from_formatted = email.utils.formataddr((display_name, from_address)) if display_name else from_address
|
||||
|
||||
local_recipients = []
|
||||
external_recipients = []
|
||||
|
|
@ -819,7 +825,7 @@ def _dispatch_send(address, data, effective_from=None, via_alias=None):
|
|||
|
||||
if external_recipients:
|
||||
worker_payload = {
|
||||
"from": from_address,
|
||||
"from": from_formatted,
|
||||
"to": external_recipients,
|
||||
"cc": data.get('cc'),
|
||||
"bcc": data.get('bcc'),
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
<script setup lang="ts">
|
||||
import { Bell, Palette, AtSign, Mail, Shield, Info, HelpCircle } from 'lucide-vue-next'
|
||||
import { Bell, Palette, AtSign, Mail, Shield, Info, HelpCircle, User } from 'lucide-vue-next'
|
||||
import { useMailStore } from '@/stores/mail'
|
||||
|
||||
const store = useMailStore()
|
||||
|
||||
const categories = [
|
||||
{ key: 'profile', label: 'Profile', icon: User },
|
||||
{ key: 'notifications', label: 'Notifications', icon: Bell },
|
||||
{ key: 'appearance', label: 'Appearance', icon: Palette },
|
||||
{ key: 'aliases', label: 'Aliases', icon: AtSign },
|
||||
|
|
|
|||
|
|
@ -11,10 +11,12 @@ import SettingsAutoResponder from './sections/SettingsAutoResponder.vue'
|
|||
import SettingsSecurity from './sections/SettingsSecurity.vue'
|
||||
import SettingsAbout from './sections/SettingsAbout.vue'
|
||||
import SettingsHelp from './sections/SettingsHelp.vue'
|
||||
import SettingsProfile from './sections/SettingsProfile.vue'
|
||||
|
||||
const store = useMailStore()
|
||||
|
||||
const sectionMap: Record<string, any> = {
|
||||
profile: SettingsProfile,
|
||||
notifications: SettingsNotifications,
|
||||
appearance: SettingsAppearance,
|
||||
aliases: SettingsAliases,
|
||||
|
|
|
|||
102
webmail/src/components/settings/sections/SettingsProfile.vue
Normal file
102
webmail/src/components/settings/sections/SettingsProfile.vue
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue'
|
||||
import { useMailStore } from '@/stores/mail'
|
||||
import { mailApi } from '@/api/mail'
|
||||
|
||||
const store = useMailStore()
|
||||
|
||||
const displayName = ref('')
|
||||
const loading = ref(false)
|
||||
const saving = ref(false)
|
||||
const error = ref('')
|
||||
const success = ref('')
|
||||
|
||||
async function load(address: string) {
|
||||
loading.value = true
|
||||
error.value = ''
|
||||
try {
|
||||
const res = await mailApi.getMailboxPreferences(address)
|
||||
displayName.value = res.data.display_name || ''
|
||||
} catch {
|
||||
error.value = 'Failed to load profile.'
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
watch(() => store.currentMailbox, (addr) => { if (addr) load(addr) }, { immediate: true })
|
||||
|
||||
async function save() {
|
||||
if (!store.currentMailbox) return
|
||||
saving.value = true
|
||||
error.value = ''
|
||||
success.value = ''
|
||||
try {
|
||||
await mailApi.updateMailboxPreferences(store.currentMailbox, { display_name: displayName.value })
|
||||
const mb = store.mailboxes.find(m => m.address === store.currentMailbox)
|
||||
if (mb) mb.display_name = displayName.value
|
||||
success.value = 'Display name updated.'
|
||||
} catch {
|
||||
error.value = 'Failed to save.'
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="space-y-6">
|
||||
<div>
|
||||
<h2 class="text-base font-semibold">Profile</h2>
|
||||
<p class="text-sm text-muted-foreground mt-1">Your display name appears in the From field of emails you send.</p>
|
||||
</div>
|
||||
|
||||
<div v-if="!store.currentMailbox" class="text-sm text-muted-foreground">No mailbox selected.</div>
|
||||
|
||||
<template v-else>
|
||||
<div v-if="loading" class="text-sm text-muted-foreground">Loading…</div>
|
||||
|
||||
<template v-else>
|
||||
<div class="rounded-lg border p-4 space-y-4">
|
||||
<div class="space-y-1.5">
|
||||
<label class="text-sm font-medium">Email address</label>
|
||||
<p class="text-sm font-mono text-muted-foreground">{{ store.currentMailbox }}</p>
|
||||
</div>
|
||||
|
||||
<div class="space-y-1.5">
|
||||
<label class="text-sm font-medium" for="display-name-input">Display name</label>
|
||||
<input
|
||||
id="display-name-input"
|
||||
v-model="displayName"
|
||||
type="text"
|
||||
placeholder="Your Name"
|
||||
maxlength="100"
|
||||
class="w-full rounded-md border bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring"
|
||||
@keydown.enter="save"
|
||||
/>
|
||||
<p class="text-xs text-muted-foreground">Sent as: {{ displayName ? `${displayName} <${store.currentMailbox}>` : store.currentMailbox }}</p>
|
||||
</div>
|
||||
|
||||
<p v-if="error" class="text-xs text-destructive">{{ error }}</p>
|
||||
<p v-if="success" class="text-xs text-green-600 dark:text-green-400">{{ success }}</p>
|
||||
|
||||
<button
|
||||
:disabled="saving"
|
||||
class="inline-flex items-center justify-center rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 transition-colors disabled:opacity-50"
|
||||
@click="save"
|
||||
>{{ saving ? 'Saving…' : 'Save' }}</button>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.dark input {
|
||||
background-color: hsl(var(--muted)) !important;
|
||||
color: hsl(var(--foreground));
|
||||
}
|
||||
.dark input::placeholder {
|
||||
color: hsl(var(--muted-foreground)); opacity: 1;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -15,7 +15,15 @@ export function useMail() {
|
|||
const decoded = authStore.decodeToken()
|
||||
if (decoded?.role === 'user') {
|
||||
const addresses: string[] = decoded.mailboxes || []
|
||||
store.mailboxes = addresses.map((addr: string) => ({ address: addr, display_name: addr }))
|
||||
store.mailboxes = addresses.map((addr: string) => ({ address: addr, display_name: '' }))
|
||||
const prefs = await Promise.allSettled(
|
||||
addresses.map(addr => mailApi.getMailboxPreferences(addr))
|
||||
)
|
||||
prefs.forEach((result, i) => {
|
||||
if (result.status === 'fulfilled') {
|
||||
store.mailboxes[i].display_name = result.value.data.display_name || ''
|
||||
}
|
||||
})
|
||||
} else {
|
||||
const res = await mailApi.getMailboxes()
|
||||
store.mailboxes = res.data
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue