@@ -34,6 +34,7 @@ function toast(msg, kind = "ok") {
3434
3535let state = {
3636 cfg : null ,
37+ identity : "all" , // "all" or one of cfg.identities[].address
3738 emails : { in : [ ] , out : [ ] } ,
3839 events : [ ] ,
3940 calCursor : new Date ( ) ,
@@ -88,15 +89,37 @@ function switchTab(name) {
8889}
8990
9091// ------------------------------------------------------------------ mail list
92+ const parseAddrs = ( j ) => {
93+ try {
94+ return JSON . parse ( j ) ;
95+ } catch {
96+ return [ ] ;
97+ }
98+ } ;
99+
100+ // Which of our identities an inbound email was addressed to (to/cc).
101+ const ourRecipientOf = ( m ) => {
102+ const ids = state . cfg ?. identities || [ ] ;
103+ const rcpts = [ ...parseAddrs ( m . to_addresses ) , ...parseAddrs ( m . cc_addresses ) ] ;
104+ for ( const r of rcpts ) {
105+ const hit = ids . find ( ( i ) => i . address === String ( r ) . toLowerCase ( ) ) ;
106+ if ( hit ) return hit . address ;
107+ }
108+ return "" ;
109+ } ;
110+
91111async function loadBox ( box ) {
92- const { emails } = await api ( `/api/emails?box=${ box } ` ) ;
112+ const addr =
113+ state . identity === "all" ? "" : `&address=${ encodeURIComponent ( state . identity ) } ` ;
114+ const { emails } = await api ( `/api/emails?box=${ box } ${ addr } ` ) ;
93115 state . emails [ box ] = emails ;
94116 if ( box === "in" ) {
95117 const unread = emails . filter ( ( m ) => ! m . read ) . length ;
96118 $ ( "#inbox-count" ) . textContent = unread || "" ;
97119 }
98120 const list = $ ( box === "in" ? "#inbox-list" : "#sent-list" ) ;
99121 list . innerHTML = emails . length ? "" : `<div class="item sub">No messages yet</div>` ;
122+ const multi = ( state . cfg ?. identities ?. length || 0 ) > 1 ;
100123 for ( const m of emails ) {
101124 const div = document . createElement ( "div" ) ;
102125 div . className =
@@ -106,8 +129,15 @@ async function loadBox(box) {
106129 const badge = m . has_calendar
107130 ? `<span class="badge ${ m . calendar_method === "CANCEL" ? "cancel" : m . calendar_method === "REPLY" ? "reply" : "" } ">${ esc ( m . calendar_method || "invite" ) } </span>`
108131 : "" ;
132+ // In the combined view, tag which address received/sent each message.
133+ const whoBadge =
134+ multi && state . identity === "all"
135+ ? `<span class="badge to">${ esc (
136+ box === "in" ? ourRecipientOf ( m ) : m . from_address ,
137+ ) } </span>`
138+ : "" ;
109139 div . innerHTML = `
110- <div class="from">${ esc ( box === "in" ? m . from_address : joinAddrs ( m . to_addresses ) ) } ${ badge } </div>
140+ <div class="from">${ esc ( box === "in" ? m . from_address : joinAddrs ( m . to_addresses ) ) } ${ badge } ${ whoBadge } </div>
111141 <div class="subj">${ esc ( m . subject ) || "(no subject)" } </div>
112142 <div class="meta"><span>${ fmtDate ( m . received_at ) } </span></div>` ;
113143 div . onclick = ( ) => openEmail ( m . id , box ) ;
@@ -190,11 +220,19 @@ async function openEmail(id, box) {
190220 } ) ,
191221 ) ;
192222 detail . querySelector ( "#btn-reply" ) . onclick = ( ) => {
223+ // Reply as the identity that was addressed (inbox) or that sent (sent box).
224+ const from =
225+ box === "in"
226+ ? ourRecipientOf ( email )
227+ : ( state . cfg ?. identities || [ ] ) . find (
228+ ( i ) => i . address === email . from_address . toLowerCase ( ) ,
229+ ) ?. address || "" ;
193230 openCompose (
194- email . from_address ,
231+ box === "in" ? email . from_address : joinAddrs ( email . to_addresses ) ,
195232 "Re: " + ( email . subject || "" ) ,
196233 "" ,
197234 email . message_id ,
235+ from ,
198236 ) ;
199237 } ;
200238 detail . querySelector ( "#btn-del" ) . onclick = async ( ) => {
@@ -208,7 +246,12 @@ async function openEmail(id, box) {
208246
209247// ------------------------------------------------------------------ compose
210248const composeDlg = $ ( "#compose-modal" ) ;
211- function openCompose ( to = "" , subject = "" , body = "" , inReplyTo = "" ) {
249+ function openCompose ( to = "" , subject = "" , body = "" , inReplyTo = "" , from = "" ) {
250+ const sel = $ ( "#compose-from" ) ;
251+ sel . value =
252+ from ||
253+ ( state . identity !== "all" ? state . identity : state . cfg ?. primaryAddress ) ||
254+ sel . value ;
212255 $ ( "#compose-to" ) . value = to ;
213256 $ ( "#compose-subject" ) . value = subject ;
214257 $ ( "#compose-body" ) . value = body ;
@@ -229,6 +272,7 @@ $("#compose-form").addEventListener("submit", async (e) => {
229272 await api ( "/api/send" , {
230273 method : "POST" ,
231274 body : {
275+ from : $ ( "#compose-from" ) . value ,
232276 to : $ ( "#compose-to" ) . value ,
233277 subject : $ ( "#compose-subject" ) . value ,
234278 text : $ ( "#compose-body" ) . value ,
@@ -336,6 +380,14 @@ function openEventModal(ev, date) {
336380 $ ( "#ev-start" ) . value = toLocalInput ( start ) ;
337381 $ ( "#ev-end" ) . value = toLocalInput ( end ) ;
338382 $ ( "#ev-attendees" ) . value = ( ev ?. attendees || [ ] ) . map ( ( a ) => a . email ) . join ( ", " ) ;
383+ // Organizer is fixed once the event exists.
384+ const orgSel = $ ( "#ev-organizer" ) ;
385+ orgSel . disabled = ! ! ev ;
386+ if ( ! ev )
387+ orgSel . value =
388+ state . identity !== "all" ? state . identity : state . cfg ?. primaryAddress ;
389+ else if ( ( state . cfg ?. identities || [ ] ) . some ( ( i ) => i . address === ev . organizerEmail ) )
390+ orgSel . value = ev . organizerEmail ;
339391 $ ( "#ev-delete" ) . classList . toggle ( "hidden" , ! ev ) ;
340392 $ ( "#ev-ics" ) . classList . toggle ( "hidden" , ! ev ) ;
341393 $ ( "#ev-ics" ) . onclick = ( ) => {
@@ -409,7 +461,7 @@ $("#event-form").addEventListener("submit", async (e) => {
409461 } else {
410462 await api ( "/api/events" , {
411463 method : "POST" ,
412- body : { ...body , sendInvites : notify } ,
464+ body : { ...body , organizer : $ ( "#ev-organizer" ) . value , sendInvites : notify } ,
413465 } ) ;
414466 toast ( "Event created" ) ;
415467 }
@@ -424,7 +476,43 @@ $("#event-form").addEventListener("submit", async (e) => {
424476// ------------------------------------------------------------------ boot
425477async function boot ( ) {
426478 state . cfg = await api ( "/api/config" ) ;
427- $ ( "#me-address" ) . textContent = state . cfg . primaryAddress ;
479+ const ids = state . cfg . identities ?. length
480+ ? state . cfg . identities
481+ : [ { address : state . cfg . primaryAddress , name : state . cfg . displayName } ] ;
482+
483+ const idSel = $ ( "#identity-picker" ) ;
484+ idSel . innerHTML =
485+ ( ids . length > 1 ? `<option value="all">All addresses</option>` : "" ) +
486+ ids
487+ . map ( ( i ) => `<option value="${ esc ( i . address ) } ">${ esc ( i . address ) } </option>` )
488+ . join ( "" ) ;
489+ state . identity = ids . length > 1 ? "all" : ids [ 0 ] . address ;
490+ idSel . value = state . identity ;
491+
492+ const fromOpts = ids
493+ . map (
494+ ( i ) =>
495+ `<option value="${ esc ( i . address ) } ">${ esc (
496+ i . name && i . name !== i . address ? `${ i . name } <${ i . address } >` : i . address ,
497+ ) } </option>`,
498+ )
499+ . join ( "" ) ;
500+ $ ( "#compose-from" ) . innerHTML = fromOpts ;
501+ $ ( "#ev-organizer" ) . innerHTML = fromOpts ;
502+
503+ const showMe = ( ) => {
504+ $ ( "#me-address" ) . textContent =
505+ state . identity === "all" ? `${ ids . length } addresses` : state . identity ;
506+ } ;
507+ showMe ( ) ;
508+ idSel . onchange = ( ) => {
509+ state . identity = idSel . value ;
510+ state . selectedId = null ;
511+ showMe ( ) ;
512+ loadBox ( "in" ) ;
513+ loadBox ( "out" ) ;
514+ } ;
515+
428516 await Promise . all ( [ loadBox ( "in" ) , refreshEvents ( ) ] ) ;
429517 showApp ( ) ;
430518}
0 commit comments