diff options
| author | Bobby <[email protected]> | 2026-03-08 08:11:41 +0530 |
|---|---|---|
| committer | Bobby <[email protected]> | 2026-03-08 08:11:41 +0530 |
| commit | b2a231280ce3265d20cdc5f317ae1bcc5eb59924 (patch) | |
| tree | 90eb1a5f5409025db47097e2e083361f8fa52555 /templates | |
| parent | 662dd2069dc8590e8b54823a33726464cf10c4e7 (diff) | |
| download | dove-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')
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 }} <{{ active_mailbox.Address }}></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 + '">×</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> |
