diff --git a/cmd/server/main.go b/cmd/server/main.go
index 657c805..b8283d9 100644
--- a/cmd/server/main.go
+++ b/cmd/server/main.go
@@ -135,6 +135,9 @@ func main() {
api.HandleFunc("/folders/{account_id:[0-9]+}", h.API.ListAccountFolders).Methods("GET")
api.HandleFunc("/folders/{id:[0-9]+}/sync", h.API.SyncFolder).Methods("POST")
api.HandleFunc("/folders/{id:[0-9]+}/visibility", h.API.SetFolderVisibility).Methods("PUT")
+ api.HandleFunc("/folders/{id:[0-9]+}/count", h.API.CountFolderMessages).Methods("GET")
+ api.HandleFunc("/folders/{id:[0-9]+}/move-to/{toId:[0-9]+}", h.API.MoveFolderContents).Methods("POST")
+ api.HandleFunc("/folders/{id:[0-9]+}", h.API.DeleteFolder).Methods("DELETE")
api.HandleFunc("/sync-interval", h.API.GetSyncInterval).Methods("GET")
api.HandleFunc("/sync-interval", h.API.SetSyncInterval).Methods("PUT")
diff --git a/internal/db/db.go b/internal/db/db.go
index 1290d92..7225657 100644
--- a/internal/db/db.go
+++ b/internal/db/db.go
@@ -1081,6 +1081,40 @@ func (d *DB) SetFolderVisibility(folderID, userID int64, isHidden, syncEnabled b
return err
}
+// CountFolderMessages returns how many messages are in a folder (owned by user).
+func (d *DB) CountFolderMessages(folderID, userID int64) (int, error) {
+ var count int
+ err := d.sql.QueryRow(`
+ SELECT COUNT(*) FROM messages m
+ JOIN folders f ON f.id=m.folder_id
+ JOIN email_accounts a ON a.id=f.account_id
+ WHERE m.folder_id=? AND a.user_id=?`, folderID, userID).Scan(&count)
+ return count, err
+}
+
+// DeleteFolder removes a folder and all its messages (cascade).
+func (d *DB) DeleteFolder(folderID, userID int64) error {
+ _, err := d.sql.Exec(`
+ DELETE FROM folders WHERE id=?
+ AND account_id IN (SELECT id FROM email_accounts WHERE user_id=?)`,
+ folderID, userID)
+ return err
+}
+
+// MoveFolderContents moves all messages from one folder to another (both must belong to user).
+func (d *DB) MoveFolderContents(fromID, toID, userID int64) (int64, error) {
+ res, err := d.sql.Exec(`
+ UPDATE messages SET folder_id=?
+ WHERE folder_id=?
+ AND folder_id IN (SELECT f.id FROM folders f JOIN email_accounts a ON a.id=f.account_id WHERE a.user_id=?)`,
+ toID, fromID, userID)
+ if err != nil {
+ return 0, err
+ }
+ n, _ := res.RowsAffected()
+ return n, nil
+}
+
func (d *DB) GetFolderByID(folderID int64) (*models.Folder, error) {
f := &models.Folder{}
var isHidden, syncEnabled int
diff --git a/internal/handlers/api.go b/internal/handlers/api.go
index fca7c5d..1fbd9c1 100644
--- a/internal/handlers/api.go
+++ b/internal/handlers/api.go
@@ -390,6 +390,39 @@ func (h *APIHandler) SetFolderVisibility(w http.ResponseWriter, r *http.Request)
h.writeJSON(w, map[string]bool{"ok": true})
}
+func (h *APIHandler) CountFolderMessages(w http.ResponseWriter, r *http.Request) {
+ userID := middleware.GetUserID(r)
+ folderID := pathInt64(r, "id")
+ count, err := h.db.CountFolderMessages(folderID, userID)
+ if err != nil {
+ h.writeError(w, http.StatusInternalServerError, "count failed")
+ return
+ }
+ h.writeJSON(w, map[string]int{"count": count})
+}
+
+func (h *APIHandler) DeleteFolder(w http.ResponseWriter, r *http.Request) {
+ userID := middleware.GetUserID(r)
+ folderID := pathInt64(r, "id")
+ if err := h.db.DeleteFolder(folderID, userID); err != nil {
+ h.writeError(w, http.StatusInternalServerError, "delete failed")
+ return
+ }
+ h.writeJSON(w, map[string]bool{"ok": true})
+}
+
+func (h *APIHandler) MoveFolderContents(w http.ResponseWriter, r *http.Request) {
+ userID := middleware.GetUserID(r)
+ fromID := pathInt64(r, "id")
+ toID := pathInt64(r, "toId")
+ moved, err := h.db.MoveFolderContents(fromID, toID, userID)
+ if err != nil {
+ h.writeError(w, http.StatusInternalServerError, "move failed")
+ return
+ }
+ h.writeJSON(w, map[string]interface{}{"ok": true, "moved": moved})
+}
+
func (h *APIHandler) SetAccountSyncSettings(w http.ResponseWriter, r *http.Request) {
userID := middleware.GetUserID(r)
accountID := pathInt64(r, "id")
diff --git a/web/static/css/gomail.css b/web/static/css/gomail.css
index 7cd873d..41c9ef4 100644
--- a/web/static/css/gomail.css
+++ b/web/static/css/gomail.css
@@ -191,9 +191,14 @@ body.app-page{overflow:hidden}
.message-item{padding:10px 12px;border-bottom:1px solid var(--border);cursor:pointer;transition:background .1s;position:relative}
.message-item:hover{background:var(--surface2)}
.message-item.active{background:var(--accent-dim);border-left:2px solid var(--accent);padding-left:10px}
-.message-item.unread .msg-subject{font-weight:500;color:var(--text)}
-.message-item.unread::before{content:'';position:absolute;left:0;top:50%;transform:translateY(-50%);
- width:3px;height:22px;background:var(--accent);border-radius:0 2px 2px 0}
+/* Unread: lighter background + bold sender so it pops clearly */
+.message-item.unread{background:rgba(255,255,255,.035)}
+.message-item.unread:hover{background:rgba(255,255,255,.055)}
+.message-item.unread .msg-from{color:var(--text);font-weight:600}
+.message-item.unread .msg-subject{font-weight:600;color:var(--text)}
+.message-item.unread::before{content:'';position:absolute;left:0;top:0;bottom:0;
+ width:3px;background:var(--accent);border-radius:0 2px 2px 0}
+.message-item.unread.active{background:var(--accent-dim)}
.message-item.unread.active::before{display:none}
.msg-top{display:flex;align-items:center;justify-content:space-between;gap:6px;margin-bottom:2px}
.msg-from{font-size:13px;font-weight:500;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1}
@@ -253,7 +258,7 @@ body.app-page{overflow:hidden}
cursor:grab;flex-shrink:0;background:var(--surface2);
}
.compose-dialog-header:active{cursor:grabbing}
-.compose-body-wrap{display:flex;flex-direction:column;flex:1;overflow:hidden}
+.compose-body-wrap{display:flex;flex-direction:column;flex:1;overflow:hidden;min-height:0}
.compose-title{font-size:13px;font-weight:500;pointer-events:none}
.compose-close{background:none;border:none;color:var(--muted);font-size:17px;cursor:pointer;
line-height:1;padding:2px 5px;border-radius:4px;pointer-events:all}
@@ -344,8 +349,8 @@ body.admin-page{overflow:auto;background:var(--bg)}
.fmt-btn{background:none;border:none;color:var(--text2);cursor:pointer;padding:4px 7px;border-radius:4px;font-size:13px;line-height:1;transition:background .1s}
.fmt-btn:hover{background:var(--border2);color:var(--text)}
.fmt-sep{width:1px;height:16px;background:var(--border2);margin:0 3px}
-.compose-editor{flex:1;min-height:160px;max-height:320px;overflow-y:auto;padding:12px 14px;
- font-size:13px;line-height:1.6;color:var(--text);outline:none;background:var(--bg)}
+.compose-editor{flex:1;overflow-y:auto;padding:12px 14px;
+ font-size:13px;line-height:1.6;color:var(--text);outline:none;background:var(--bg);min-height:0}
.compose-editor:empty::before{content:attr(placeholder);color:var(--muted);pointer-events:none}
.compose-editor blockquote{border-left:3px solid var(--border2);margin:8px 0;padding-left:12px;color:var(--muted)}
.compose-editor .quote-divider{font-size:11px;color:var(--muted);margin:10px 0 4px}
@@ -408,13 +413,13 @@ body.admin-page{overflow:auto;background:var(--bg)}
/* ── Inline confirm toast ────────────────────────────────────── */
.inline-confirm{
- position:fixed;bottom:24px;left:50%;transform:translateX(-50%) translateY(20px);
- background:var(--surface2);border:1px solid var(--border2);border-radius:10px;
- box-shadow:0 12px 40px rgba(0,0,0,.55);padding:16px 18px;
- min-width:280px;max-width:400px;z-index:400;
+ position:fixed;top:50%;left:50%;transform:translate(-50%,-44%);
+ background:var(--surface2);border:1px solid var(--border2);border-radius:12px;
+ box-shadow:0 24px 64px rgba(0,0,0,.7);padding:20px 22px;
+ min-width:300px;max-width:440px;z-index:400;
opacity:0;pointer-events:none;transition:opacity .18s,transform .18s;
}
-.inline-confirm.open{opacity:1;pointer-events:all;transform:translateX(-50%) translateY(0)}
+.inline-confirm.open{opacity:1;pointer-events:all;transform:translate(-50%,-50%)}
/* ── Folder no-sync indicator ────────────────────────────────── */
.folder-nosync{opacity:.65}
@@ -426,3 +431,20 @@ body.admin-page{overflow:auto;background:var(--bg)}
.icon-sync-btn{background:none;border:none;color:var(--muted);cursor:pointer;
padding:2px;border-radius:4px;line-height:1;flex-shrink:0;transition:color .15s}
.icon-sync-btn:hover{color:var(--text)}
+
+/* ── Message filter dropdown ─────────────────────────────────── */
+.filter-dropdown{position:relative}
+.filter-dropdown-btn{display:flex;align-items:center;gap:5px;background:none;
+ border:1px solid var(--border2);border-radius:6px;color:var(--muted);
+ font-family:'DM Sans',sans-serif;font-size:12px;cursor:pointer;
+ padding:4px 9px;transition:all .1s;white-space:nowrap}
+.filter-dropdown-btn:hover{background:var(--surface3);color:var(--text)}
+.filter-dropdown-btn.active{border-color:rgba(91,141,239,.4);color:var(--accent);background:var(--accent-dim)}
+.filter-dropdown-menu{position:absolute;top:calc(100% + 6px);right:0;
+ background:var(--surface2);border:1px solid var(--border2);border-radius:8px;
+ box-shadow:0 8px 28px rgba(0,0,0,.5);min-width:160px;padding:4px;
+ z-index:250}
+.filter-opt{padding:7px 12px;border-radius:5px;font-size:13px;cursor:pointer;
+ color:var(--text2);transition:background .1s;white-space:nowrap}
+.filter-opt:hover{background:var(--surface3);color:var(--text)}
+.filter-sep-line{height:1px;background:var(--border);margin:3px 0}
diff --git a/web/static/js/app.js b/web/static/js/app.js
index 411da65..4dc27ed 100644
--- a/web/static/js/app.js
+++ b/web/static/js/app.js
@@ -10,6 +10,9 @@ const S = {
remoteWhitelist: new Set(),
draftTimer: null, draftDirty: false,
composeVisible: false, composeMinimised: false,
+ // Message list filters
+ filterUnread: false,
+ sortOrder: 'date-desc', // 'date-desc' | 'date-asc' | 'size-desc'
};
// ── Boot ───────────────────────────────────────────────────────────────────
@@ -280,7 +283,6 @@ function renderFolders() {
const el=document.getElementById('folders-by-account');
const accMap={}; S.accounts.forEach(a=>accMap[a.id]=a);
const byAcc={};
- // Only show non-hidden folders
S.folders.filter(f=>!f.is_hidden).forEach(f=>{(byAcc[f.account_id]=byAcc[f.account_id]||[]).push(f);});
const prio=['inbox','sent','drafts','trash','spam','archive'];
el.innerHTML=Object.entries(byAcc).map(([accId,folders])=>{
@@ -291,7 +293,10 @@ function renderFolders() {
const sorted=[...prio.map(t=>folders.find(f=>f.folder_type===t)).filter(Boolean),...folders.filter(f=>f.folder_type==='custom')];
return `
`+sorted.map(f=>`
@@ -308,12 +313,15 @@ function showFolderMenu(e, folderId) {
const f = S.folders.find(f=>f.id===folderId);
if (!f) return;
const syncLabel = f.sync_enabled ? '⊘ Disable sync' : '↻ Enable sync';
- const hideLabel = '👁 Hide from sidebar';
+ const otherFolders = S.folders.filter(x=>x.id!==folderId&&x.account_id===f.account_id&&!x.is_hidden).slice(0,12);
+ const moveSub = otherFolders.map(x=>`
${esc(x.name)}
`).join('');
showCtxMenu(e, `
↻ Sync this folder
${syncLabel}
-
${hideLabel}
`);
+ ${moveSub?`
Move messages to
${moveSub}
`:''}
+
👁 Hide from sidebar
+
🗑 Delete folder
`);
}
async function syncFolderNow(folderId) {
@@ -335,14 +343,50 @@ async function toggleFolderSync(folderId) {
} else toast('Update failed','error');
}
-async function hideFolder(folderId) {
+async function confirmHideFolder(folderId) {
const f = S.folders.find(f=>f.id===folderId);
if (!f) return;
- const r = await api('PUT','/folders/'+folderId+'/visibility',{is_hidden:true, sync_enabled:false});
- if (r?.ok) {
- toast('Folder hidden. Manage in account settings.','success');
- await loadFolders();
- } else toast('Update failed','error');
+ inlineConfirm(
+ `Hide "${f.name}" from sidebar? You can unhide it from account settings.`,
+ async () => {
+ const r = await api('PUT','/folders/'+folderId+'/visibility',{is_hidden:true, sync_enabled:false});
+ if (r?.ok) { toast('Folder hidden','success'); await loadFolders(); }
+ else toast('Update failed','error');
+ }
+ );
+}
+
+async function confirmDeleteFolder(folderId) {
+ const f = S.folders.find(f=>f.id===folderId);
+ if (!f) return;
+ const countRes = await api('GET','/folders/'+folderId+'/count');
+ const count = countRes?.count ?? '?';
+ inlineConfirm(
+ `Delete folder "${f.name}"? This will permanently delete all ${count} message${count===1?'':'s'} inside it. This cannot be undone.`,
+ async () => {
+ const r = await api('DELETE','/folders/'+folderId);
+ if (r?.ok) {
+ toast('Folder deleted','success');
+ S.folders = S.folders.filter(x=>x.id!==folderId);
+ if (S.currentFolder===folderId) selectFolder('unified','Unified Inbox');
+ renderFolders(); loadMessages();
+ } else toast(r?.error||'Delete failed','error');
+ }
+ );
+}
+
+async function moveFolderContents(fromId, toId) {
+ const from = S.folders.find(f=>f.id===fromId);
+ const to = S.folders.find(f=>f.id===toId);
+ if (!from||!to) return;
+ inlineConfirm(
+ `Move all messages from "${from.name}" into "${to.name}"?`,
+ async () => {
+ const r = await api('POST','/folders/'+fromId+'/move-to/'+toId);
+ if (r?.ok) { toast(`Moved ${r.moved||0} messages`,'success'); loadFolders(); loadMessages(); }
+ else toast(r?.error||'Move failed','error');
+ }
+ );
}
function updateUnreadBadge() {
@@ -386,13 +430,52 @@ async function loadMessages(append) {
document.getElementById('panel-count').textContent=S.totalMessages>0?S.totalMessages+' messages':'';
}
+function setFilter(mode) {
+ S.filterUnread = (mode === 'unread');
+ S.sortOrder = (mode === 'unread' || mode === 'default') ? 'date-desc' : mode;
+
+ // Update checkmarks
+ ['default','unread','date-desc','date-asc','size-desc'].forEach(k => {
+ const el = document.getElementById('fopt-'+k);
+ if (el) el.textContent = (k === mode ? '✓ ' : '○ ') + el.textContent.slice(2);
+ });
+
+ // Update button label
+ const labels = {
+ 'default':'Filter', 'unread':'Unread', 'date-desc':'↓ Date',
+ 'date-asc':'↑ Date', 'size-desc':'↓ Size'
+ };
+ const labelEl = document.getElementById('filter-label');
+ if (labelEl) {
+ labelEl.textContent = labels[mode] || 'Filter';
+ labelEl.style.color = mode !== 'default' ? 'var(--accent)' : '';
+ }
+ const menuEl = document.getElementById('filter-dropdown-menu');
+ if (menuEl) menuEl.style.display = 'none';
+ renderMessageList();
+}
+
+// Keep old names as aliases so nothing else breaks
+function toggleFilterUnread() { setFilter(S.filterUnread ? 'default' : 'unread'); }
+function setSortOrder(order) { setFilter(order); }
+
function renderMessageList() {
const list=document.getElementById('message-list');
- if (!S.messages.length){
- list.innerHTML=`
`;
+ let msgs = [...S.messages];
+
+ // Filter
+ if (S.filterUnread) msgs = msgs.filter(m => !m.is_read);
+
+ // Sort
+ if (S.sortOrder === 'date-asc') msgs.sort((a,b) => new Date(a.date)-new Date(b.date));
+ else if (S.sortOrder === 'size-desc') msgs.sort((a,b) => (b.size||0)-(a.size||0));
+ else msgs.sort((a,b) => new Date(b.date)-new Date(a.date)); // date-desc default
+
+ if (!msgs.length){
+ list.innerHTML=`
${S.filterUnread?'No unread messages':'No messages'}
`;
return;
}
- list.innerHTML=S.messages.map(m=>`
+ list.innerHTML=msgs.map(m=>`
@@ -404,6 +487,7 @@ function renderMessageList() {
@@ -538,9 +622,13 @@ async function markRead(id, read) {
}
async function moveMessage(msgId, folderId) {
- const r=await api('PUT','/messages/'+msgId+'/move',{folder_id:folderId});
- if(r?.ok){toast('Moved','success');S.messages=S.messages.filter(m=>m.id!==msgId);renderMessageList();
- if(S.currentMessage?.id===msgId)resetDetail();loadFolders();}
+ const folder = S.folders.find(f=>f.id===folderId);
+ inlineConfirm(`Move this message to "${folder?.name||'selected folder'}"?`, async () => {
+ const r=await api('PUT','/messages/'+msgId+'/move',{folder_id:folderId});
+ if(r?.ok){toast('Moved','success');S.messages=S.messages.filter(m=>m.id!==msgId);renderMessageList();
+ if(S.currentMessage?.id===msgId)resetDetail();loadFolders();}
+ else toast('Move failed','error');
+ });
}
async function deleteMessage(id) {
@@ -771,13 +859,39 @@ async function sendMessage() {
}
// ── Compose drag + all-edge resize ─────────────────────────────────────────
+function saveComposeGeometry(dlg) {
+ const r = dlg.getBoundingClientRect();
+ document.cookie = `compose_geo=${JSON.stringify({l:Math.round(r.left),t:Math.round(r.top),w:Math.round(r.width),h:Math.round(r.height)})};path=/;max-age=31536000`;
+}
+
+function loadComposeGeometry(dlg) {
+ try {
+ const m = document.cookie.match(/compose_geo=([^;]+)/);
+ if (!m) return false;
+ const g = JSON.parse(decodeURIComponent(m[1]));
+ if (!g.w||!g.h) return false;
+ const maxL = window.innerWidth - Math.max(360, g.w);
+ const maxT = window.innerHeight - Math.max(280, g.h);
+ dlg.style.left = Math.max(0, Math.min(g.l, maxL)) + 'px';
+ dlg.style.top = Math.max(0, Math.min(g.t, maxT)) + 'px';
+ dlg.style.width = Math.max(360, g.w) + 'px';
+ dlg.style.height = Math.max(280, g.h) + 'px';
+ dlg.style.right = 'auto'; dlg.style.bottom = 'auto';
+ const editor = document.getElementById('compose-editor');
+ if (editor) editor.style.height = (Math.max(280,g.h) - 242) + 'px';
+ return true;
+ } catch(e) { return false; }
+}
+
function initComposeDragResize() {
const dlg=document.getElementById('compose-dialog');
if(!dlg) return;
- // Default position — bottom-right
- dlg.style.right='24px'; dlg.style.bottom='20px';
- dlg.style.left='auto'; dlg.style.top='auto';
+ // Restore saved position/size, or fall back to default bottom-right
+ if (!loadComposeGeometry(dlg)) {
+ dlg.style.right='24px'; dlg.style.bottom='20px';
+ dlg.style.left='auto'; dlg.style.top='auto';
+ }
// Drag by header
const header=document.getElementById('compose-drag-handle');
@@ -793,7 +907,7 @@ function initComposeDragResize() {
dlg.style.left=Math.max(0,Math.min(window.innerWidth-dlg.offsetWidth, startL+(ev.clientX-ox)))+'px';
dlg.style.top= Math.max(0,Math.min(window.innerHeight-30, startT+(ev.clientY-oy)))+'px';
};
- const mu=()=>{ document.removeEventListener('mousemove',mm); document.removeEventListener('mouseup',mu); };
+ const mu=()=>{ document.removeEventListener('mousemove',mm); document.removeEventListener('mouseup',mu); saveComposeGeometry(dlg); };
document.addEventListener('mousemove',mm);
document.addEventListener('mouseup',mu);
e.preventDefault();
@@ -820,7 +934,7 @@ function initComposeDragResize() {
const editor=document.getElementById('compose-editor');
if(editor) editor.style.height=(h-242)+'px';
};
- const mu=()=>{ document.removeEventListener('mousemove',mm); document.removeEventListener('mouseup',mu); };
+ const mu=()=>{ document.removeEventListener('mousemove',mm); document.removeEventListener('mouseup',mu); saveComposeGeometry(dlg); };
document.addEventListener('mousemove',mm);
document.addEventListener('mouseup',mu);
e.preventDefault();
@@ -898,12 +1012,34 @@ function showCtxMenu(e, html) {
});
}
-// ── Init tag fields on DOMContentLoaded ───────────────────────────────────
-document.addEventListener('DOMContentLoaded', ()=>{
- // Tag fields are re-init each time openCompose() is called.
- // Initial init here covers the first open.
+// ── Init tag fields and filter dropdown ───────────────────────────────────
+// app.js loads at the bottom of so the DOM is already ready here —
+// we must NOT wrap in DOMContentLoaded (that event has already fired).
+function _bootApp() {
initTagField('compose-to');
initTagField('compose-cc-tags');
initTagField('compose-bcc-tags');
+
+ // Filter dropdown
+ const dropBtn = document.getElementById('filter-dropdown-btn');
+ const dropMenu = document.getElementById('filter-dropdown-menu');
+ if (dropBtn && dropMenu) {
+ dropBtn.addEventListener('click', e => {
+ e.stopPropagation();
+ const isOpen = dropMenu.classList.contains('open');
+ dropMenu.classList.toggle('open', !isOpen);
+ if (!isOpen) {
+ document.addEventListener('click', () => dropMenu.classList.remove('open'), {once:true});
+ }
+ });
+ ['default','unread','date-desc','date-asc','size-desc'].forEach(mode => {
+ const el = document.getElementById('fopt-'+mode);
+ if (el) el.addEventListener('click', e => { e.stopPropagation(); setFilter(mode); });
+ });
+ }
+
init();
-});
+}
+
+// Run immediately — DOM is ready since this script is at end of
+_bootApp();
diff --git a/web/static/js/gomail.js b/web/static/js/gomail.js
index f315098..99533c9 100644
--- a/web/static/js/gomail.js
+++ b/web/static/js/gomail.js
@@ -108,3 +108,24 @@ function insertLink() {
document.getElementById('compose-editor').focus();
document.execCommand('createLink', false, url);
}
+
+// ── Filter dropdown (stubs — real logic in app.js, but onclick needs global scope) ──
+function goMailToggleFilter(e) {
+ e.stopPropagation();
+ const menu = document.getElementById('filter-dropdown-menu');
+ if (!menu) return;
+ const isOpen = menu.classList.contains('open');
+ menu.classList.toggle('open', !isOpen);
+ if (!isOpen) {
+ document.addEventListener('click', function closeFilter() {
+ menu.classList.remove('open');
+ document.removeEventListener('click', closeFilter);
+ });
+ }
+}
+
+function goMailSetFilter(mode) {
+ var menu = document.getElementById('filter-dropdown-menu');
+ if (menu) menu.style.display = 'none';
+ if (typeof setFilter === 'function') setFilter(mode);
+}
diff --git a/web/templates/app.html b/web/templates/app.html
index c5c7116..6346745 100644
--- a/web/templates/app.html
+++ b/web/templates/app.html
@@ -50,7 +50,24 @@
@@ -147,7 +164,7 @@
-
+
@@ -283,5 +300,5 @@
{{end}}
{{define "scripts"}}
-
+
{{end}}
diff --git a/web/templates/base.html b/web/templates/base.html
index f9d59e7..e4ca24e 100644
--- a/web/templates/base.html
+++ b/web/templates/base.html
@@ -5,13 +5,13 @@
{{block "title" .}}GoMail{{end}}
-
+
{{block "head_extra" .}}{{end}}
{{block "body" .}}{{end}}
-
+
{{block "scripts" .}}{{end}}
-{{end}}
\ No newline at end of file
+{{end}}