aboutsummaryrefslogtreecommitdiff
path: root/templates
diff options
context:
space:
mode:
authorBobby <[email protected]>2026-03-08 08:11:41 +0530
committerBobby <[email protected]>2026-03-08 08:11:41 +0530
commitb2a231280ce3265d20cdc5f317ae1bcc5eb59924 (patch)
tree90eb1a5f5409025db47097e2e083361f8fa52555 /templates
parent662dd2069dc8590e8b54823a33726464cf10c4e7 (diff)
downloaddove-b2a231280ce3265d20cdc5f317ae1bcc5eb59924.tar.xz
dove-b2a231280ce3265d20cdc5f317ae1bcc5eb59924.zip
Add webmail email management templates and storage utilities
- Implemented email listing template with read/unread and star functionality. - Created empty state template for webmail when no emails are present. - Developed folder navigation template for managing email folders. - Added email preview template for displaying selected email details. - Introduced storage utilities for managing email files, including creation, reading, moving, and deletion. - Defined constants for storage paths and error messages related to file operations.
Diffstat (limited to 'templates')
-rw-r--r--templates/dashboard/htmx/overview.htmx.django1
-rw-r--r--templates/domains/htmx/domains.htmx.django1
-rw-r--r--templates/domains/htmx/editdomain.htmx.django1
-rw-r--r--templates/domains/htmx/edittld.htmx.django1
-rw-r--r--templates/domains/htmx/index.htmx.django1
-rw-r--r--templates/domains/htmx/newdomain.htmx.django3
-rw-r--r--templates/domains/htmx/newtld.htmx.django3
-rw-r--r--templates/domains/htmx/tlds.htmx.django1
-rw-r--r--templates/layouts/dashboard.django2
-rw-r--r--templates/mail/htmx/editmailbox.htmx.django1
-rw-r--r--templates/mail/htmx/edituser.htmx.django1
-rw-r--r--templates/mail/htmx/index.htmx.django1
-rw-r--r--templates/mail/htmx/mailbox.htmx.django17
-rw-r--r--templates/mail/htmx/mailboxes.htmx.django4
-rw-r--r--templates/mail/htmx/newmailbox.htmx.django3
-rw-r--r--templates/mail/htmx/newuser.htmx.django3
-rw-r--r--templates/mail/htmx/users.htmx.django1
-rw-r--r--templates/mail/htmx/webmail.htmx.django229
-rw-r--r--templates/mail/mailbox.django16
-rw-r--r--templates/mail/webmail.django5
-rw-r--r--templates/mail/webmail/compose.django309
-rw-r--r--templates/mail/webmail/emails.django46
-rw-r--r--templates/mail/webmail/empty.django9
-rw-r--r--templates/mail/webmail/folders.django73
-rw-r--r--templates/mail/webmail/preview.django102
-rw-r--r--templates/partials/sidebar.django7
26 files changed, 785 insertions, 56 deletions
diff --git a/templates/dashboard/htmx/overview.htmx.django b/templates/dashboard/htmx/overview.htmx.django
index c5a7b4d..8e1529e 100644
--- a/templates/dashboard/htmx/overview.htmx.django
+++ b/templates/dashboard/htmx/overview.htmx.django
@@ -1,4 +1,3 @@
-<h1 id="page-title" class="text-sm font-medium text-zinc-100" hx-swap-oob="true">{{ PageTitle }}</h1>
<div class="slide-up space-y-8">
<div class="grid grid-cols-3 gap-5">
<div class="glass rounded-xl p-5 glow-border">
diff --git a/templates/domains/htmx/domains.htmx.django b/templates/domains/htmx/domains.htmx.django
index 11843f6..4e09799 100644
--- a/templates/domains/htmx/domains.htmx.django
+++ b/templates/domains/htmx/domains.htmx.django
@@ -1,4 +1,3 @@
-<h1 id="page-title" class="text-sm font-medium text-zinc-100" hx-swap-oob="true">{{ PageTitle }}</h1>
<div class="slide-up space-y-6">
<div class="glass rounded-xl glow-border">
<div class="flex items-center justify-between px-5 py-4 border-b border-white/[0.04]">
diff --git a/templates/domains/htmx/editdomain.htmx.django b/templates/domains/htmx/editdomain.htmx.django
index f26dbce..7969999 100644
--- a/templates/domains/htmx/editdomain.htmx.django
+++ b/templates/domains/htmx/editdomain.htmx.django
@@ -1,4 +1,3 @@
-<h1 id="page-title" class="text-sm font-medium text-zinc-100" hx-swap-oob="true">{{ PageTitle }}</h1>
<div class="slide-up flex items-start justify-center pt-12">
<div class="glass rounded-xl glow-border w-full max-w-lg">
<div class="px-5 py-4 border-b border-white/[0.04]">
diff --git a/templates/domains/htmx/edittld.htmx.django b/templates/domains/htmx/edittld.htmx.django
index 773578b..70c816d 100644
--- a/templates/domains/htmx/edittld.htmx.django
+++ b/templates/domains/htmx/edittld.htmx.django
@@ -1,4 +1,3 @@
-<h1 id="page-title" class="text-sm font-medium text-zinc-100" hx-swap-oob="true">{{ PageTitle }}</h1>
<div class="slide-up flex items-start justify-center pt-12">
<div class="glass rounded-xl glow-border w-full max-w-lg">
<div class="px-5 py-4 border-b border-white/[0.04]">
diff --git a/templates/domains/htmx/index.htmx.django b/templates/domains/htmx/index.htmx.django
index 8f38bd3..0efaf45 100644
--- a/templates/domains/htmx/index.htmx.django
+++ b/templates/domains/htmx/index.htmx.django
@@ -1,4 +1,3 @@
-<h1 id="page-title" class="text-sm font-medium text-zinc-100" hx-swap-oob="true">{{ PageTitle }}</h1>
<div class="slide-up space-y-6">
<div class="glass rounded-xl glow-border">
<div class="px-6 py-5 border-b border-white/[0.04]">
diff --git a/templates/domains/htmx/newdomain.htmx.django b/templates/domains/htmx/newdomain.htmx.django
index 992bbd9..fcce626 100644
--- a/templates/domains/htmx/newdomain.htmx.django
+++ b/templates/domains/htmx/newdomain.htmx.django
@@ -1,4 +1,3 @@
-<h1 id="page-title" class="text-sm font-medium text-zinc-100" hx-swap-oob="true">{{ PageTitle }}</h1>
<div class="slide-up flex items-start justify-center pt-12">
<div class="glass rounded-xl glow-border w-full max-w-lg">
<div class="px-5 py-4 border-b border-white/[0.04]">
@@ -6,7 +5,7 @@
</div>
<div class="p-5">
{% url "domains.manage.create" as create_path %}
- <form method="POST" action="{{ create_path }}" class="space-y-4">
+ <form hx-post="{{ create_path }}" hx-swap="none" class="space-y-4">
<div>
<label class="block text-xs font-medium text-zinc-400 mb-1.5 ml-1">Domain Name</label>
<input type="text" name="name" required autocomplete="off" placeholder="myproject" class="input-field">
diff --git a/templates/domains/htmx/newtld.htmx.django b/templates/domains/htmx/newtld.htmx.django
index 9962435..74df6f6 100644
--- a/templates/domains/htmx/newtld.htmx.django
+++ b/templates/domains/htmx/newtld.htmx.django
@@ -1,4 +1,3 @@
-<h1 id="page-title" class="text-sm font-medium text-zinc-100" hx-swap-oob="true">{{ PageTitle }}</h1>
<div class="slide-up flex items-start justify-center pt-12">
<div class="glass rounded-xl glow-border w-full max-w-lg">
<div class="px-5 py-4 border-b border-white/[0.04]">
@@ -6,7 +5,7 @@
</div>
<div class="p-5">
{% url "domains.tlds.create" as create_path %}
- <form method="POST" action="{{ create_path }}" class="space-y-4">
+ <form hx-post="{{ create_path }}" hx-swap="none" class="space-y-4">
<div>
<label class="block text-xs font-medium text-zinc-400 mb-1.5 ml-1">TLD Name</label>
<input type="text" name="name" required autocomplete="off" placeholder="example" class="input-field">
diff --git a/templates/domains/htmx/tlds.htmx.django b/templates/domains/htmx/tlds.htmx.django
index 13705d8..7ec1e01 100644
--- a/templates/domains/htmx/tlds.htmx.django
+++ b/templates/domains/htmx/tlds.htmx.django
@@ -1,4 +1,3 @@
-<h1 id="page-title" class="text-sm font-medium text-zinc-100" hx-swap-oob="true">{{ PageTitle }}</h1>
<div class="slide-up space-y-6">
<div class="glass rounded-xl glow-border">
<div class="flex items-center justify-between px-5 py-4 border-b border-white/[0.04]">
diff --git a/templates/layouts/dashboard.django b/templates/layouts/dashboard.django
index 26aad2e..7863804 100644
--- a/templates/layouts/dashboard.django
+++ b/templates/layouts/dashboard.django
@@ -5,8 +5,6 @@
{% include "partials/sidebar.django" %}
<div class="flex-1 flex flex-col min-h-screen" id="panel">
- {% include "partials/header.django" %}
-
<main class="flex-1 p-8" id="content">
{% block dashboard %}{% endblock %}
</main>
diff --git a/templates/mail/htmx/editmailbox.htmx.django b/templates/mail/htmx/editmailbox.htmx.django
index b25563f..56fd89a 100644
--- a/templates/mail/htmx/editmailbox.htmx.django
+++ b/templates/mail/htmx/editmailbox.htmx.django
@@ -1,4 +1,3 @@
-<h1 id="page-title" class="text-sm font-medium text-zinc-100" hx-swap-oob="true">{{ PageTitle }}</h1>
<div class="slide-up space-y-6 max-w-lg mx-auto pt-12">
<div class="glass rounded-xl glow-border">
<div class="px-5 py-4 border-b border-white/[0.04]">
diff --git a/templates/mail/htmx/edituser.htmx.django b/templates/mail/htmx/edituser.htmx.django
index 8b22afc..6a6c7cb 100644
--- a/templates/mail/htmx/edituser.htmx.django
+++ b/templates/mail/htmx/edituser.htmx.django
@@ -1,4 +1,3 @@
-<h1 id="page-title" class="text-sm font-medium text-zinc-100" hx-swap-oob="true">{{ PageTitle }}</h1>
<div class="slide-up flex items-start justify-center pt-12">
<div class="glass rounded-xl glow-border w-full max-w-lg">
<div class="px-5 py-4 border-b border-white/[0.04]">
diff --git a/templates/mail/htmx/index.htmx.django b/templates/mail/htmx/index.htmx.django
index b5b4895..802d047 100644
--- a/templates/mail/htmx/index.htmx.django
+++ b/templates/mail/htmx/index.htmx.django
@@ -1,4 +1,3 @@
-<h1 id="page-title" class="text-sm font-medium text-zinc-100" hx-swap-oob="true">{{ PageTitle }}</h1>
<div class="slide-up space-y-6">
<div class="glass rounded-xl glow-border">
<div class="px-6 py-5 border-b border-white/[0.04]">
diff --git a/templates/mail/htmx/mailbox.htmx.django b/templates/mail/htmx/mailbox.htmx.django
deleted file mode 100644
index 831a07d..0000000
--- a/templates/mail/htmx/mailbox.htmx.django
+++ /dev/null
@@ -1,17 +0,0 @@
-<h1 id="page-title" class="text-sm font-medium text-zinc-100" hx-swap-oob="true">{{ PageTitle }}</h1>
-<div class="slide-up space-y-6">
- <div class="glass rounded-xl glow-border">
- <div class="flex items-center justify-between px-5 py-4 border-b border-white/[0.04]">
- <h2 class="text-sm font-medium text-zinc-200">Inbox</h2>
- </div>
- <div class="flex flex-col items-center justify-center py-16 text-center">
- <div class="flex items-center justify-center w-12 h-12 rounded-2xl bg-surface-800 mb-4">
- <svg class="w-6 h-6 text-zinc-600" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
- <path stroke-linecap="round" stroke-linejoin="round" d="M21.75 6.75v10.5a2.25 2.25 0 0 1-2.25 2.25h-15a2.25 2.25 0 0 1-2.25-2.25V6.75m19.5 0A2.25 2.25 0 0 0 19.5 4.5h-15a2.25 2.25 0 0 0-2.25 2.25m19.5 0v.243a2.25 2.25 0 0 1-1.07 1.916l-7.5 4.615a2.25 2.25 0 0 1-2.36 0L3.32 8.91a2.25 2.25 0 0 1-1.07-1.916V6.75" />
- </svg>
- </div>
- <p class="text-sm text-zinc-400">No emails in this mailbox</p>
- <p class="mt-1 text-xs text-zinc-600">Emails sent to {{ Address }} will appear here</p>
- </div>
- </div>
-</div> \ No newline at end of file
diff --git a/templates/mail/htmx/mailboxes.htmx.django b/templates/mail/htmx/mailboxes.htmx.django
index c0200af..34c9fc5 100644
--- a/templates/mail/htmx/mailboxes.htmx.django
+++ b/templates/mail/htmx/mailboxes.htmx.django
@@ -1,4 +1,3 @@
-<h1 id="page-title" class="text-sm font-medium text-zinc-100" hx-swap-oob="true">{{ PageTitle }}</h1>
<div class="slide-up space-y-6">
<div class="glass rounded-xl glow-border">
<div class="flex items-center justify-between px-5 py-4 border-b border-white/[0.04]">
@@ -14,8 +13,7 @@
{% for mailbox in items %}
<div class="flex items-center justify-between px-5 py-3">
<div class="flex items-center gap-3">
- {% url "mail.mailbox" address=mailbox.Address as mailbox_path %}
- <a href="{{ mailbox_path }}" hx-get="{{ mailbox_path }}" hx-target="#content" hx-swap="innerHTML" hx-push-url="true" class="flex items-center gap-3 hover:opacity-80 transition-opacity duration-150">
+ <a href="/mail/webmail/{{ mailbox.ID }}" hx-get="/mail/webmail/{{ mailbox.ID }}" hx-target="#content" hx-swap="innerHTML" hx-push-url="true" class="flex items-center gap-3 hover:opacity-80 transition-opacity duration-150">
<div class="flex items-center justify-center w-8 h-8 rounded-lg bg-accent-500/10">
<svg class="w-4 h-4 text-accent-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M2.25 13.5h3.86a2.25 2.25 0 0 1 2.012 1.244l.256.512a2.25 2.25 0 0 0 2.013 1.244h2.21a2.25 2.25 0 0 0 2.013-1.244l.256-.512a2.25 2.25 0 0 1 2.013-1.244h3.859m-19.5.338V18a2.25 2.25 0 0 0 2.25 2.25h15A2.25 2.25 0 0 0 21.75 18v-4.162c0-.224-.034-.447-.1-.661L19.24 5.338a2.25 2.25 0 0 0-2.15-1.588H6.911a2.25 2.25 0 0 0-2.15 1.588L2.35 13.177a2.25 2.25 0 0 0-.1.661Z" />
diff --git a/templates/mail/htmx/newmailbox.htmx.django b/templates/mail/htmx/newmailbox.htmx.django
index b8a8ed7..6c0e925 100644
--- a/templates/mail/htmx/newmailbox.htmx.django
+++ b/templates/mail/htmx/newmailbox.htmx.django
@@ -1,4 +1,3 @@
-<h1 id="page-title" class="text-sm font-medium text-zinc-100" hx-swap-oob="true">{{ PageTitle }}</h1>
<div class="slide-up flex items-start justify-center pt-12">
<div class="glass rounded-xl glow-border w-full max-w-lg">
<div class="px-5 py-4 border-b border-white/[0.04]">
@@ -6,7 +5,7 @@
</div>
<div class="p-5">
{% url "mail.mailboxes.create" as create_path %}
- <form method="POST" action="{{ create_path }}" class="space-y-4">
+ <form hx-post="{{ create_path }}" hx-swap="none" class="space-y-4">
<div>
<label class="block text-xs font-medium text-zinc-400 mb-1.5 ml-1">Address</label>
<div class="flex items-center gap-2">
diff --git a/templates/mail/htmx/newuser.htmx.django b/templates/mail/htmx/newuser.htmx.django
index c6d4d10..8507b89 100644
--- a/templates/mail/htmx/newuser.htmx.django
+++ b/templates/mail/htmx/newuser.htmx.django
@@ -1,4 +1,3 @@
-<h1 id="page-title" class="text-sm font-medium text-zinc-100" hx-swap-oob="true">{{ PageTitle }}</h1>
<div class="slide-up flex items-start justify-center pt-12">
<div class="glass rounded-xl glow-border w-full max-w-lg">
<div class="px-5 py-4 border-b border-white/[0.04]">
@@ -6,7 +5,7 @@
</div>
<div class="p-5">
{% url "mail.users.create" as create_path %}
- <form method="POST" action="{{ create_path }}" class="space-y-4">
+ <form hx-post="{{ create_path }}" hx-swap="none" class="space-y-4">
<div>
<label class="block text-xs font-medium text-zinc-400 mb-1.5 ml-1">Username</label>
<input type="text" name="username" required autocomplete="off" class="input-field">
diff --git a/templates/mail/htmx/users.htmx.django b/templates/mail/htmx/users.htmx.django
index 8929907..e2a9113 100644
--- a/templates/mail/htmx/users.htmx.django
+++ b/templates/mail/htmx/users.htmx.django
@@ -1,4 +1,3 @@
-<h1 id="page-title" class="text-sm font-medium text-zinc-100" hx-swap-oob="true">{{ PageTitle }}</h1>
<div class="slide-up space-y-6">
<div class="glass rounded-xl glow-border">
<div class="flex items-center justify-between px-5 py-4 border-b border-white/[0.04]">
diff --git a/templates/mail/htmx/webmail.htmx.django b/templates/mail/htmx/webmail.htmx.django
new file mode 100644
index 0000000..1657b2a
--- /dev/null
+++ b/templates/mail/htmx/webmail.htmx.django
@@ -0,0 +1,229 @@
+<div class="-m-8 flex flex-col h-[calc(100vh)]" id="webmail" data-mailbox-id="{{ active_mailbox.ID }}" data-folder-id="{{ active_folder.ID }}" data-folder-slug="{% if is_starred_view %}starred{% else %}{{ active_folder.Slug }}{% endif %}">
+ <div class="flex items-center gap-3 px-4 h-12 shrink-0 border-b border-white/[0.04] bg-surface-900/50">
+ <div class="dropdown" data-dropdown>
+ <input type="hidden" value="{{ active_mailbox.ID }}" data-dropdown-value>
+ <button type="button" class="flex items-center gap-2 px-3 py-1.5 rounded-lg bg-surface-800 border border-white/[0.06] hover:border-white/[0.1] text-sm text-zinc-200 transition-all duration-150" data-dropdown-trigger>
+ <svg class="w-4 h-4 text-accent-400 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
+ <path stroke-linecap="round" stroke-linejoin="round" d="M21.75 6.75v10.5a2.25 2.25 0 0 1-2.25 2.25h-15a2.25 2.25 0 0 1-2.25-2.25V6.75m19.5 0A2.25 2.25 0 0 0 19.5 4.5h-15a2.25 2.25 0 0 0-2.25 2.25m19.5 0v.243a2.25 2.25 0 0 1-1.07 1.916l-7.5 4.615a2.25 2.25 0 0 1-2.36 0L3.32 8.91a2.25 2.25 0 0 1-1.07-1.916V6.75" />
+ </svg>
+ <span class="truncate max-w-[200px]" data-dropdown-label>{{ active_mailbox.Address }}</span>
+ <svg class="w-3.5 h-3.5 text-zinc-500 shrink-0 transition-transform duration-150" data-dropdown-chevron fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
+ <path stroke-linecap="round" stroke-linejoin="round" d="m19.5 8.25-7.5 7.5-7.5-7.5" />
+ </svg>
+ </button>
+ <div class="dropdown-menu" data-dropdown-menu>
+ <div class="p-2 border-b border-white/[0.04]">
+ <input type="text" placeholder="Search mailboxes..." class="dropdown-search" data-dropdown-search>
+ </div>
+ <div class="dropdown-options" data-dropdown-options>
+ {% for mailbox in mailboxes %}
+ <a href="/mail/webmail/{{ mailbox.ID }}" hx-get="/mail/webmail/{{ mailbox.ID }}" hx-target="#content" hx-swap="innerHTML" hx-push-url="true" class="dropdown-option block" data-dropdown-option data-value="{{ mailbox.ID }}" data-label="{{ mailbox.Address }}">
+ <p class="text-sm text-zinc-200">{{ mailbox.Address }}</p>
+ <p class="text-xs text-zinc-500">{{ mailbox.User.DisplayName }}</p>
+ </a>
+ {% endfor %}
+ </div>
+ <div class="dropdown-empty hidden" data-dropdown-empty>
+ <p class="text-xs text-zinc-500 text-center py-3">No mailboxes found</p>
+ </div>
+ </div>
+ </div>
+
+ <div class="flex-1 max-w-md">
+ <div class="relative">
+ <svg class="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-zinc-500" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
+ <path stroke-linecap="round" stroke-linejoin="round" d="m21 21-5.197-5.197m0 0A7.5 7.5 0 1 0 5.196 5.196a7.5 7.5 0 0 0 10.607 10.607Z" />
+ </svg>
+ <input type="text" name="search" value="{{ search }}" placeholder="Search emails..." class="w-full pl-10 pr-4 py-1.5 rounded-lg bg-surface-800 border border-white/[0.06] text-sm text-zinc-200 placeholder-zinc-600 focus:outline-none focus:border-accent-500/50 transition-colors duration-150" hx-get="/mail/webmail/{{ active_mailbox.ID }}/folder/{{ active_folder.ID }}/emails" hx-target="#webmail-emails" hx-swap="innerHTML" hx-trigger="input changed delay:300ms" hx-include="[name='search']">
+ </div>
+ </div>
+
+ <div class="ml-auto flex items-center gap-2">
+ <button hx-get="/mail/webmail/{{ active_mailbox.ID }}/compose" hx-target="#webmail-preview" hx-swap="innerHTML" class="webmail-btn webmail-btn-primary">
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
+ <path stroke-linecap="round" stroke-linejoin="round" d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L10.582 16.07a4.5 4.5 0 0 1-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 0 1 1.13-1.897l8.932-8.931Zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0 1 15.75 21H5.25A2.25 2.25 0 0 1 3 18.75V8.25A2.25 2.25 0 0 1 5.25 6H10" />
+ </svg>
+ Compose
+ </button>
+ </div>
+ </div>
+
+ <div class="flex flex-1 min-h-0">
+ <div class="w-52 shrink-0 border-r border-white/[0.04] overflow-hidden" id="webmail-folders">
+ {% include "mail/webmail/folders.django" %}
+ </div>
+
+ <div class="w-80 shrink-0 border-r border-white/[0.04] overflow-y-auto" id="webmail-emails">
+ {% include "mail/webmail/emails.django" %}
+ </div>
+
+ <div class="flex-1 min-w-0 min-h-0" id="webmail-preview">
+ {% include "mail/webmail/empty.django" %}
+ </div>
+ </div>
+</div>
+
+<template id="webmail-empty-template">
+ {% include "mail/webmail/empty.django" %}
+</template>
+
+<div id="webmail-modal" class="fixed inset-0 z-50 hidden">
+ <div class="absolute inset-0 bg-black/60 backdrop-blur-sm" data-webmail-modal-backdrop></div>
+ <div class="flex items-center justify-center min-h-screen p-4">
+ <div class="relative glass rounded-xl glow-border w-full max-w-sm p-6 space-y-4">
+ <h3 id="webmail-modal-title" class="text-sm font-medium text-zinc-100"></h3>
+ <div id="webmail-modal-body"></div>
+ <div class="flex items-center justify-end gap-3 pt-2">
+ <button data-webmail-modal-cancel class="px-4 py-2 text-xs text-zinc-400 hover:text-zinc-200 rounded-lg bg-surface-800 border border-white/[0.06] hover:border-white/[0.1] transition-all duration-150">Cancel</button>
+ <button id="webmail-modal-confirm" class="px-4 py-2 text-xs text-white rounded-lg bg-red-500/80 hover:bg-red-500 border border-red-400/20 transition-all duration-150">Confirm</button>
+ </div>
+ </div>
+ </div>
+</div>
+
+<script>
+(function() {
+ var webmail = document.getElementById("webmail");
+ if (!webmail) return;
+
+ function getMailboxID() {
+ return webmail.dataset.mailboxId;
+ }
+
+ function getFolderSlug() {
+ return webmail.dataset.folderSlug;
+ }
+
+ function refreshEmailList() {
+ var mailboxId = getMailboxID();
+ var folderSlug = getFolderSlug();
+ var url;
+
+ if (folderSlug === "starred") {
+ url = "/mail/webmail/" + mailboxId + "/starred/emails";
+ } else {
+ var folderId = webmail.dataset.folderId;
+ url = "/mail/webmail/" + mailboxId + "/folder/" + folderId + "/emails";
+ }
+
+ htmx.ajax("GET", url, { target: "#webmail-emails", swap: "innerHTML" });
+ }
+
+ function refreshFolderSidebar() {
+ var mailboxId = getMailboxID();
+ var folderSlug = getFolderSlug();
+ htmx.ajax("GET", "/mail/webmail/" + mailboxId + "/folders?folder=" + folderSlug, { target: "#webmail-folders", swap: "innerHTML" });
+ }
+
+ function clearPreview() {
+ var template = document.getElementById("webmail-empty-template");
+ var preview = document.getElementById("webmail-preview");
+ if (template && preview) {
+ preview.innerHTML = template.innerHTML;
+ }
+ }
+
+ var starredSvgFilled = '<svg class="w-4 h-4 text-yellow-400 fill-yellow-400" viewBox="0 0 24 24" stroke-width="1.5"><path d="M11.48 3.499a.562.562 0 0 1 1.04 0l2.125 5.111a.563.563 0 0 0 .475.345l5.518.442c.499.04.701.663.321.988l-4.204 3.602a.563.563 0 0 0-.182.557l1.285 5.385a.562.562 0 0 1-.84.61l-4.725-2.885a.562.562 0 0 0-.586 0L6.982 20.54a.562.562 0 0 1-.84-.61l1.285-5.386a.562.562 0 0 0-.182-.557l-4.204-3.602a.562.562 0 0 1 .321-.988l5.518-.442a.563.563 0 0 0 .475-.345L11.48 3.5Z" /></svg>';
+ var starredSvgEmpty = '<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M11.48 3.499a.562.562 0 0 1 1.04 0l2.125 5.111a.563.563 0 0 0 .475.345l5.518.442c.499.04.701.663.321.988l-4.204 3.602a.563.563 0 0 0-.182.557l1.285 5.385a.562.562 0 0 1-.84.61l-4.725-2.885a.562.562 0 0 0-.586 0L6.982 20.54a.562.562 0 0 1-.84-.61l1.285-5.386a.562.562 0 0 0-.182-.557l-4.204-3.602a.562.562 0 0 1 .321-.988l5.518-.442a.563.563 0 0 0 .475-.345L11.48 3.5Z" /></svg>';
+
+ document.body.addEventListener("emailStarred", function(event) {
+ var triggerElement = event.target;
+ if (triggerElement) {
+ var emailId = triggerElement.getAttribute("data-email-star") || triggerElement.getAttribute("data-preview-star");
+ if (!emailId) {
+ triggerElement = triggerElement.closest("[data-email-star], [data-preview-star]");
+ if (triggerElement) {
+ emailId = triggerElement.getAttribute("data-email-star") || triggerElement.getAttribute("data-preview-star");
+ }
+ }
+ if (emailId) {
+ var isFilled = triggerElement.querySelector(".fill-yellow-400");
+ var newSvg = isFilled ? starredSvgEmpty : starredSvgFilled;
+
+ var listStar = document.querySelector("[data-email-star='" + emailId + "']");
+ if (listStar) listStar.innerHTML = newSvg;
+
+ var previewStar = document.querySelector("[data-preview-star='" + emailId + "']");
+ if (previewStar) previewStar.innerHTML = newSvg;
+ }
+ }
+ refreshFolderSidebar();
+ if (getFolderSlug() === "starred") {
+ refreshEmailList();
+ }
+ });
+
+ document.body.addEventListener("emailDeleted", function() {
+ clearPreview();
+ refreshEmailList();
+ refreshFolderSidebar();
+ });
+
+ document.body.addEventListener("emailMoved", function() {
+ clearPreview();
+ refreshEmailList();
+ refreshFolderSidebar();
+ });
+
+ document.body.addEventListener("emailReadChanged", function() {
+ refreshEmailList();
+ refreshFolderSidebar();
+ });
+
+ var modal = document.getElementById("webmail-modal");
+ var modalTitle = document.getElementById("webmail-modal-title");
+ var modalBody = document.getElementById("webmail-modal-body");
+ var modalConfirm = document.getElementById("webmail-modal-confirm");
+ var modalBackdrop = modal.querySelector("[data-webmail-modal-backdrop]");
+ var modalCancel = modal.querySelector("[data-webmail-modal-cancel]");
+
+ function closeModal() {
+ modal.classList.add("hidden");
+ modalBody.innerHTML = "";
+ }
+
+ modalBackdrop.addEventListener("click", closeModal);
+ modalCancel.addEventListener("click", closeModal);
+
+ window.webmailConfirm = function(options) {
+ modalTitle.textContent = options.title || "Are you sure?";
+ modalBody.innerHTML = '<p class="text-xs text-zinc-400 leading-relaxed">' + (options.message || "") + '</p>';
+ modalConfirm.textContent = options.confirmText || "Confirm";
+ modalConfirm.className = "px-4 py-2 text-xs text-white rounded-lg transition-all duration-150 " + (options.danger ? "bg-red-500/80 hover:bg-red-500 border border-red-400/20" : "bg-accent-500/80 hover:bg-accent-500 border border-accent-400/20");
+ modal.classList.remove("hidden");
+
+ var cloned = modalConfirm.cloneNode(true);
+ modalConfirm.parentNode.replaceChild(cloned, modalConfirm);
+ modalConfirm = cloned;
+
+ modalConfirm.addEventListener("click", function() {
+ closeModal();
+ if (options.onConfirm) options.onConfirm();
+ });
+ };
+
+ window.webmailPrompt = function(options) {
+ modalTitle.textContent = options.title || "Input";
+ modalBody.innerHTML = '<input type="text" id="webmail-modal-input" placeholder="' + (options.placeholder || "") + '" class="input-field mt-2" value="' + (options.value || "") + '">';
+ modalConfirm.textContent = options.confirmText || "OK";
+ modalConfirm.className = "px-4 py-2 text-xs text-white rounded-lg bg-accent-500/80 hover:bg-accent-500 border border-accent-400/20 transition-all duration-150";
+ modal.classList.remove("hidden");
+
+ setTimeout(function() {
+ var input = document.getElementById("webmail-modal-input");
+ if (input) input.focus();
+ }, 50);
+
+ var cloned = modalConfirm.cloneNode(true);
+ modalConfirm.parentNode.replaceChild(cloned, modalConfirm);
+ modalConfirm = cloned;
+
+ modalConfirm.addEventListener("click", function() {
+ var input = document.getElementById("webmail-modal-input");
+ var value = input ? input.value.trim() : "";
+ closeModal();
+ if (value && options.onConfirm) options.onConfirm(value);
+ });
+ };
+})();
+</script>
diff --git a/templates/mail/mailbox.django b/templates/mail/mailbox.django
deleted file mode 100644
index ed03a72..0000000
--- a/templates/mail/mailbox.django
+++ /dev/null
@@ -1,16 +0,0 @@
-{% extends "layouts/dashboard.django" %}
-
-{% block header_actions %}
-<div class="flex items-center gap-3">
- <div class="flex items-center gap-2 px-3 py-1.5 rounded-lg bg-surface-800 border border-white/[0.06]">
- <svg class="w-3.5 h-3.5 text-accent-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
- <path stroke-linecap="round" stroke-linejoin="round" d="M21.75 6.75v10.5a2.25 2.25 0 0 1-2.25 2.25h-15a2.25 2.25 0 0 1-2.25-2.25V6.75m19.5 0A2.25 2.25 0 0 0 19.5 4.5h-15a2.25 2.25 0 0 0-2.25 2.25m19.5 0v.243a2.25 2.25 0 0 1-1.07 1.916l-7.5 4.615a2.25 2.25 0 0 1-2.36 0L3.32 8.91a2.25 2.25 0 0 1-1.07-1.916V6.75" />
- </svg>
- <span class="text-xs text-zinc-300">{{ Address }}</span>
- </div>
-</div>
-{% endblock %}
-
-{% block dashboard %}
-{% include "mail/htmx/mailbox.htmx.django" %}
-{% endblock %} \ No newline at end of file
diff --git a/templates/mail/webmail.django b/templates/mail/webmail.django
new file mode 100644
index 0000000..e444c5c
--- /dev/null
+++ b/templates/mail/webmail.django
@@ -0,0 +1,5 @@
+{% extends "layouts/dashboard.django" %}
+
+{% block dashboard %}
+{% include "mail/htmx/webmail.htmx.django" %}
+{% endblock %}
diff --git a/templates/mail/webmail/compose.django b/templates/mail/webmail/compose.django
new file mode 100644
index 0000000..5fefb7a
--- /dev/null
+++ b/templates/mail/webmail/compose.django
@@ -0,0 +1,309 @@
+<div class="flex flex-col h-full">
+ <div class="flex items-center justify-between px-6 py-2 border-b border-white/[0.04] shrink-0">
+ <h3 class="text-sm font-medium text-zinc-200">{% if reply_to %}Reply{% else %}New Message{% endif %}</h3>
+ <button type="button" onclick="var t = document.getElementById('webmail-empty-template'); if (t) document.getElementById('webmail-preview').innerHTML = t.innerHTML;" class="webmail-btn-icon" title="Discard">
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
+ <path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12" />
+ </svg>
+ </button>
+ </div>
+
+ <form id="compose-form" class="flex flex-col flex-1 min-h-0">
+ <input type="hidden" id="compose-draft-id" value="{{ draft_id }}">
+ <input type="hidden" id="compose-from-mailbox-id" value="{{ active_mailbox.ID }}">
+ <input type="hidden" id="compose-to-value" value="{% if reply_to %}{{ reply_to.FromAddress }}{% endif %}">
+ <input type="hidden" id="compose-cc-value" value="">
+ <input type="hidden" id="compose-bcc-value" value="">
+
+ <div class="border-b border-white/[0.04]">
+ <div class="flex items-start px-6 py-1.5 border-b border-white/[0.04]">
+ <label class="text-xs text-zinc-500 w-12 shrink-0 leading-[24px]">From</label>
+ <div class="dropdown flex-1 min-w-0" data-dropdown>
+ <input type="hidden" value="{{ active_mailbox.ID }}" data-dropdown-value>
+ <button type="button" class="flex items-center gap-1 text-left" data-dropdown-trigger>
+ <span class="webmail-tag" data-dropdown-label>{{ active_mailbox.User.DisplayName }} &lt;{{ active_mailbox.Address }}&gt;</span>
+ <svg class="w-3 h-3 text-zinc-600 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
+ <path stroke-linecap="round" stroke-linejoin="round" d="m19.5 8.25-7.5 7.5-7.5-7.5" />
+ </svg>
+ </button>
+ <div class="dropdown-menu min-w-[16rem]" data-dropdown-menu>
+ <div class="dropdown-options" data-dropdown-options>
+ {% for address in from_addresses %}
+ <button type="button" class="dropdown-option" data-dropdown-option data-value="{{ address.MailboxID }}" data-label="{{ address.DisplayName }} <{{ address.Address }}>" onclick="document.getElementById('compose-from-mailbox-id').value = '{{ address.MailboxID }}';">
+ <p class="text-sm text-zinc-200">{{ address.DisplayName }}</p>
+ <p class="text-xs text-zinc-500">{{ address.Address }}</p>
+ </button>
+ {% endfor %}
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <div class="flex items-start px-6 py-1.5 border-b border-white/[0.04]">
+ <label class="text-xs text-zinc-500 w-12 shrink-0 leading-[24px]">To</label>
+ <div class="webmail-tag-input flex-1 min-w-0" id="to-field">
+ <div class="tags-container">
+ <span id="to-tags"></span>
+ <input type="text" autocomplete="off" placeholder="Add recipient..." class="webmail-compose-field flex-1 min-w-[80px] leading-[24px]" data-recipient-input data-field="to">
+ </div>
+ <div class="webmail-autocomplete" data-autocomplete></div>
+ </div>
+ </div>
+
+ <div class="flex items-start px-6 py-1.5 border-b border-white/[0.04]">
+ <label class="text-xs text-zinc-500 w-12 shrink-0 leading-[24px]">Cc</label>
+ <div class="webmail-tag-input flex-1 min-w-0" id="cc-field">
+ <div class="tags-container">
+ <span id="cc-tags"></span>
+ <input type="text" autocomplete="off" placeholder="Add Cc..." class="webmail-compose-field flex-1 min-w-[80px] leading-[24px]" data-recipient-input data-field="cc">
+ </div>
+ <div class="webmail-autocomplete" data-autocomplete></div>
+ </div>
+ </div>
+
+ <div class="flex items-start px-6 py-1.5 border-b border-white/[0.04]">
+ <label class="text-xs text-zinc-500 w-12 shrink-0 leading-[24px]">Bcc</label>
+ <div class="webmail-tag-input flex-1 min-w-0" id="bcc-field">
+ <div class="tags-container">
+ <span id="bcc-tags"></span>
+ <input type="text" autocomplete="off" placeholder="Add Bcc..." class="webmail-compose-field flex-1 min-w-[80px] leading-[24px]" data-recipient-input data-field="bcc">
+ </div>
+ <div class="webmail-autocomplete" data-autocomplete></div>
+ </div>
+ </div>
+
+ <div class="flex items-start px-6 py-1.5">
+ <label class="text-xs text-zinc-500 w-12 shrink-0 leading-[24px]">Subject</label>
+ <input type="text" name="subject" value="{% if reply_to %}Re: {{ reply_to.Subject }}{% endif %}" required autocomplete="off" placeholder="Subject" class="webmail-compose-field flex-1 min-w-0 leading-[24px]">
+ </div>
+ </div>
+
+ <div class="webmail-toolbar" id="compose-toolbar">
+ <button type="button" data-command="bold" title="Bold"><b>B</b></button>
+ <button type="button" data-command="italic" title="Italic"><i>I</i></button>
+ <button type="button" data-command="underline" title="Underline"><u>U</u></button>
+ <div class="separator"></div>
+ <button type="button" data-command="insertUnorderedList" title="Bullet list">
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
+ <path stroke-linecap="round" stroke-linejoin="round" d="M8.25 6.75h12M8.25 12h12m-12 5.25h12M3.75 6.75h.007v.008H3.75V6.75Zm.375 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0ZM3.75 12h.007v.008H3.75V12Zm.375 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Zm-.375 5.25h.007v.008H3.75v-.008Zm.375 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Z" />
+ </svg>
+ </button>
+ <button type="button" data-command="insertOrderedList" title="Numbered list">
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
+ <path stroke-linecap="round" stroke-linejoin="round" d="M8.242 5.992h12m-12 6.003h12m-12 5.999h12M4.117 7.495v-3.75H2.99m1.125 3.75H2.99m1.125 0H5.24m-1.92 2.577a1.125 1.125 0 1 1 1.591 1.59l-1.83 1.83h2.16M2.99 15.745h1.125a1.125 1.125 0 0 1 0 2.25H3.74m0-.002h.375a1.125 1.125 0 0 1 0 2.25H2.99" />
+ </svg>
+ </button>
+ <div class="separator"></div>
+ <button type="button" data-command="formatBlock" data-value="blockquote" title="Quote">
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
+ <path stroke-linecap="round" stroke-linejoin="round" d="M7.5 8.25h9m-9 3H12m-9.75 1.51c0 1.6 1.123 2.994 2.707 3.227 1.129.166 2.27.293 3.423.379.35.026.67.21.865.501L12 21l2.755-4.133a1.14 1.14 0 0 1 .865-.501 48.172 48.172 0 0 0 3.423-.379c1.584-.233 2.707-1.626 2.707-3.228V6.741c0-1.602-1.123-2.995-2.707-3.228A48.394 48.394 0 0 0 12 3c-2.392 0-4.744.175-7.043.513C3.373 3.746 2.25 5.14 2.25 6.741v6.018Z" />
+ </svg>
+ </button>
+ <button type="button" data-command="createLink" title="Link">
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
+ <path stroke-linecap="round" stroke-linejoin="round" d="M13.19 8.688a4.5 4.5 0 0 1 1.242 7.244l-4.5 4.5a4.5 4.5 0 0 1-6.364-6.364l1.757-1.757m13.35-.622 1.757-1.757a4.5 4.5 0 0 0-6.364-6.364l-4.5 4.5a4.5 4.5 0 0 0 1.242 7.244" />
+ </svg>
+ </button>
+ </div>
+
+ <div class="flex-1 min-h-0 overflow-y-auto">
+ <div id="compose-editor" class="webmail-editor" contenteditable="true" data-placeholder="Write your message...">{% if reply_to %}<br><br><blockquote>On {{ reply_to.CreatedAt|date:"Jan 2, 2006 3:04 PM" }}, {{ reply_to.FromAddress }} wrote:<br><br>{{ reply_to.Snippet }}</blockquote>{% endif %}</div>
+ </div>
+
+ <div class="flex items-center justify-between px-6 py-3 border-t border-white/[0.04] shrink-0">
+ <div class="flex items-center gap-2">
+ <button type="button" onclick="submitCompose('send')" class="webmail-btn webmail-btn-primary">
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
+ <path stroke-linecap="round" stroke-linejoin="round" d="M6 12 3.269 3.125A59.769 59.769 0 0 1 21.485 12 59.768 59.768 0 0 1 3.27 20.875L5.999 12Zm0 0h7.5" />
+ </svg>
+ Send
+ </button>
+ <button type="button" onclick="submitCompose('draft')" class="webmail-btn webmail-btn-ghost">
+ Save Draft
+ </button>
+ </div>
+ </div>
+ </form>
+</div>
+
+<script>
+(function() {
+ var composeState = { to: [], cc: [], bcc: [] };
+ var allRecipients = [
+ {% for r in all_recipients %}
+ { address: "{{ r.Address }}", display_name: "{{ r.DisplayName }}" }{% if not forloop.Last %},{% endif %}
+ {% endfor %}
+ ];
+
+ {% if reply_to %}
+ composeState.to.push("{{ reply_to.FromAddress }}");
+ {% endif %}
+
+ function renderTags(field) {
+ var container = document.getElementById(field + "-tags");
+ if (!container) return;
+ container.innerHTML = "";
+ composeState[field].forEach(function(address, index) {
+ var tag = document.createElement("span");
+ tag.className = "webmail-tag";
+ tag.innerHTML = '<span class="truncate">' + address + '</span><span class="remove" data-field="' + field + '" data-index="' + index + '">&times;</span>';
+ container.appendChild(tag);
+ });
+ updateHiddenField(field);
+ }
+
+ function updateHiddenField(field) {
+ var hiddenInput = document.getElementById("compose-" + field + "-value");
+ if (hiddenInput) {
+ hiddenInput.value = composeState[field].join(", ");
+ }
+ }
+
+ function addRecipient(field, address) {
+ address = address.trim();
+ if (!address || composeState[field].indexOf(address) !== -1) return;
+ composeState[field].push(address);
+ renderTags(field);
+ }
+
+ function removeRecipient(field, index) {
+ composeState[field].splice(index, 1);
+ renderTags(field);
+ }
+
+ document.querySelectorAll("[data-recipient-input]").forEach(function(input) {
+ var field = input.dataset.field;
+ var autocomplete = input.closest(".webmail-tag-input").querySelector("[data-autocomplete]");
+
+ input.addEventListener("input", function() {
+ var query = input.value.toLowerCase().trim();
+ if (query.length < 1) { autocomplete.classList.remove("visible"); return; }
+
+ var matches = allRecipients.filter(function(r) {
+ var alreadyAdded = composeState[field].indexOf(r.address) !== -1;
+ return !alreadyAdded && (r.address.toLowerCase().indexOf(query) !== -1 || r.display_name.toLowerCase().indexOf(query) !== -1);
+ }).slice(0, 8);
+
+ if (matches.length === 0) { autocomplete.classList.remove("visible"); return; }
+
+ autocomplete.innerHTML = "";
+ matches.forEach(function(match) {
+ var option = document.createElement("div");
+ option.className = "webmail-autocomplete-option";
+ option.innerHTML = '<p class="text-sm text-zinc-200">' + match.display_name + '</p><p class="text-xs text-zinc-500">' + match.address + '</p>';
+ option.addEventListener("mousedown", function(e) {
+ e.preventDefault();
+ addRecipient(field, match.address);
+ input.value = "";
+ autocomplete.classList.remove("visible");
+ });
+ autocomplete.appendChild(option);
+ });
+ autocomplete.classList.add("visible");
+ });
+
+ input.addEventListener("keydown", function(e) {
+ if (e.key === "Enter" || e.key === "," || e.key === "Tab") {
+ e.preventDefault();
+ var val = input.value.replace(",", "").trim();
+ if (val) { addRecipient(field, val); input.value = ""; }
+ autocomplete.classList.remove("visible");
+ }
+ if (e.key === "Backspace" && input.value === "" && composeState[field].length > 0) {
+ removeRecipient(field, composeState[field].length - 1);
+ }
+ });
+
+ input.addEventListener("blur", function() {
+ setTimeout(function() { autocomplete.classList.remove("visible"); }, 200);
+ var val = input.value.replace(",", "").trim();
+ if (val) { addRecipient(field, val); input.value = ""; }
+ });
+ });
+
+ document.addEventListener("click", function(e) {
+ if (e.target.classList.contains("remove") && e.target.dataset.field) {
+ removeRecipient(e.target.dataset.field, parseInt(e.target.dataset.index));
+ }
+ });
+
+ renderTags("to");
+ renderTags("cc");
+ renderTags("bcc");
+
+ var toolbar = document.getElementById("compose-toolbar");
+ var editor = document.getElementById("compose-editor");
+
+ toolbar.querySelectorAll("[data-command]").forEach(function(button) {
+ button.addEventListener("click", function() {
+ var command = button.dataset.command;
+ var value = button.dataset.value || null;
+
+ if (command === "createLink") {
+ if (typeof webmailPrompt === "function") {
+ webmailPrompt({
+ title: "Insert Link",
+ placeholder: "https://example.com",
+ confirmText: "Insert",
+ onConfirm: function(url) {
+ editor.focus();
+ document.execCommand("createLink", false, url);
+ }
+ });
+ }
+ return;
+ }
+
+ document.execCommand(command, false, value);
+ editor.focus();
+ updateToolbarState();
+ });
+ });
+
+ function updateToolbarState() {
+ toolbar.querySelectorAll("[data-command]").forEach(function(button) {
+ var command = button.dataset.command;
+ var isActive = false;
+
+ if (command === "bold" || command === "italic" || command === "underline" || command === "insertUnorderedList" || command === "insertOrderedList") {
+ isActive = document.queryCommandState(command);
+ }
+
+ if (isActive) {
+ button.classList.add("active");
+ } else {
+ button.classList.remove("active");
+ }
+ });
+ }
+
+ editor.addEventListener("keyup", updateToolbarState);
+ editor.addEventListener("mouseup", updateToolbarState);
+ editor.addEventListener("focus", updateToolbarState);
+
+ window.submitCompose = function(action) {
+ var form = document.getElementById("compose-form");
+ var url = action === "draft" ? "/mail/webmail/draft" : "/mail/webmail/send";
+ var draftButton = form.querySelector("[onclick=\"submitCompose('draft')\"]");
+
+ htmx.ajax("POST", url, {
+ swap: "none",
+ values: {
+ from_mailbox_id: document.getElementById("compose-from-mailbox-id").value,
+ to_address: document.getElementById("compose-to-value").value,
+ cc_addresses: document.getElementById("compose-cc-value").value,
+ bcc_addresses: document.getElementById("compose-bcc-value").value,
+ subject: form.querySelector("[name='subject']").value,
+ body: editor.innerHTML,
+ draft_id: document.getElementById("compose-draft-id").value
+ }
+ }).then(function() {
+ if (action === "draft" && draftButton) {
+ var originalText = draftButton.textContent;
+ draftButton.textContent = "Saved!";
+ setTimeout(function() { draftButton.textContent = originalText; }, 1500);
+ }
+ });
+ };
+})();
+</script>
diff --git a/templates/mail/webmail/emails.django b/templates/mail/webmail/emails.django
new file mode 100644
index 0000000..a96a40f
--- /dev/null
+++ b/templates/mail/webmail/emails.django
@@ -0,0 +1,46 @@
+{% if emails %}
+<div class="divide-y divide-white/[0.04]">
+ {% for email in emails %}
+ <div class="webmail-email w-full text-left px-4 py-3 hover:bg-white/[0.02] transition-colors duration-150 cursor-pointer {% if not email.IsRead %}bg-accent-500/[0.03]{% endif %}" data-email-id="{{ email.ID }}" hx-get="/mail/webmail/{{ email.MailboxID }}/email/{{ email.ID }}" hx-target="#webmail-preview" hx-swap="innerHTML" hx-trigger="click consume" data-email-row>
+ <div class="flex items-start gap-3">
+ <button type="button" hx-put="/mail/webmail/email/{{ email.ID }}/star" hx-swap="none" class="webmail-star mt-0.5 shrink-0 text-zinc-600 hover:text-yellow-400 transition-colors duration-150" onclick="event.stopPropagation();" data-email-star="{{ email.ID }}">
+ {% if email.IsStarred %}
+ <svg class="w-4 h-4 text-yellow-400 fill-yellow-400" viewBox="0 0 24 24" stroke-width="1.5">
+ <path d="M11.48 3.499a.562.562 0 0 1 1.04 0l2.125 5.111a.563.563 0 0 0 .475.345l5.518.442c.499.04.701.663.321.988l-4.204 3.602a.563.563 0 0 0-.182.557l1.285 5.385a.562.562 0 0 1-.84.61l-4.725-2.885a.562.562 0 0 0-.586 0L6.982 20.54a.562.562 0 0 1-.84-.61l1.285-5.386a.562.562 0 0 0-.182-.557l-4.204-3.602a.562.562 0 0 1 .321-.988l5.518-.442a.563.563 0 0 0 .475-.345L11.48 3.5Z" />
+ </svg>
+ {% else %}
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
+ <path stroke-linecap="round" stroke-linejoin="round" d="M11.48 3.499a.562.562 0 0 1 1.04 0l2.125 5.111a.563.563 0 0 0 .475.345l5.518.442c.499.04.701.663.321.988l-4.204 3.602a.563.563 0 0 0-.182.557l1.285 5.385a.562.562 0 0 1-.84.61l-4.725-2.885a.562.562 0 0 0-.586 0L6.982 20.54a.562.562 0 0 1-.84-.61l1.285-5.386a.562.562 0 0 0-.182-.557l-4.204-3.602a.562.562 0 0 1 .321-.988l5.518-.442a.563.563 0 0 0 .475-.345L11.48 3.5Z" />
+ </svg>
+ {% endif %}
+ </button>
+ <div class="flex-1 min-w-0">
+ <div class="flex items-center justify-between gap-2">
+ <p class="text-sm truncate {% if not email.IsRead %}text-zinc-100 font-medium{% else %}text-zinc-400{% endif %}">
+ {% if email.FromName %}{{ email.FromName }}{% else %}{{ email.FromAddress }}{% endif %}
+ </p>
+ <span class="text-[10px] text-zinc-600 shrink-0">{{ email.CreatedAt|date:"Jan 2" }}</span>
+ </div>
+ <p class="text-xs truncate mt-0.5 {% if not email.IsRead %}text-zinc-300{% else %}text-zinc-500{% endif %}">{{ email.Subject }}</p>
+ <p class="text-xs text-zinc-600 truncate mt-0.5">{{ email.Snippet }}</p>
+ </div>
+ {% if email.AttachmentCount > 0 %}
+ <svg class="w-3.5 h-3.5 text-zinc-600 shrink-0 mt-1" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
+ <path stroke-linecap="round" stroke-linejoin="round" d="m18.375 12.739-7.693 7.693a4.5 4.5 0 0 1-6.364-6.364l10.94-10.94A3 3 0 1 1 19.5 7.372L8.552 18.32m.009-.01-.01.01m5.699-9.941-7.81 7.81a1.5 1.5 0 0 0 2.112 2.13" />
+ </svg>
+ {% endif %}
+ </div>
+ </div>
+ {% endfor %}
+</div>
+{% else %}
+<div class="flex flex-col items-center justify-center h-full text-center p-8">
+ <div class="flex items-center justify-center w-12 h-12 rounded-2xl bg-surface-800 mb-4">
+ <svg class="w-6 h-6 text-zinc-600" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
+ <path stroke-linecap="round" stroke-linejoin="round" d="M21.75 6.75v10.5a2.25 2.25 0 0 1-2.25 2.25h-15a2.25 2.25 0 0 1-2.25-2.25V6.75m19.5 0A2.25 2.25 0 0 0 19.5 4.5h-15a2.25 2.25 0 0 0-2.25 2.25m19.5 0v.243a2.25 2.25 0 0 1-1.07 1.916l-7.5 4.615a2.25 2.25 0 0 1-2.36 0L3.32 8.91a2.25 2.25 0 0 1-1.07-1.916V6.75" />
+ </svg>
+ </div>
+ <p class="text-sm text-zinc-400">No emails</p>
+ <p class="text-xs text-zinc-600 mt-1">This folder is empty</p>
+</div>
+{% endif %}
diff --git a/templates/mail/webmail/empty.django b/templates/mail/webmail/empty.django
new file mode 100644
index 0000000..d1c1c20
--- /dev/null
+++ b/templates/mail/webmail/empty.django
@@ -0,0 +1,9 @@
+<div class="flex flex-col items-center justify-center h-full text-center" id="webmail-empty-state">
+ <div class="flex items-center justify-center w-16 h-16 rounded-2xl bg-surface-800 mb-4">
+ <svg class="w-8 h-8 text-zinc-700" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
+ <path stroke-linecap="round" stroke-linejoin="round" d="M21.75 6.75v10.5a2.25 2.25 0 0 1-2.25 2.25h-15a2.25 2.25 0 0 1-2.25-2.25V6.75m19.5 0A2.25 2.25 0 0 0 19.5 4.5h-15a2.25 2.25 0 0 0-2.25 2.25m19.5 0v.243a2.25 2.25 0 0 1-1.07 1.916l-7.5 4.615a2.25 2.25 0 0 1-2.36 0L3.32 8.91a2.25 2.25 0 0 1-1.07-1.916V6.75" />
+ </svg>
+ </div>
+ <p class="text-sm text-zinc-500">Select an email to read</p>
+ <p class="text-xs text-zinc-600 mt-1">Or compose a new message</p>
+</div>
diff --git a/templates/mail/webmail/folders.django b/templates/mail/webmail/folders.django
new file mode 100644
index 0000000..ee6f47c
--- /dev/null
+++ b/templates/mail/webmail/folders.django
@@ -0,0 +1,73 @@
+<div class="flex flex-col h-full">
+ <div class="flex-1 overflow-y-auto p-2 space-y-0.5">
+ <p class="px-3 py-1.5 text-[10px] font-semibold text-zinc-600 uppercase tracking-wider">Folders</p>
+
+ {% for folder in folders %}
+ <a href="/mail/webmail/{{ active_mailbox.ID }}?folder={{ folder.Slug }}" hx-get="/mail/webmail/{{ active_mailbox.ID }}?folder={{ folder.Slug }}" hx-target="#content" hx-swap="innerHTML" hx-push-url="true" class="webmail-folder flex items-center justify-between w-full px-3 py-1.5 rounded-lg text-sm transition-colors duration-150 group {% if active_folder.ID == folder.ID and not is_starred_view %}bg-accent-500/10 text-accent-400{% else %}text-zinc-400 hover:text-zinc-200 hover:bg-white/[0.04]{% endif %}" data-folder-id="{{ folder.ID }}" data-folder-slug="{{ folder.Slug }}">
+ <span class="flex items-center gap-2.5 min-w-0">
+ {% if folder.Slug == "inbox" %}
+ <svg class="w-4 h-4 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
+ <path stroke-linecap="round" stroke-linejoin="round" d="M2.25 13.5h3.86a2.25 2.25 0 0 1 2.012 1.244l.256.512a2.25 2.25 0 0 0 2.013 1.244h2.21a2.25 2.25 0 0 0 2.013-1.244l.256-.512a2.25 2.25 0 0 1 2.013-1.244h3.859m-19.5.338V18a2.25 2.25 0 0 0 2.25 2.25h15A2.25 2.25 0 0 0 21.75 18v-4.162c0-.224-.034-.447-.1-.661L19.24 5.338a2.25 2.25 0 0 0-2.15-1.588H6.911a2.25 2.25 0 0 0-2.15 1.588L2.35 13.177a2.25 2.25 0 0 0-.1.661Z" />
+ </svg>
+ {% elif folder.Slug == "sent" %}
+ <svg class="w-4 h-4 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
+ <path stroke-linecap="round" stroke-linejoin="round" d="M6 12 3.269 3.125A59.769 59.769 0 0 1 21.485 12 59.768 59.768 0 0 1 3.27 20.875L5.999 12Zm0 0h7.5" />
+ </svg>
+ {% elif folder.Slug == "drafts" %}
+ <svg class="w-4 h-4 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
+ <path stroke-linecap="round" stroke-linejoin="round" d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L6.832 19.82a4.5 4.5 0 0 1-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 0 1 1.13-1.897L16.863 4.487Zm0 0L19.5 7.125" />
+ </svg>
+ {% elif folder.Slug == "spam" %}
+ <svg class="w-4 h-4 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
+ <path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126ZM12 15.75h.007v.008H12v-.008Z" />
+ </svg>
+ {% elif folder.Slug == "trash" %}
+ <svg class="w-4 h-4 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
+ <path stroke-linecap="round" stroke-linejoin="round" d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0" />
+ </svg>
+ {% else %}
+ <svg class="w-4 h-4 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
+ <path stroke-linecap="round" stroke-linejoin="round" d="M2.25 12.75V12A2.25 2.25 0 0 1 4.5 9.75h15A2.25 2.25 0 0 1 21.75 12v.75m-8.69-6.44-2.12-2.12a1.5 1.5 0 0 0-1.061-.44H4.5A2.25 2.25 0 0 0 2.25 6v12a2.25 2.25 0 0 0 2.25 2.25h15A2.25 2.25 0 0 0 21.75 18V9a2.25 2.25 0 0 0-2.25-2.25h-5.379a1.5 1.5 0 0 1-1.06-.44Z" />
+ </svg>
+ {% endif %}
+ <span class="truncate">{{ folder.Name }}</span>
+ </span>
+ <span class="flex items-center gap-1">
+ {% if folder.UnreadCount > 0 %}
+ <span class="text-[10px] font-medium px-1.5 py-0.5 rounded-full bg-accent-500/20 text-accent-400">{{ folder.UnreadCount }}</span>
+ {% endif %}
+ {% if not folder.IsSystem %}
+ <button type="button" class="p-0.5 rounded text-zinc-600 hover:text-red-400 opacity-0 group-hover:opacity-100 transition-all duration-150" onclick="event.preventDefault(); event.stopPropagation(); webmailConfirm({ title: 'Delete Folder', message: 'Are you sure you want to delete this folder?', confirmText: 'Delete', danger: true, onConfirm: function() { htmx.ajax('DELETE', '/mail/webmail/{{ active_mailbox.ID }}/folder/{{ folder.ID }}', {swap: 'none'}); }});">
+ <svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2">
+ <path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12" />
+ </svg>
+ </button>
+ {% endif %}
+ </span>
+ </a>
+ {% endfor %}
+
+ <a href="/mail/webmail/{{ active_mailbox.ID }}?folder=starred" hx-get="/mail/webmail/{{ active_mailbox.ID }}?folder=starred" hx-target="#content" hx-swap="innerHTML" hx-push-url="true" class="webmail-folder flex items-center justify-between w-full px-3 py-1.5 rounded-lg text-sm transition-colors duration-150 {% if is_starred_view %}bg-accent-500/10 text-accent-400{% else %}text-zinc-400 hover:text-zinc-200 hover:bg-white/[0.04]{% endif %}" data-folder-slug="starred">
+ <span class="flex items-center gap-2.5">
+ <svg class="w-4 h-4 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
+ <path stroke-linecap="round" stroke-linejoin="round" d="M11.48 3.499a.562.562 0 0 1 1.04 0l2.125 5.111a.563.563 0 0 0 .475.345l5.518.442c.499.04.701.663.321.988l-4.204 3.602a.563.563 0 0 0-.182.557l1.285 5.385a.562.562 0 0 1-.84.61l-4.725-2.885a.562.562 0 0 0-.586 0L6.982 20.54a.562.562 0 0 1-.84-.61l1.285-5.386a.562.562 0 0 0-.182-.557l-4.204-3.602a.562.562 0 0 1 .321-.988l5.518-.442a.563.563 0 0 0 .475-.345L11.48 3.5Z" />
+ </svg>
+ Starred
+ </span>
+ {% if starred_count > 0 %}
+ <span class="text-[10px] font-medium px-1.5 py-0.5 rounded-full bg-yellow-400/20 text-yellow-400">{{ starred_count }}</span>
+ {% endif %}
+ </a>
+ </div>
+
+ <div class="border-t border-white/[0.04] p-2">
+ <form hx-post="/mail/webmail/{{ active_mailbox.ID }}/folders" hx-swap="none" class="flex items-center gap-1">
+ <input type="text" name="name" placeholder="New folder..." class="flex-1 min-w-0 px-2 py-1 rounded bg-transparent border border-transparent text-xs text-zinc-400 placeholder-zinc-600 focus:outline-none focus:border-white/[0.06] transition-colors duration-150">
+ <button type="submit" class="p-1 rounded text-zinc-600 hover:text-zinc-300 shrink-0 transition-colors duration-150">
+ <svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
+ <path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
+ </svg>
+ </button>
+ </form>
+ </div>
+</div>
diff --git a/templates/mail/webmail/preview.django b/templates/mail/webmail/preview.django
new file mode 100644
index 0000000..967cb0f
--- /dev/null
+++ b/templates/mail/webmail/preview.django
@@ -0,0 +1,102 @@
+<div class="flex flex-col h-full">
+ <div class="flex items-center justify-between px-6 py-2 border-b border-white/[0.04] shrink-0">
+ <div class="flex items-center gap-1">
+ <button hx-put="/mail/webmail/email/{{ email.ID }}/star" hx-swap="none" class="webmail-btn-icon" title="{% if email.IsStarred %}Unstar{% else %}Star{% endif %}" data-preview-star="{{ email.ID }}">
+ {% if email.IsStarred %}
+ <svg class="w-4 h-4 text-yellow-400 fill-yellow-400" viewBox="0 0 24 24" stroke-width="1.5">
+ <path d="M11.48 3.499a.562.562 0 0 1 1.04 0l2.125 5.111a.563.563 0 0 0 .475.345l5.518.442c.499.04.701.663.321.988l-4.204 3.602a.563.563 0 0 0-.182.557l1.285 5.385a.562.562 0 0 1-.84.61l-4.725-2.885a.562.562 0 0 0-.586 0L6.982 20.54a.562.562 0 0 1-.84-.61l1.285-5.386a.562.562 0 0 0-.182-.557l-4.204-3.602a.562.562 0 0 1 .321-.988l5.518-.442a.563.563 0 0 0 .475-.345L11.48 3.5Z" />
+ </svg>
+ {% else %}
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
+ <path stroke-linecap="round" stroke-linejoin="round" d="M11.48 3.499a.562.562 0 0 1 1.04 0l2.125 5.111a.563.563 0 0 0 .475.345l5.518.442c.499.04.701.663.321.988l-4.204 3.602a.563.563 0 0 0-.182.557l1.285 5.385a.562.562 0 0 1-.84.61l-4.725-2.885a.562.562 0 0 0-.586 0L6.982 20.54a.562.562 0 0 1-.84-.61l1.285-5.386a.562.562 0 0 0-.182-.557l-4.204-3.602a.562.562 0 0 1 .321-.988l5.518-.442a.563.563 0 0 0 .475-.345L11.48 3.5Z" />
+ </svg>
+ {% endif %}
+ </button>
+
+ {% if email.IsRead %}
+ <button hx-put="/mail/webmail/email/{{ email.ID }}/unread" hx-swap="none" class="webmail-btn-icon" title="Mark as unread">
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
+ <path stroke-linecap="round" stroke-linejoin="round" d="M21.75 6.75v10.5a2.25 2.25 0 0 1-2.25 2.25h-15a2.25 2.25 0 0 1-2.25-2.25V6.75m19.5 0A2.25 2.25 0 0 0 19.5 4.5h-15a2.25 2.25 0 0 0-2.25 2.25m19.5 0v.243a2.25 2.25 0 0 1-1.07 1.916l-7.5 4.615a2.25 2.25 0 0 1-2.36 0L3.32 8.91a2.25 2.25 0 0 1-1.07-1.916V6.75" />
+ </svg>
+ </button>
+ {% else %}
+ <button hx-put="/mail/webmail/email/{{ email.ID }}/read" hx-swap="none" class="webmail-btn-icon" title="Mark as read">
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
+ <path stroke-linecap="round" stroke-linejoin="round" d="M21.75 9v.906a2.25 2.25 0 0 1-1.183 1.981l-6.478 3.488M2.25 9v.906a2.25 2.25 0 0 0 1.183 1.981l6.478 3.488m8.839 2.51-4.66-2.51m0 0-1.023-.55a2.25 2.25 0 0 0-2.134 0l-1.022.55m0 0-4.661 2.51m16.5 1.615a2.25 2.25 0 0 1-2.25 2.25h-15a2.25 2.25 0 0 1-2.25-2.25V8.844a2.25 2.25 0 0 1 1.183-1.981l7.5-4.039a2.25 2.25 0 0 1 2.134 0l7.5 4.039a2.25 2.25 0 0 1 1.183 1.98V19.5Z" />
+ </svg>
+ </button>
+ {% endif %}
+
+ <button hx-get="/mail/webmail/{{ active_mailbox.ID }}/compose?reply_to={{ email.ID }}" hx-target="#webmail-preview" hx-swap="innerHTML" class="webmail-btn-icon" title="Reply">
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
+ <path stroke-linecap="round" stroke-linejoin="round" d="M9 15 3 9m0 0 6-6M3 9h12a6 6 0 0 1 0 12h-3" />
+ </svg>
+ </button>
+ </div>
+
+ <div class="flex items-center gap-1">
+ <div class="dropdown" data-dropdown>
+ <button type="button" class="webmail-btn-icon" data-dropdown-trigger title="Move to folder">
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
+ <path stroke-linecap="round" stroke-linejoin="round" d="M2.25 12.75V12A2.25 2.25 0 0 1 4.5 9.75h15A2.25 2.25 0 0 1 21.75 12v.75m-8.69-6.44-2.12-2.12a1.5 1.5 0 0 0-1.061-.44H4.5A2.25 2.25 0 0 0 2.25 6v12a2.25 2.25 0 0 0 2.25 2.25h15A2.25 2.25 0 0 0 21.75 18V9a2.25 2.25 0 0 0-2.25-2.25h-5.379a1.5 1.5 0 0 1-1.06-.44Z" />
+ </svg>
+ </button>
+ <div class="dropdown-menu min-w-[10rem] right-0 left-auto" data-dropdown-menu>
+ <div class="dropdown-options" data-dropdown-options>
+ {% for folder in folders %}
+ <button type="button" class="dropdown-option whitespace-nowrap" hx-put="/mail/webmail/email/{{ email.ID }}/move" hx-vals='{"folder_id": {{ folder.ID }}}' hx-swap="none" data-dropdown-option data-value="{{ folder.ID }}" data-label="{{ folder.Name }}">
+ <p class="text-sm text-zinc-200">{{ folder.Name }}</p>
+ </button>
+ {% endfor %}
+ </div>
+ </div>
+ </div>
+
+ <button type="button" onclick="webmailConfirm({ title: 'Delete Email', message: '{% if folder_slug == "trash" %}Permanently delete this email?{% else %}Move this email to trash?{% endif %}', confirmText: '{% if folder_slug == "trash" %}Delete Permanently{% else %}Delete{% endif %}', danger: true, onConfirm: function() { htmx.ajax('DELETE', '/mail/webmail/{{ active_mailbox.ID }}/email/{{ email.ID }}', {swap: 'none'}); }})" class="webmail-btn-icon danger" title="Delete">
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
+ <path stroke-linecap="round" stroke-linejoin="round" d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0" />
+ </svg>
+ </button>
+ </div>
+ </div>
+
+ <div class="flex-1 overflow-y-auto p-6">
+ <div class="max-w-2xl">
+ <h2 class="text-lg font-medium text-zinc-100">{{ email.Subject }}</h2>
+
+ <div class="flex items-start gap-3 mt-4">
+ <div class="flex items-center justify-center w-10 h-10 rounded-full bg-accent-500/10 shrink-0">
+ <span class="text-sm font-medium text-accent-400">{% if email.FromName %}{{ email.FromName|first|upper }}{% else %}{{ email.FromAddress|first|upper }}{% endif %}</span>
+ </div>
+ <div class="flex-1 min-w-0">
+ <div class="flex items-center justify-between">
+ <div>
+ <p class="text-sm font-medium text-zinc-200">{% if email.FromName %}{{ email.FromName }}{% else %}{{ email.FromAddress }}{% endif %}</p>
+ <p class="text-xs text-zinc-500">{{ email.FromAddress }}</p>
+ </div>
+ <p class="text-xs text-zinc-600">{{ email.CreatedAt|date:"Jan 2, 2006 3:04 PM" }}</p>
+ </div>
+ <div class="mt-1 text-xs text-zinc-500">
+ <span>To: {{ email.ToAddresses }}</span>
+ {% if email.CcAddresses %}
+ <span class="ml-2">Cc: {{ email.CcAddresses }}</span>
+ {% endif %}
+ {% if email.BccAddresses %}
+ <span class="ml-2">Bcc: {{ email.BccAddresses }}</span>
+ {% endif %}
+ </div>
+ </div>
+ </div>
+
+ <div class="mt-6 border-t border-white/[0.04] pt-6">
+ <div class="prose prose-invert prose-sm max-w-none text-zinc-300 leading-relaxed">{% if email_body %}{{ email_body|safe }}{% else %}{{ email.Snippet }}{% endif %}</div>
+ </div>
+
+ {% if email.AttachmentCount > 0 %}
+ <div class="mt-6 border-t border-white/[0.04] pt-4">
+ <p class="text-xs text-zinc-500 mb-2">{{ email.AttachmentCount }} attachment{% if email.AttachmentCount > 1 %}s{% endif %}</p>
+ </div>
+ {% endif %}
+ </div>
+ </div>
+</div>
diff --git a/templates/partials/sidebar.django b/templates/partials/sidebar.django
index 1a6ab8c..01dd602 100644
--- a/templates/partials/sidebar.django
+++ b/templates/partials/sidebar.django
@@ -14,6 +14,7 @@
{% url "domains.tlds" as tlds_path %}
{% url "domains.manage" as manage_path %}
{% url "mail.index" as mail_path %}
+ {% url "mail.webmail" as webmail_path %}
{% url "mail.users" as users_path %}
{% url "mail.mailboxes" as mailboxes_path %}
@@ -77,6 +78,12 @@
</svg>
Mailboxes
</a>
+ <a href="{{ webmail_path }}" hx-get="{{ webmail_path }}" class="nav-link flex items-center gap-3 px-3 py-2 rounded-lg text-sm transition-colors duration-150">
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
+ <path stroke-linecap="round" stroke-linejoin="round" d="M9 3.75H6.912a2.25 2.25 0 0 0-2.15 1.588L2.35 13.177a2.25 2.25 0 0 0-.1.661V18a2.25 2.25 0 0 0 2.25 2.25h15A2.25 2.25 0 0 0 21.75 18v-4.162c0-.224-.034-.447-.1-.661L19.24 5.338a2.25 2.25 0 0 0-2.15-1.588H15M2.25 13.5h3.86a2.25 2.25 0 0 1 2.012 1.244l.256.512a2.25 2.25 0 0 0 2.013 1.244h2.21a2.25 2.25 0 0 0 2.013-1.244l.256-.512a2.25 2.25 0 0 1 2.013-1.244h3.859M12 3v8.25m0 0-3-3m3 3 3-3" />
+ </svg>
+ WebMail
+ </a>
</div>
</div>
</nav>