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:
Chris 2026-05-07 16:16:50 +02:00 committed by GitHub
commit 452ebe9c80
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 138 additions and 9 deletions

View file

@ -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

View file

@ -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 ---

View file

@ -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 }), {

View file

@ -1 +0,0 @@
test github action single container

View file

@ -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'),

View file

@ -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 },

View file

@ -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,

View 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>

View file

@ -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