Skip to content

Commit 452ebe9

Browse files
Merge pull request #366 from ChrispyBacon-dev/unstable
Display name in Email From Header - Fixed and added Profile Setting i…
2 parents 47b6f88 + 71e4c73 commit 452ebe9

9 files changed

Lines changed: 138 additions & 9 deletions

File tree

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,15 @@
33
All notable changes to this project will be documented in this file.
44

55

6+
## [v3.1.2] - 2026-05-07
7+
8+
### Added
9+
- **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.
10+
11+
### Fixed
12+
- **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).
13+
14+
615
## [v3.1.1] - 2026-04-24
716

817
### Added

dockflare/app/config.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ def _get_int_env(name, default, minimum=None):
3535
return default
3636

3737
# --- DockFlare Version ---
38-
APP_VERSION = "v3.1.1"
38+
APP_VERSION = "v3.1.2"
3939
# --- web: https://dockflare.app ---
4040
# --- github: https://github.com/ChrispyBacon-dev/DockFlare ---
4141

dockflare/app/core/worker_templates/outbound_worker.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,10 +72,12 @@ export default {
7272
}
7373

7474
try {
75+
const fromMatch = typeof body.from === 'string' ? body.from.match(/<([^>]+)>/) : null;
76+
const fromEnvelope = fromMatch ? fromMatch[1] : (body.from || '').trim();
7577
for (const recipient of toList) {
7678
const addrMatch = typeof recipient === 'string' ? recipient.match(/<([^>]+)>/) : null;
7779
const toAddress = addrMatch ? addrMatch[1] : (typeof recipient === 'string' ? recipient.trim() : recipient);
78-
const message = new EmailMessage(body.from, toAddress, mimeMessage);
80+
const message = new EmailMessage(fromEnvelope, toAddress, mimeMessage);
7981
await env.SEND_EMAIL.send(message);
8082
}
8183
return new Response(JSON.stringify({ success: true, message_id: body.messageId }), {

dockflare/trigger-test.txt

Lines changed: 0 additions & 1 deletion
This file was deleted.

mail-manager/app/api/routes.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import base64
2+
import email.utils
23
import json
34
import logging
45
import os
@@ -299,12 +300,12 @@ def get_mailbox_preferences(address):
299300
if not _check_mailbox_access(address):
300301
return jsonify({"error": "forbidden"}), 403
301302
db = get_db()
302-
cur = db.execute("SELECT notification_preview FROM mailboxes WHERE address=?", (address,))
303+
cur = db.execute("SELECT notification_preview, display_name FROM mailboxes WHERE address=?", (address,))
303304
row = cur.fetchone()
304305
if not row:
305306
return jsonify({"error": "not found"}), 404
306307
preview = row['notification_preview'] if row['notification_preview'] is not None else 1
307-
return jsonify({"notification_preview": bool(preview)})
308+
return jsonify({"notification_preview": bool(preview), "display_name": row['display_name'] or ''})
308309

309310

310311
@api_bp.route('/mailboxes/<address>/preferences', methods=['PATCH'])
@@ -319,7 +320,9 @@ def patch_mailbox_preferences(address):
319320
"UPDATE mailboxes SET notification_preview=? WHERE address=?",
320321
(int(bool(data['notification_preview'])), address),
321322
)
322-
db.commit()
323+
if 'display_name' in data:
324+
db.execute("UPDATE mailboxes SET display_name=? WHERE address=?", (data['display_name'], address))
325+
db.commit()
323326
return jsonify({"status": "updated"})
324327

325328

@@ -789,6 +792,9 @@ def _dispatch_send(address, data, effective_from=None, via_alias=None):
789792
msg_id = f"<{uuid.uuid4()}@{address.split('@')[1]}>"
790793

791794
db = get_db()
795+
mb_row = db.execute("SELECT display_name FROM mailboxes WHERE address=?", (address,)).fetchone()
796+
display_name = (mb_row['display_name'] if mb_row else '') or ''
797+
from_formatted = email.utils.formataddr((display_name, from_address)) if display_name else from_address
792798

793799
local_recipients = []
794800
external_recipients = []
@@ -819,7 +825,7 @@ def _dispatch_send(address, data, effective_from=None, via_alias=None):
819825

820826
if external_recipients:
821827
worker_payload = {
822-
"from": from_address,
828+
"from": from_formatted,
823829
"to": external_recipients,
824830
"cc": data.get('cc'),
825831
"bcc": data.get('bcc'),

webmail/src/components/settings/SettingsNav.vue

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
<script setup lang="ts">
2-
import { Bell, Palette, AtSign, Mail, Shield, Info, HelpCircle } from 'lucide-vue-next'
2+
import { Bell, Palette, AtSign, Mail, Shield, Info, HelpCircle, User } from 'lucide-vue-next'
33
import { useMailStore } from '@/stores/mail'
44
55
const store = useMailStore()
66
77
const categories = [
8+
{ key: 'profile', label: 'Profile', icon: User },
89
{ key: 'notifications', label: 'Notifications', icon: Bell },
910
{ key: 'appearance', label: 'Appearance', icon: Palette },
1011
{ key: 'aliases', label: 'Aliases', icon: AtSign },

webmail/src/components/settings/SettingsPanel.vue

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,12 @@ import SettingsAutoResponder from './sections/SettingsAutoResponder.vue'
1111
import SettingsSecurity from './sections/SettingsSecurity.vue'
1212
import SettingsAbout from './sections/SettingsAbout.vue'
1313
import SettingsHelp from './sections/SettingsHelp.vue'
14+
import SettingsProfile from './sections/SettingsProfile.vue'
1415
1516
const store = useMailStore()
1617
1718
const sectionMap: Record<string, any> = {
19+
profile: SettingsProfile,
1820
notifications: SettingsNotifications,
1921
appearance: SettingsAppearance,
2022
aliases: SettingsAliases,
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
<script setup lang="ts">
2+
import { ref, watch } from 'vue'
3+
import { useMailStore } from '@/stores/mail'
4+
import { mailApi } from '@/api/mail'
5+
6+
const store = useMailStore()
7+
8+
const displayName = ref('')
9+
const loading = ref(false)
10+
const saving = ref(false)
11+
const error = ref('')
12+
const success = ref('')
13+
14+
async function load(address: string) {
15+
loading.value = true
16+
error.value = ''
17+
try {
18+
const res = await mailApi.getMailboxPreferences(address)
19+
displayName.value = res.data.display_name || ''
20+
} catch {
21+
error.value = 'Failed to load profile.'
22+
} finally {
23+
loading.value = false
24+
}
25+
}
26+
27+
watch(() => store.currentMailbox, (addr) => { if (addr) load(addr) }, { immediate: true })
28+
29+
async function save() {
30+
if (!store.currentMailbox) return
31+
saving.value = true
32+
error.value = ''
33+
success.value = ''
34+
try {
35+
await mailApi.updateMailboxPreferences(store.currentMailbox, { display_name: displayName.value })
36+
const mb = store.mailboxes.find(m => m.address === store.currentMailbox)
37+
if (mb) mb.display_name = displayName.value
38+
success.value = 'Display name updated.'
39+
} catch {
40+
error.value = 'Failed to save.'
41+
} finally {
42+
saving.value = false
43+
}
44+
}
45+
</script>
46+
47+
<template>
48+
<div class="space-y-6">
49+
<div>
50+
<h2 class="text-base font-semibold">Profile</h2>
51+
<p class="text-sm text-muted-foreground mt-1">Your display name appears in the From field of emails you send.</p>
52+
</div>
53+
54+
<div v-if="!store.currentMailbox" class="text-sm text-muted-foreground">No mailbox selected.</div>
55+
56+
<template v-else>
57+
<div v-if="loading" class="text-sm text-muted-foreground">Loading…</div>
58+
59+
<template v-else>
60+
<div class="rounded-lg border p-4 space-y-4">
61+
<div class="space-y-1.5">
62+
<label class="text-sm font-medium">Email address</label>
63+
<p class="text-sm font-mono text-muted-foreground">{{ store.currentMailbox }}</p>
64+
</div>
65+
66+
<div class="space-y-1.5">
67+
<label class="text-sm font-medium" for="display-name-input">Display name</label>
68+
<input
69+
id="display-name-input"
70+
v-model="displayName"
71+
type="text"
72+
placeholder="Your Name"
73+
maxlength="100"
74+
class="w-full rounded-md border bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring"
75+
@keydown.enter="save"
76+
/>
77+
<p class="text-xs text-muted-foreground">Sent as: {{ displayName ? `${displayName} <${store.currentMailbox}>` : store.currentMailbox }}</p>
78+
</div>
79+
80+
<p v-if="error" class="text-xs text-destructive">{{ error }}</p>
81+
<p v-if="success" class="text-xs text-green-600 dark:text-green-400">{{ success }}</p>
82+
83+
<button
84+
:disabled="saving"
85+
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"
86+
@click="save"
87+
>{{ saving ? 'Saving…' : 'Save' }}</button>
88+
</div>
89+
</template>
90+
</template>
91+
</div>
92+
</template>
93+
94+
<style scoped>
95+
.dark input {
96+
background-color: hsl(var(--muted)) !important;
97+
color: hsl(var(--foreground));
98+
}
99+
.dark input::placeholder {
100+
color: hsl(var(--muted-foreground)); opacity: 1;
101+
}
102+
</style>

webmail/src/composables/useMail.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,15 @@ export function useMail() {
1515
const decoded = authStore.decodeToken()
1616
if (decoded?.role === 'user') {
1717
const addresses: string[] = decoded.mailboxes || []
18-
store.mailboxes = addresses.map((addr: string) => ({ address: addr, display_name: addr }))
18+
store.mailboxes = addresses.map((addr: string) => ({ address: addr, display_name: '' }))
19+
const prefs = await Promise.allSettled(
20+
addresses.map(addr => mailApi.getMailboxPreferences(addr))
21+
)
22+
prefs.forEach((result, i) => {
23+
if (result.status === 'fulfilled') {
24+
store.mailboxes[i].display_name = result.value.data.display_name || ''
25+
}
26+
})
1927
} else {
2028
const res = await mailApi.getMailboxes()
2129
store.mailboxes = res.data

0 commit comments

Comments
 (0)