2026-08-14 13:04:55 +01:00
{{define "webmail_folder.html"}}
<!DOCTYPE html>
< html lang = "en" data-bs-theme = "dark" >
< head >
< meta charset = "UTF-8" >
< meta name = "viewport" content = "width=device-width, initial-scale=1.0" >
< title > {{if .search_query}}Search: {{.search_query}}{{else}}{{.active_folder}}{{end}} - Webmail</ title >
< link href = "/webmail/static/vendor/bootstrap/css/bootstrap.min.css" rel = "stylesheet" >
< link href = "/webmail/static/vendor/bootstrap-icons/font/bootstrap-icons.css" rel = "stylesheet" >
< style >
2026-08-15 12:35:44 +01:00
html , body { background-color : #1a1a1a ; color : #e0e0e0 ; height : 100 % ; }
2026-08-14 13:04:55 +01:00
. card { background-color : #2d2d2d ; border : 1 px solid #404040 ; }
. folder-link . active { background-color : #0d6efd ; color : #fff !important ; }
. msg-row { cursor : grab ; }
. msg-row . dragging { opacity : 0.4 ; }
. folder-link . drop-hover { background-color : #0d6efd ; color : #fff !important ; outline : 2 px dashed #6ea8fe ; outline-offset : -2 px ; }
. folder-unread-badge { font-size : .7 rem ; }
. msg-row-older { display : none ; }
. msg-group-toggle { cursor : pointer ; }
2026-08-15 12:35:44 +01:00
/* A search result can span multiple folders, and the bulk-action endpoint is
scoped to one folder path — rather than a bulk action silently no-oping on
every row from a different folder, selection/bulk actions are just not
offered while searching (per-row actions in the reading pane still work). */
. search-mode . msg-check , . search-mode # selectAllCheck , . search-mode . bulk-btn { display : none ; }
/* Outlook-style three-pane shell: fixed-width sidebar + fixed-width list +
flexible reading pane, instead of a responsive 12-column grid — this is
deliberately a fixed desktop layout to match the reference, not a
mobile-first one. */
. mail-shell { display : flex ; align-items : stretch ; height : calc ( 100 vh - 56 px ); overflow : hidden ; }
. mail-sidebar { width : 230 px ; flex : 0 0 auto ; overflow-y : auto ; border-right : 1 px solid #404040 ; padding : .75 rem ; }
. mail-list-pane { width : 380 px ; flex : 0 0 auto ; overflow-y : auto ; border-right : 1 px solid #404040 ; display : flex ; flex-direction : column ; }
. mail-reading-pane { flex : 1 1 auto ; overflow-y : auto ; padding : 1.5 rem ; min-width : 0 ; }
. mail-toolbar { flex : 0 0 auto ; padding : .5 rem .75 rem ; border-bottom : 1 px solid #404040 ; display : flex ; align-items : center ; gap : .35 rem ; flex-wrap : wrap ; }
. mail-list-scroll { flex : 1 1 auto ; overflow-y : auto ; }
. mail-list-header { padding : .35 rem .75 rem ; font-size : .75 rem ; text-transform : uppercase ; color : #8a8a8a ; display : flex ; justify-content : space-between ; }
. msg-item { display : flex ; align-items : flex-start ; gap : .6 rem ; padding : .55 rem .75 rem ; border-bottom : 1 px solid #333 ; cursor : pointer ; }
. msg-item : hover { background-color : #262626 ; }
. msg-item . active { background-color : #0d3860 ; }
. msg-item . unread . msg-subject { font-weight : 700 ; color : #fff ; }
. msg-item . unread . msg-from { font-weight : 700 ; color : #fff ; }
. msg-check { margin-top : .35 rem ; flex : 0 0 auto ; }
. msg-avatar { width : 34 px ; height : 34 px ; border-radius : 50 % ; background : #495057 ; color : #fff ; display : flex ; align-items : center ; justify-content : center ; font-size : .85 rem ; font-weight : 600 ; flex : 0 0 auto ; }
. msg-item . unread . msg-avatar { background : #0d6efd ; }
. msg-main { min-width : 0 ; flex : 1 1 auto ; }
. msg-row1 { display : flex ; justify-content : space-between ; gap : .5 rem ; }
. msg-from { overflow : hidden ; text-overflow : ellipsis ; white-space : nowrap ; }
. msg-date { flex : 0 0 auto ; font-size : .75 rem ; color : #8a8a8a ; }
. msg-row2 { overflow : hidden ; text-overflow : ellipsis ; white-space : nowrap ; font-size : .85 rem ; color : #adb5bd ; }
. msg-subject { color : #e0e0e0 ; }
. msg-body-html { background-color : #fff ; color : #000 ; border-radius : 6 px ; padding : 1 rem ; overflow-x : auto ; }
. msg-body-text { white-space : pre-wrap ; word-break : break-word ; }
# readingPaneBody . pane-toolbar { border-bottom : 1 px solid #404040 ; padding-bottom : .75 rem ; }
2026-08-14 13:04:55 +01:00
</ style >
</ head >
< body >
{{template "csrf_script" .}}
2026-08-15 12:35:44 +01:00
< nav class = "navbar navbar-expand-lg navbar-dark bg-dark px-3" style = "height: 56px;" >
< span class = "navbar-brand mb-0 h1" >< i class = "bi bi-envelope-fill me-2" ></ i > Webmail < small class = "text-muted" > {{.mailbox.Email}}</ small ></ span >
< form method = "get" action = "/webmail/mail/search" class = "mx-auto" style = "width: 360px;" >
< div class = "input-group input-group-sm" >
< span class = "input-group-text bg-body-secondary border-0" >< i class = "bi bi-search" ></ i ></ span >
< input type = "search" name = "q" id = "mailSearchInput" class = "form-control" placeholder = "Search all mail" value = "{{.search_query}}" >
2026-08-14 13:04:55 +01:00
</ div >
2026-08-15 12:35:44 +01:00
</ form >
< div class = "navbar-nav flex-row gap-2 ms-auto" >
< button type = "button" onclick = "openCompose('/webmail/mail/compose')" class = "btn btn-primary btn-sm" >< i class = "bi bi-pencil-square me-1" ></ i > Compose</ button >
< a href = "/webmail/rules" class = "btn btn-outline-light btn-sm" title = "Rules" >< i class = "bi bi-funnel" ></ i ></ a >
< a href = "/webmail/certs" class = "btn btn-outline-light btn-sm" title = "Certs" >< i class = "bi bi-shield-lock" ></ i ></ a >
< a href = "/webmail/account" class = "btn btn-outline-light btn-sm" title = "Account" >< i class = "bi bi-gear" ></ i ></ a >
< form method = "post" action = "/webmail/logout" class = "d-inline" >
< button type = "submit" class = "btn btn-outline-light btn-sm" title = "Sign out" >< i class = "bi bi-box-arrow-right" ></ i ></ button >
</ form >
2026-08-14 13:04:55 +01:00
</ div >
</ nav >
< div class = "toast-container position-fixed top-0 end-0 p-3" style = "z-index: 1090;" >
{{range .flashes}}
< div class = "toast align-items-center text-bg-{{if eq .Category " error "}} danger {{ else }}{{. Category }}{{ end }} border-0 " role = "alert" aria-live = "assertive" aria-atomic = "true" data-bs-autohide = "false" >
< div class = "d-flex" >
< div class = "toast-body" >
< i class = "bi bi-{{if eq .Category " error "}} exclamation-triangle {{ else if eq . Category " success "}} check-circle {{ else }} info-circle {{ end }} me-2 " ></ i >
{{.Message}}
</ div >
< button type = "button" class = "btn-close btn-close-white me-2 m-auto" data-bs-dismiss = "toast" aria-label = "Close" ></ button >
</ div >
</ div >
{{end}}
</ div >
2026-08-15 12:35:44 +01:00
< div class = "mail-shell" >
< div class = "mail-sidebar" id = "folderSidebarCol" >
< div class = "d-flex justify-content-between align-items-center mb-2" >
< span class = "text-muted small text-uppercase" > Folders</ span >
< button type = "button" class = "btn btn-sm btn-outline-secondary border-0 py-0" id = "sidebarCollapseBtn" title = "Hide folder list" >< i class = "bi bi-chevron-bar-left" ></ i ></ button >
</ div >
< div class = "list-group list-group-flush" >
{{$active := .active_folder}}
{{$unread := .unread_counts}}
{{$counts := .folder_counts}}
{{range .folders}}
< div class = "d-flex align-items-center folder-row" >
< a href = "/webmail/mail/{{.}}" data-folder = "{{.}}" class = "list-group-item list-group-item-action bg-transparent text-white folder-link flex-grow-1 d-flex justify-content-between align-items-center {{if eq . $active}}active{{end}}" >
< span >< i class = "bi bi-folder2 me-1" ></ i > {{.}}</ span >
{{$n := index $unread .}}
{{$total := index $counts .}}
{{if $total}}
< span class = "badge {{if $n}}bg-primary{{else}}bg-secondary{{end}} rounded-pill folder-unread-badge" title = "{{$total}} total{{if $n}}, {{$n}} unread{{end}}" > {{$total}}{{if $n}} / < strong > {{$n}}</ strong > {{end}}</ span >
{{end}}
</ a >
{{if not (isStandardFolder .)}}
< form method = "post" action = "/webmail/mail/folders/{{.}}/remove" class = "d-inline" >
< button type = "submit" class = "btn btn-sm btn-outline-danger border-0" title = "Remove folder" data-confirm = "Remove folder "{{.}}"? Any mail in it moves to INBOX." >< i class = "bi bi-x-lg" ></ i ></ button >
</ form >
{{end}}
</ div >
{{end}}
</ div >
< hr class = "my-2" >
< form method = "post" action = "/webmail/mail/folders/add" class = "d-flex gap-1" >
< input type = "text" class = "form-control form-control-sm" name = "name" placeholder = "New folder" maxlength = "60" required >
< button type = "submit" class = "btn btn-sm btn-outline-primary" title = "Create folder" >< i class = "bi bi-plus-lg" ></ i ></ button >
</ form >
</ div >
< div class = "mail-list-pane{{if .search_query}} search-mode{{end}}" id = "messageListCol" >
< div class = "mail-toolbar" >
< button type = "button" class = "btn btn-sm btn-outline-secondary border-0 py-0 d-none" id = "sidebarShowBtn" title = "Show folder list" >< i class = "bi bi-chevron-bar-right" ></ i ></ button >
< input type = "checkbox" class = "form-check-input" id = "selectAllCheck" title = "Select all" >
< div class = "btn-group btn-group-sm" >
< button type = "button" class = "btn btn-outline-secondary bulk-btn" data-action = "delete" disabled title = "Delete" >< i class = "bi bi-trash" ></ i ></ button >
< button type = "button" class = "btn btn-outline-secondary bulk-btn" data-action = "read" disabled title = "Mark as read" >< i class = "bi bi-envelope-open" ></ i ></ button >
< button type = "button" class = "btn btn-outline-secondary bulk-btn" data-action = "unread" disabled title = "Mark as unread" >< i class = "bi bi-envelope" ></ i ></ button >
</ div >
{{if not .search_query}}
< select class = "form-select form-select-sm bulk-move-select" id = "bulkMoveSelect" disabled style = "width: auto;" title = "Move selected to…" >
< option value = "" > Move to… </ option >
{{$folder := .active_folder}}
{{range .folders}}{{if ne . $folder}}< option value = "{{.}}" > {{.}}</ option > {{end}}{{end}}
</ select >
{{end}}
< button type = "button" class = "btn btn-sm btn-outline-secondary border-0" onclick = "location.reload()" title = "Refresh" >< i class = "bi bi-arrow-clockwise" ></ i ></ button >
< div class = "ms-auto d-flex align-items-center gap-1" >
{{if not .search_query}}
< a href = "{{.sort_from_href}}" class = "btn btn-sm btn-outline-secondary border-0 py-0" title = "Sort by sender" >< i class = "bi bi-person{{if eq .sort_by " from "}} -fill {{ end }}" ></ i > {{if eq .sort_by "from"}} < i class = "bi bi-caret-{{if eq .sort_dir " asc "}} up {{ else }} down {{ end }} -fill " ></ i > {{end}}</ a >
< a href = "{{.sort_date_href}}" class = "btn btn-sm btn-outline-secondary border-0 py-0" title = "Sort by date" >< i class = "bi bi-calendar3{{if ne .sort_by " from "}} -fill {{ end }}" ></ i > {{if ne .sort_by "from"}} < i class = "bi bi-caret-{{if eq .sort_dir " asc "}} up {{ else }} down {{ end }} -fill " ></ i > {{end}}</ a >
< a href = "{{.unread_only_href}}" class = "btn btn-sm border-0 py-0 {{if .unread_only}}btn-primary{{else}}btn-outline-secondary{{end}}" title = "Unread only" >< i class = "bi bi-envelope-fill" ></ i ></ a >
{{end}}
2026-08-14 13:04:55 +01:00
</ div >
</ div >
2026-08-15 12:35:44 +01:00
< div class = "mail-list-header" >
< span > {{if .search_query}}Search: “ {{.search_query}}” {{else}}{{.active_folder}}{{end}}</ span >
< span > {{.total}} message{{if ne .total 1}}s{{end}}</ span >
</ div >
< div class = "mail-list-scroll" id = "mailListScroll" >
{{if .messages}}
{{range .messages}}
{{$rowHref := printf "/webmail/mail/%s/%d" .Folder .ID}}
{{$paneHref := printf "/webmail/mail/%s/%d/pane" .Folder .ID}}
{{$isDraft := eq .Folder "Drafts"}}
{{if $isDraft}}{{$rowHref = printf "/webmail/mail/compose?draft=%d& folder=Drafts" .ID}}{{end}}
{{$displayName := senderName .CachedFrom}}
{{if eq .Folder "Sent"}}{{if .CachedTo}}{{$displayName = senderName .CachedTo}}{{else}}{{$displayName = "(no recipient)"}}{{end}}{{end}}
< div class = "msg-item {{if .Unread}}unread{{end}}{{if .Collapsed}} msg-row-older{{end}} msg-row" draggable = "true" data-uid = "{{.ID}}" data-folder = "{{.Folder}}" data-href = "{{$rowHref}}" data-pane-href = "{{$paneHref}}" data-is-draft = "{{$isDraft}}" >
< input type = "checkbox" class = "form-check-input msg-check" value = "{{.ID}}" onclick = "event.stopPropagation()" >
< div class = "msg-avatar" > {{initial $displayName}}</ div >
< div class = "msg-main" >
< div class = "msg-row1" >
< span class = "msg-from" title = "{{if eq .Folder " Sent "}}{{. CachedTo }}{{ else }}{{. CachedFrom }}{{ end }}" > {{$displayName}}</ span >
< span class = "msg-date" > {{strftime "%Y-%m-%d %H:%M" .InternalDate}}</ span >
</ div >
< div class = "msg-row2" >
< span class = "msg-subject" > {{if .CachedSubject}}{{.CachedSubject}}{{else}}(no subject){{end}}</ span >
{{if gt .GroupExtra 0}}< span class = "badge bg-secondary msg-group-toggle" data-group-toggle = "{{.ID}}" > +{{.GroupExtra}} more</ span > {{end}}
{{if .CachedPreview}} – {{.CachedPreview}}{{end}}
</ div >
2026-08-14 13:04:55 +01:00
</ div >
</ div >
2026-08-15 12:35:44 +01:00
{{end}}
{{else}}
< div class = "text-center py-5" >
< i class = "bi bi-inbox text-muted" style = "font-size: 3rem;" ></ i >
< h6 class = "text-muted mt-3" > No messages</ h6 >
2026-08-14 13:04:55 +01:00
</ div >
2026-08-15 12:35:44 +01:00
{{end}}
</ div >
{{if or .has_prev .has_next}}
< div class = "d-flex justify-content-between p-2 border-top" style = "border-color: #404040 !important;" >
{{if .has_prev}}< a href = "{{.prev_href}}" class = "btn btn-outline-secondary btn-sm" > « Newer</ a > {{else}}< span ></ span > {{end}}
{{if .has_next}}< a href = "{{.next_href}}" class = "btn btn-outline-secondary btn-sm" > Older » </ a > {{end}}
</ div >
{{end}}
</ div >
< div class = "mail-reading-pane" id = "readingPaneBody" >
< div class = "text-center text-muted py-5" >
< i class = "bi bi-envelope-open" style = "font-size: 3rem;" ></ i >
< p class = "mt-3" > Select a message to read</ p >
2026-08-14 13:04:55 +01:00
</ div >
</ div >
</ div >
2026-08-15 12:35:44 +01:00
< form method = "post" id = "bulkActionForm" class = "d-none" >
< input type = "hidden" name = "action" id = "bulkActionField" >
< input type = "hidden" name = "target_folder" id = "bulkTargetFolderField" >
</ form >
2026-08-14 13:04:55 +01:00
< div class = "modal fade" id = "confirmationModal" tabindex = "-1" aria-hidden = "true" >
< div class = "modal-dialog" >
< div class = "modal-content" >
< div class = "modal-header" >
< h5 class = "modal-title" >< i class = "bi bi-question-circle me-2" ></ i > Confirm Action</ h5 >
< button type = "button" class = "btn-close" data-bs-dismiss = "modal" aria-label = "Close" ></ button >
</ div >
< div class = "modal-body" id = "confirmationModalBody" > Are you sure you want to proceed?</ div >
< div class = "modal-footer" >
< button type = "button" class = "btn btn-secondary" data-bs-dismiss = "modal" > Cancel</ button >
< button type = "button" class = "btn btn-danger" id = "confirmationModalConfirm" > Confirm</ button >
</ div >
</ div >
</ div >
</ div >
{{template "compose_widget" .}}
{{template "webmail_shortcuts" .}}
< script src = "/webmail/static/vendor/bootstrap/js/bootstrap.bundle.min.js" ></ script >
< script >
document . addEventListener ( 'DOMContentLoaded' , function () {
document . querySelectorAll ( '.toast' ). forEach ( function ( el ) { new bootstrap . Toast ( el , { delay : 5000 }). show (); });
});
function showConfirmation ( message ) {
return new Promise (( resolve ) => {
const modal = document . getElementById ( 'confirmationModal' );
document . getElementById ( 'confirmationModalBody' ). textContent = message ;
const confirmButton = document . getElementById ( 'confirmationModalConfirm' );
const handleConfirm = () => { resolve ( true ); bootstrap . Modal . getInstance ( modal ). hide (); cleanup (); };
const handleCancel = () => { resolve ( false ); cleanup (); };
const cleanup = () => {
confirmButton . removeEventListener ( 'click' , handleConfirm );
modal . removeEventListener ( 'hidden.bs.modal' , handleCancel );
};
confirmButton . addEventListener ( 'click' , handleConfirm );
modal . addEventListener ( 'hidden.bs.modal' , handleCancel , { once : true });
new bootstrap . Modal ( modal ). show ();
});
}
document . addEventListener ( 'DOMContentLoaded' , function () {
document . querySelectorAll ( '[data-confirm]' ). forEach ( function ( button ) {
button . addEventListener ( 'click' , async function ( e ) {
e . preventDefault ();
if ( await showConfirmation ( this . getAttribute ( 'data-confirm' ))) {
const form = this . closest ( 'form' );
if ( form ) form . submit ();
}
});
});
});
// Drag a message row onto a folder in the sidebar to move it there — a
2026-08-15 12:35:44 +01:00
// shortcut for the toolbar's "Move to..." control. Each row carries its OWN
// folder (data-folder) rather than assuming the page's active folder, since a
// search result can span multiple folders.
2026-08-14 13:04:55 +01:00
( function () {
let draggedUID = null ;
let draggedFolder = null ;
document . querySelectorAll ( '.msg-row' ). forEach ( function ( row ) {
row . addEventListener ( 'dragstart' , function () {
draggedUID = row . dataset . uid ;
draggedFolder = row . dataset . folder ;
row . classList . add ( 'dragging' );
});
row . addEventListener ( 'dragend' , function () {
row . classList . remove ( 'dragging' );
});
});
document . querySelectorAll ( '.folder-link' ). forEach ( function ( link ) {
link . addEventListener ( 'dragover' , function ( e ) {
if ( ! draggedUID || link . dataset . folder === draggedFolder ) return ;
e . preventDefault ();
link . classList . add ( 'drop-hover' );
});
link . addEventListener ( 'dragleave' , function () {
link . classList . remove ( 'drop-hover' );
});
link . addEventListener ( 'drop' , async function ( e ) {
e . preventDefault ();
link . classList . remove ( 'drop-hover' );
const targetFolder = link . dataset . folder ;
if ( ! draggedUID || ! targetFolder || targetFolder === draggedFolder ) return ;
const body = new URLSearchParams ();
body . set ( 'target_folder' , targetFolder );
2026-08-15 12:35:44 +01:00
body . set ( 'csrf_token' , window . __csrfToken || '' );
2026-08-14 13:04:55 +01:00
await fetch ( `/webmail/mail/ ${ draggedFolder } / ${ draggedUID } /move` , {
method : 'POST' ,
headers : { 'Content-Type' : 'application/x-www-form-urlencoded' },
body : body . toString (),
});
window . location . reload ();
});
});
})();
// "+N more" toggle for a collapsed same-subject run — reveals every
// immediately-following row marked as part of that group, self-terminating
// at the first row that isn't (no need to track the count client-side).
document . querySelectorAll ( '[data-group-toggle]' ). forEach ( function ( badge ) {
badge . addEventListener ( 'click' , function ( e ) {
e . preventDefault ();
e . stopPropagation ();
2026-08-15 12:35:44 +01:00
let sib = badge . closest ( '.msg-item' ). nextElementSibling ;
2026-08-14 13:04:55 +01:00
while ( sib && sib . classList . contains ( 'msg-row-older' )) {
sib . style . display = '' ;
sib = sib . nextElementSibling ;
}
badge . style . display = 'none' ;
});
});
2026-08-15 12:35:44 +01:00
// Folder sidebar collapse — a display preference remembered per-browser
// (localStorage), not server state; default is pinned open (nothing stored
// yet == not collapsed).
( function () {
const KEY = 'webmail_sidebar_collapsed' ;
const sidebarCol = document . getElementById ( 'folderSidebarCol' );
const showBtn = document . getElementById ( 'sidebarShowBtn' );
function apply ( collapsed ) {
sidebarCol . style . display = collapsed ? 'none' : '' ;
showBtn . classList . toggle ( 'd-none' , ! collapsed );
}
apply ( localStorage . getItem ( KEY ) === '1' );
document . getElementById ( 'sidebarCollapseBtn' ). addEventListener ( 'click' , function () {
localStorage . setItem ( KEY , '1' );
apply ( true );
});
showBtn . addEventListener ( 'click' , function () {
localStorage . setItem ( KEY , '0' );
apply ( false );
});
})();
// Reading pane: clicking a row loads the message via fetch instead of
// navigating away, mirroring the admin dashboard's message-log modal. Drafts
// still navigate to compose (there's nothing to "read"). The clicked row is
// marked read optimistically client-side — the pane fetch itself is what
// actually marks it read server-side (see webmailMessagePane).
( function () {
const paneBody = document . getElementById ( 'readingPaneBody' );
document . querySelectorAll ( '.msg-item' ). forEach ( function ( row ) {
row . addEventListener ( 'click' , function ( e ) {
if ( e . target . closest ( '.msg-group-toggle' ) || e . target . classList . contains ( 'msg-check' )) return ;
if ( row . dataset . isDraft === 'true' ) { window . location . href = row . dataset . href ; return ; }
document . querySelectorAll ( '.msg-item.active' ). forEach ( function ( r ) { r . classList . remove ( 'active' ); });
row . classList . add ( 'active' );
row . classList . remove ( 'unread' );
paneBody . innerHTML = '<div class="text-center text-muted py-5"><div class="spinner-border" role="status"></div></div>' ;
fetch ( row . dataset . paneHref )
. then ( function ( r ) { if ( ! r . ok ) throw new Error ( r . status ); return r . text (); })
. then ( function ( html ) { paneBody . innerHTML = html ; })
. catch ( function () { paneBody . innerHTML = '<p class="text-danger">Failed to load the message.</p>' ; });
});
});
})();
// Selection (checkboxes + select-all + Shift-click range) driving the bulk
// toolbar buttons — enabled only once something's actually selected.
( function () {
const checks = Array . from ( document . querySelectorAll ( '.msg-check' ));
const selectAll = document . getElementById ( 'selectAllCheck' );
const bulkBtns = document . querySelectorAll ( '.bulk-btn' );
const moveSelect = document . getElementById ( 'bulkMoveSelect' );
let lastCheckedIndex = null ;
function updateToolbar () {
const any = checks . some ( function ( c ) { return c . checked ; });
bulkBtns . forEach ( function ( b ) { b . disabled = ! any ; });
if ( moveSelect ) moveSelect . disabled = ! any ;
selectAll . checked = checks . length > 0 && checks . every ( function ( c ) { return c . checked ; });
}
checks . forEach ( function ( cb , i ) {
cb . addEventListener ( 'click' , function ( e ) {
if ( e . shiftKey && lastCheckedIndex !== null ) {
const [ from , to ] = [ lastCheckedIndex , i ]. sort ( function ( a , b ) { return a - b ; });
for ( let j = from ; j <= to ; j ++ ) checks [ j ]. checked = cb . checked ;
}
lastCheckedIndex = i ;
updateToolbar ();
});
});
selectAll . addEventListener ( 'change' , function () {
checks . forEach ( function ( c ) { c . checked = selectAll . checked ; });
updateToolbar ();
});
document . addEventListener ( 'keydown' , function ( e ) {
if (( e . ctrlKey || e . metaKey ) && e . key . toLowerCase () === 'a' && document . activeElement . tagName !== 'INPUT' ) {
e . preventDefault ();
selectAll . checked = true ;
checks . forEach ( function ( c ) { c . checked = true ; });
updateToolbar ();
}
});
function selectedUIDs () { return checks . filter ( function ( c ) { return c . checked ; }). map ( function ( c ) { return c . value ; }); }
function submitBulk ( action , targetFolder ) {
const uids = selectedUIDs ();
if ( uids . length === 0 ) return ;
const form = document . getElementById ( 'bulkActionForm' );
form . action = '/webmail/mail/{{.active_folder}}/bulk' ;
document . getElementById ( 'bulkActionField' ). value = action ;
document . getElementById ( 'bulkTargetFolderField' ). value = targetFolder || '' ;
form . querySelectorAll ( 'input[name="uid"]' ). forEach ( function ( el ) { el . remove (); });
uids . forEach ( function ( uid ) {
const input = document . createElement ( 'input' );
input . type = 'hidden' ; input . name = 'uid' ; input . value = uid ;
form . appendChild ( input );
});
const csrf = document . createElement ( 'input' );
csrf . type = 'hidden' ; csrf . name = 'csrf_token' ; csrf . value = window . __csrfToken || '' ;
form . appendChild ( csrf );
form . submit ();
}
document . querySelectorAll ( '.bulk-btn' ). forEach ( function ( btn ) {
btn . addEventListener ( 'click' , async function () {
const action = btn . dataset . action ;
if ( action === 'delete' && ! ( await showConfirmation ( 'Move the selected message(s) to Trash?' ))) return ;
submitBulk ( action );
});
});
if ( moveSelect ) {
moveSelect . addEventListener ( 'change' , function () {
if ( moveSelect . value ) submitBulk ( 'move' , moveSelect . value );
});
}
})();
2026-08-14 13:04:55 +01:00
</ script >
</ body >
</ html >
{{end}}