Grammar change in the license declaration.
[citadel.git] / webcit-ng / static / js / view_mail.js
1 // This module handles the view for "mailbox" rooms.
2 //
3 // Copyright (c) 2016-2023 by the citadel.org team
4 //
5 // This program is open source software.  Use, duplication, or
6 // disclosure is subject to the GNU General Public License v3.
7
8
9 var displayed_message = 0;                                                      // ID of message currently being displayed
10 var RefreshMailboxInterval;                                                     // We store our refresh timer here
11 var highest_mailnum;                                                            // This is used to detect newly arrived mail
12 var newmail_notify = {
13         NO  : 0,                                                                // do not perform new mail notifications
14         YES : 1                                                                 // yes, perform new mail notifications
15 };
16
17
18 // This is the async back end for mail_delete_selected()
19 mail_delete_func = async(table, row) => {
20         let m = parseInt(row["id"].substring(12));                              // derive msgnum from row id
21
22         if (is_trash_folder) {
23                 response = await fetch(
24                         "/ctdl/r/" + escapeHTMLURI(current_room) + "/" + m,
25                         {
26                                 method: "DELETE"                                // If this is the Trash folder, delete permanently
27                         },
28                 );
29         }
30         else {
31                 response = await fetch(
32                         "/ctdl/r/" + escapeHTMLURI(current_room) + "/" + m,
33                         {
34                                 method: "MOVE",                                 // Otherwise, move to the Trash folder
35                                 headers: { "Destination" : "/ctdl/r/_TRASH_" }
36                         },
37                 );
38         }
39
40         if (response.ok) {                              // If the server accepted the delete, blank out the message div
41                 table.deleteRow(row.rowIndex);
42                 if (m == displayed_message) {
43                         document.getElementById("ctdl-mailbox-reading-pane").innerHTML = "";
44                         displayed_message = 0;
45                 }
46         }
47 }
48
49
50 // Delete the selected messages (can be activated by mouse click or keypress)
51 function mail_delete_selected() {
52         let table = document.getElementById("ctdl-onscreen-mailbox");
53         let i, row;
54         for (i=0; row=table.rows[i]; ++i) {
55                 if (row.classList.contains("ctdl-mail-selected")) {
56                         mail_delete_func(table, row);
57                 }
58         }
59 }
60
61
62 // Handler function for keypresses detected while the mail view is displayed.  Mainly for deleting messages.
63 function mail_keypress(event) {
64
65         // If the "ctdl-mailbox-pane" no longer exists, the user has navigated to a different part of the site,
66         // so cancel the event listener.
67         try {
68                 document.getElementById("ctdl-mailbox-pane").innerHTML;
69         }
70         catch {
71                 document.removeEventListener("keydown", mail_keypress);
72                 return;
73         }
74
75         const key = event.key.toLowerCase();
76         if (key == "delete") {
77                 mail_delete_selected();
78         }
79
80 }
81
82
83 // Handler function for dragging email messages to other folders
84 function mail_dragstart(event) {
85         let i;
86         let count = 0;
87         let table = document.getElementById("ctdl-onscreen-mailbox");
88         let messages_being_dragged = [] ;
89
90         if (event.target.classList.contains("ctdl-mail-selected")) {
91                 // The row being dragged IS selected.  See if any OTHER rows are selected, and they will come along for the ride.
92                 for (i=1; row=table.rows[i]; ++i) {
93                         if (row.classList.contains("ctdl-mail-selected")) {
94                                 count = count + 1;
95                                 messages_being_dragged.push(row.id);    // Tell the clipboard what's being moved.
96                         }
97                 }
98         }
99         else {
100                 // The row being dragged is NOT selected.  It will be dragged on its own, ignoring the selected rows.
101                 count = 1;
102                 messages_being_dragged.push(event.target.id);           // Tell the clipboard what's being moved.
103         }
104
105         // Set the custom drag image to an envelope + number of messages being dragged
106         d = document.getElementById("ctdl_draggo");
107         d.innerHTML = "<font size='+2'><i class='fa fa-envelope' style='color: red'></i> " + count + "</font>"
108         event.dataTransfer.setDragImage(d, 0, 0);
109         event.dataTransfer.setData("text", messages_being_dragged);
110 }
111
112
113 // Render reply address for a message (FIXME figure out how to deal with "reply-to:")
114 function reply_addr(msg) {
115         //if (msg.locl) {
116                 //return([msg.from]);
117         //}
118         //else {
119                 return([msg.from + " &lt;" + msg.rfca + "&gt;"]);
120         //}
121 }
122
123
124 // Render the To: recipients for a reply-all operation
125 function replyall_to(msg) {
126         return([...reply_addr(msg), ...msg.rcpt]);
127 }
128
129
130 // Render a message into the mailbox view
131 // (We want the message number and the message itself because we need to keep the msgnum for reply purposes)
132 function mail_render_one(msgnum, msg, target_div, include_controls) {
133         let div = "";
134         try {
135                 outmsg =
136                   "<div class=\"ctdl-mmsg-wrapper\">"                           // begin message wrapper
137                 ;
138
139                 if (include_controls) {                                         // omit controls if this is a pull quote
140
141                         let subject     = msg.subj              ? escapeJS(msg.subj)            : "" ;
142
143                         outmsg +=
144                           render_userpic(msg.from)                              // user avatar
145                         + "<div class=\"ctdl-mmsg-content\">"                   // begin content
146                         + "<div class=\"ctdl-msg-header\">"                     // begin header
147                         + "<span class=\"ctdl-msg-header-info\">"               // begin header info on left side
148                         + render_msg_author(msg, views.VIEW_MAILBOX)
149                         + "<span class=\"ctdl-msgdate\">"
150                         + string_timestamp(msg.time,0)
151                         + "</span>"                                             // end msgdate
152                         + "</span>"                                             // end header info on left side
153                         + "<span class=\"ctdl-msg-header-buttons\">"            // begin buttons on right side
154                 
155                         + "<span class=\"ctdl-msg-button\">"                    // Reply
156                         + `<a href="javascript:mail_compose(msg.wefw, ${msgnum}, reply_addr(msg), [], 'Re: ${subject}');">`
157                         + "<i class=\"fa fa-reply\"></i> " 
158                         + _("Reply")
159                         + "</a></span>"
160                 
161                         + "<span class=\"ctdl-msg-button\">"                    // Reply-All
162                         + `<a href="javascript:mail_compose(msg.wefw, ${msgnum}, replyall_to(msg), msg.cccc, 'Re: ${subject}');">`
163                         + "<i class=\"fa fa-reply-all\"></i> " 
164                         + _("ReplyAll")
165                         + "</a></span>"
166                 
167                         + "<span class=\"ctdl-msg-button\">"
168                         + `<a href="javascript:mail_compose(msg.wefw, ${msgnum}, [], [], 'Fwd: ${subject}');">`
169                         + "<i class=\"fa fa-mail-forward\"></i> " 
170                         + _("Forward")
171                         + "</a></span>";
172                 
173                         if (can_delete_messages) {
174                                 outmsg +=
175                                 "<span class=\"ctdl-msg-button\">"
176                                 + "<a href=\"javascript:forum_delete_message('"+div+"','"+msg.msgnum+"');\">"
177                                 + "<i class=\"fa fa-trash\"></i> " 
178                                 + _("Delete")
179                                 + "</a></span>";
180                         }
181                 
182                         outmsg +=
183                           "</span>";                                            // end buttons on right side
184
185                         // Display the To: recipients, if any are present
186                         if (msg.rcpt) {
187                                 outmsg += "<br><span>" + _("To:") + " ";
188                                 for (let r=0; r<msg.rcpt.length; ++r) {
189                                         if (r != 0) {
190                                                 outmsg += ", ";
191                                         }
192                                         outmsg += escapeHTML(msg.rcpt[r]);
193                                 }
194                                 outmsg += "</span>";
195                         }
196
197                         // Display the Cc: recipients, if any are present
198                         if (msg.cccc) {
199                                 outmsg += "<br><span>" + _("Cc:") + " ";
200                                 for (let r=0; r<msg.cccc.length; ++r) {
201                                         if (r != 0) {
202                                                 outmsg += ", ";
203                                         }
204                                         outmsg += escapeHTML(msg.cccc[r]);
205                                 }
206                                 outmsg += "</span>";
207                         }
208
209                         // Display a subject line, but only if the message has a subject (internal Citadel messages often don't)
210                         if (msg.subj) {
211                                 outmsg +=
212                                 "<br><span class=\"ctdl-msgsubject\">" + msg.subj + "</span>";
213                         }
214
215                         outmsg +=
216                           "</div>";                                             // end header
217                 }
218
219                 // Display attachments, if any are present (don't do this if we're quoting the message)
220                 if ( (msg.part) && (include_controls) ) {
221                         let display_attachments = 0;
222                         for (let r=0; r<msg.part.length; ++r) {
223                                 if (msg.part[r].disp == "attachment") {
224                                         if (display_attachments == 0) {
225                                                 outmsg += "<ul>";
226                                         }
227                                         display_attachments += 1;
228                                         outmsg += "<li>"
229                                                 + "<a href=\"/ctdl/r/" + escapeHTMLURI(current_room) + "/" + msgnum + "/" + msg.part[r].partnum + "/" + escapeHTMLURI(msg.part[r].filename) + "\" target=\"_blank\">"
230                                                 + "<i class=\"fa fa-paperclip\"></i>&nbsp;" + msg.part[r].partnum + ": " + msg.part[r].filename
231                                                 + " (" + msg.part[r].len + " " + _("bytes") + ")"
232                                                 + "</a>"
233                                                 + "</li>";
234                                 }
235                         }
236                         if (display_attachments > 0) {
237                                 outmsg += "</ul><br>";
238                         }
239                 }
240
241
242                 outmsg +=
243                   "<div class=\"ctdl-msg-body\" id=\"" + div + "_body\">"       // begin body
244                 + msg.text
245                 + "</div>"                                                      // end body
246                 + "</div>"                                                      // end content
247                 + "</div>"                                                      // end wrapper
248                 ;
249         }
250         catch(err) {
251                 outmsg = "<div class=\"ctdl-mmsg-wrapper\">" + err.message + "</div>";
252         }
253
254         target_div.innerHTML = outmsg;
255 }
256
257
258 // display an individual message (note: this wants an actual div object, not a string containing the name of a div)
259 function mail_display_message(msgnum, target_div, include_controls) {
260         url = "/ctdl/r/" + escapeHTMLURI(current_room) + "/" + msgnum + "/json";
261         mail_fetch_msg = async() => {
262                 response = await fetch(url);
263                 msg = await(response.json());
264                 if (response.ok) {
265                         mail_render_one(msgnum, msg, target_div, include_controls);
266                 }
267         }
268         mail_fetch_msg();
269 }
270
271
272 // A message has been selected...
273 function click_message(event, msgnum) {
274         var table = document.getElementById("ctdl-onscreen-mailbox");
275         var i, m, row;
276
277         // ctrl + click = toggle an individual message without changing existing selection
278         if (event.ctrlKey) {
279                 document.getElementById("ctdl-msgsum-" + msgnum).classList.toggle("ctdl-mail-selected");
280         }
281
282         // shift + click = select a range of messages (start with row 1 because row 0 is the header)
283         else if (event.shiftKey) {
284                 for (i=1; row=table.rows[i]; ++i) {
285                         m = parseInt(row["id"].substring(12));                          // derive msgnum from row id
286                         if (
287                                 ((msgnum >= displayed_message) && (m >= displayed_message) && (m <= msgnum))
288                                 || ((msgnum <= displayed_message) && (m <= displayed_message) && (m >= msgnum))
289                         ) {
290                                 row.classList.add("ctdl-mail-selected");
291                         }
292                         else {
293                                 row.classList.remove("ctdl-mail-selected");
294                         }
295                 }
296         }
297
298         // click + no modifiers = select one message and unselect all others (start with row 1 because row 0 is the header)
299         else {
300                 for (i=1; row=table.rows[i]; ++i) {
301                         if (row["id"] == "ctdl-msgsum-" + msgnum) {
302                                 row.classList.add("ctdl-mail-selected");
303                         }
304                         else {
305                                 row.classList.remove("ctdl-mail-selected");
306                         }
307                 }
308         }
309
310         // display the message if it isn't already displayed
311         if (displayed_message != msgnum) {
312                 displayed_message = msgnum;
313                 mail_display_message(msgnum, document.getElementById("ctdl-mailbox-reading-pane"), 1);
314         }
315 }
316
317
318 // render one row in the mailbox table (this could be called from one of several places)
319 function mail_render_row(msg, is_selected) {
320         let row = "<tr "
321                 + "id=\"ctdl-msgsum-" + msg["msgnum"] + "\" "
322                 + (is_selected ? "class=\"ctdl-mail-selected\" " : "")
323                 + "onClick=\"click_message(event," + msg["msgnum"] + ");\""
324                 + "onselectstart=\"return false;\" "
325                 + "draggable=\"true\" "
326                 + "ondragstart=\"mail_dragstart(event)\" "
327                 + ">"
328                 + "<td class=\"ctdl-mail-subject\">" + msg["subject"] + "</td>"
329                 + "<td class=\"ctdl-mail-sender\">" + msg["author"] + "</td>"
330                 + "<td class=\"ctdl-mail-date\">" + string_timestamp(msg["time"],1) + "</td>"
331                 + "<td class=\"ctdl-mail-msgnum\">" + msg["msgnum"] + "</td>"
332                 + "</tr>";
333         return(row);
334 }
335
336
337 // RENDERER FOR THIS VIEW
338 function view_render_mail() {
339         // Put the "enter new message" button into the topbar
340         document.getElementById("ctdl-newmsg-button").innerHTML = `<i class="fa fa-edit"></i>&nbsp;` + _("Write mail");
341         document.getElementById("ctdl-newmsg-button").style.display = "block";
342
343         // Put the "delete message(s)" button into the topbar
344         let d = document.getElementById("ctdl-delete-button");
345         d.innerHTML = `<i class="fa fa-trash"></i>&nbsp;` + _("Delete");
346         d.style.display = "block";
347         //d.addEventListener("click", mail_delete_selected);
348
349         document.getElementById("ctdl-main").innerHTML = `
350                 <div id="ctdl-mailbox-grid-container" class="ctdl-mailbox-grid-container">
351                 <div id="ctdl-mailbox-pane" class="ctdl-mailbox-pane"></div>
352                 <div id="ctdl-mailbox-reading-pane" class="ctdl-mailbox-reading-pane"></div>
353                 </div>
354         `;
355
356         highest_mailnum = 0;                                    // Keep track of highest msg number to track newly arrived msgs
357         render_mailbox_display(newmail_notify.NO);
358         try {                                                   // if this was already set up, clear it so there aren't multiple
359                 clearInterval(RefreshMailboxInterval);
360         }
361         catch {
362         }
363         RefreshMailboxInterval = setInterval(refresh_mail_display, 10000);
364 }
365
366
367 // Refresh the mailbox, either for the first time or whenever needed
368 function refresh_mail_display() {
369         // If the "ctdl-mailbox-pane" no longer exists, the user has navigated to a different part of the site,
370         // so cancel the refresh.
371         try {
372                 document.getElementById("ctdl-mailbox-pane").innerHTML;
373         }
374         catch {
375                 clearInterval(RefreshMailboxInterval);
376                 return;
377         }
378
379         // Ask the server if the room has been written to since our last look at it.
380         url = "/ctdl/r/" + escapeHTMLURI(current_room) + "/stat";
381         fetch_stat = async() => {
382                 response = await fetch(url);
383                 stat = await(response.json());
384                 if (stat.room_mtime > room_mtime) {                     // if modified...
385                         room_mtime = stat.room_mtime;
386                         render_mailbox_display(newmail_notify.YES);     // ...force a refresh
387                 }
388         }
389         fetch_stat();
390 }
391
392
393 // This is where the rendering of the message list in the mailbox view is performed.
394 // Set notify to newmail_notify.NO or newmail_notify.YES depending on whether we are interested in the arrival of new messages.
395 function render_mailbox_display(notify) {
396
397         url = "/ctdl/r/" + escapeHTMLURI(current_room) + "/mailbox";
398         fetch_mailbox = async() => {
399                 response = await fetch(url);
400                 msgs = await(response.json());
401                 if (response.ok) {
402                         var previously_selected = [];
403                         var oldtable = document.getElementById("ctdl-onscreen-mailbox");
404                         var i, row;
405
406                         // If one or more messages was already selected, remember them so we can re-select them
407                         if ( (displayed_message > 0) && (oldtable) ) {
408                                 for (i=0; row=oldtable.rows[i]; ++i) {
409                                         if (row.classList.contains("ctdl-mail-selected")) {
410                                                 previously_selected.push(parseInt(row["id"].substring(12)));
411                                         }
412                                 }
413                         }
414
415                         // begin rendering the mailbox table
416                         box = `
417                                 <table id="ctdl-onscreen-mailbox" class="ctdl-mailbox-table" width=100%><tr>
418                                 <th>${_("Subject")}</th>
419                                 <th>${ _("Sender")}</th>
420                                 <th>${_("Date")}</th>
421                                 <th>#</th>
422                                 </tr>
423                         `;
424
425                         for (let i=0; i<msgs.length; ++i) {
426                                 let m = parseInt(msgs[i].msgnum);
427                                 let s = (previously_selected.includes(m));
428                                 box += mail_render_row(msgs[i], s);
429                                 if (m > highest_mailnum) {
430                                         highest_mailnum = m;
431                                 }
432                         }
433
434                         box +=  "</table>";
435                         document.getElementById("ctdl-mailbox-pane").innerHTML = box;
436                         document.addEventListener("keydown", mail_keypress);
437                 }
438         }
439         fetch_mailbox();
440 }
441
442
443 // helper function for mail_compose() -- converts a recipient array to a string suitable for the To: or Cc: field
444 function recipient_array_to_string(recps_arr) {
445
446         let returned_string = ""
447
448         if (recps_arr) {
449                 is_reply = 1;
450
451                 // first clean up the recipients
452                 for (i=0; i<recps_arr.length; ++i) {
453                         recps_arr[i] = recps_arr[i].replaceAll("<", "&lt;").replaceAll(">", "&gt;");
454                 }
455
456                 // remove dupes
457                 recps_arr = Array.from(new Set(recps_arr));
458
459                 // now convert it to a string
460                 returned_string = "";
461                 for (i=0; i<recps_arr.length; ++i) {
462                         if (i > 0) {
463                                 returned_string += ", ";
464                         }
465                         returned_string += recps_arr[i];
466                 }
467         }
468
469         return(returned_string);
470 }
471
472
473
474 // Compose a new mail message (called by the Reply button here, or by the dispatcher in views.js)
475 //
476 // references           list of references, be sure to use this in a reply
477 // quoted_msgnum        if a reply, the msgid of the most recent message in the chain, the one to which we are replying
478 //                      (set to 0 if this is not a reply)
479 // m_to                 an ARRAY of zero or more recipients to pre-insert into the To: field
480 // m_cc                 an ARRAY of zero or more recipients to pre-insert into the Cc: field
481 // m_subject            a string to pre-insert into the Subject: field
482 //
483 function mail_compose(references, quoted_msgnum, m_to, m_cc, m_subject) {
484
485         let is_reply = 0;
486         let is_quoted = (quoted_msgnum > 0) ? true : false ;
487         let is_fwd = (is_quoted && m_to.length==0 && m_cc.length==0) ;
488
489         // m_to will be an array of zero or more recipients for the To: field.  Convert it to a string.
490         m_to_str = recipient_array_to_string(m_to);
491
492         // m_cc will be an array of zero or more recipients for the Cc: field.  Convert it to a string.
493         m_cc_str = recipient_array_to_string(m_cc);
494
495         quoted_div_name = randomString();
496
497         // Make the "Write mail" button disappear.  We're already there!
498         document.getElementById("ctdl-newmsg-button").style.display = "none";
499
500         // Now display the screen.  (Yes, I combined regular strings + template literals.
501         // I just learned template literals.  Converting the whole thing to template literals would be fine.)
502         compose_screen =
503                 // Hidden values that we are storing right here in the document tree for later
504                   "<div id=\"ctdl-mc-references\" style=\"display:none\">" + references + "</div>"
505
506                 // Header fields, the composition window, and the button bar are arranged using a Grid layout.
507                 + "<div id=\"ctdl-compose-mail\" class=\"ctdl-compose-mail\">"
508
509                 // Visible To: field, plus a box to make the CC/BCC lines appear
510                 + "<div class=\"ctdl-compose-to-label\">" + _("To:") + "</div>"
511                 + "<div class=\"ctdl-compose-to-line\">"
512                 + "<div class=\"ctdl-compose-to-field\" id=\"ctdl-compose-to-field\" contenteditable=\"true\">" + m_to_str + "</div>"
513                 + "<div class=\"ctdl-cc-bcc-buttons ctdl-msg-button\" id=\"ctdl-cc-bcc-buttons\" "
514                 + "onClick=\"make_cc_bcc_visible()\">"
515                 + _("CC:") + "/" + _("BCC:") + "</div>"
516                 + "</div>"
517
518                 // CC/BCC
519                 + "<div class=\"ctdl-compose-cc-label\" id=\"ctdl-compose-cc-label\">" + _("CC:") + "</div>"
520                 + "<div class=\"ctdl-compose-cc-field\" id=\"ctdl-compose-cc-field\" contenteditable=\"true\">" + m_cc_str + "</div>"
521                 + "<div class=\"ctdl-compose-bcc-label\" id=\"ctdl-compose-bcc-label\">" + _("BCC:") + "</div>"
522                 + "<div class=\"ctdl-compose-bcc-field\" id=\"ctdl-compose-bcc-field\" contenteditable=\"true\"></div>"
523
524                 // Visible subject field
525                 + "<div class=\"ctdl-compose-subject-label\">" + _("Subject:") + "</div>"
526                 + "<div class=\"ctdl-compose-subject-field\" id=\"ctdl-compose-subject-field\" contenteditable=\"true\">" + m_subject + "</div>"
527
528                 // Message composition box
529                 + "<div class=\"ctdl-compose-message-box\" id=\"ctdl-editor-body\" contenteditable=\"true\">"
530         ;
531
532         // If this is a quoted reply, insert a div within which we will render the original message.
533         if (is_quoted && is_fwd) {
534                 compose_screen += "<br><br>" + _("--- forwarded message ---") + "<br><div id=\"" + quoted_div_name + "\">QUOTE</div>";
535         }
536         else if (is_quoted) {
537                 compose_screen += "<br><br><blockquote><div id=\"" + quoted_div_name + "\">QUOTE</div></blockquote>";
538         }
539
540         // The button bar is a Grid element, and is also a Flexbox container.
541         compose_screen += `
542                 </div>
543                 <div class="ctdl-compose-toolbar">
544                 <span class="ctdl-msg-button" onclick="mail_send_message()"><i class="fa fa-paper-plane" style="color:green"></i> ${_("Send message")} </span>
545                 <span class="ctdl-msg-button"> ${_("Save to Drafts")} </span>
546                 <span class="ctdl-msg-button" onClick="show_or_hide_upload_window()"><i class="fa fa-paperclip" style="color:grey"></i> ${_("Attachments:")} <span id="ctdl_num_attachments"> ${uploads.length} </span></span>
547                 <span class="ctdl-msg-button">  ${_("Contacts")} </span>
548                 <span class="ctdl-msg-button" onClick="flush_uploads();gotoroom(current_room)"><i class="fa fa-trash" style="color:red"></i> ${_("Cancel")} </span>
549                 </div>`
550         ;
551
552
553         document.getElementById("ctdl-main").innerHTML = compose_screen;
554
555         if (m_cc) {
556                 document.getElementById("ctdl-compose-cc-label").style.display = "block";
557                 document.getElementById("ctdl-compose-cc-field").style.display = "block";
558         }
559
560         activate_uploads("ctdl-editor-body");                           // create the attachments window
561         attachment_counter_divs.push("ctdl_num_attachments");           // make the Attachments: count at the bottom update too
562
563         // If this is a quoted reply, render the original message into the div we set up earlier.
564         if (is_quoted) {
565                 mail_display_message(quoted_msgnum, document.getElementById(quoted_div_name), 0);
566         }
567
568         // If this is a forwarded messages, preload its attachments into the forwarded copy.
569         if (is_fwd) {
570                 forward_attachments(quoted_msgnum);
571         }
572
573         if (is_reply) {
574                 setTimeout(() => { document.getElementById("ctdl-editor-body").focus(); }, 0);
575         }
576         else {
577                 setTimeout(() => { document.getElementById("ctdl-compose-to-field").focus(); }, 0);
578         }
579
580 }
581
582 // Called when the user clicks the button to make the hidden "CC" and "BCC" lines appear.
583 // It is also called automatically during a Reply when CC is pre-populated.
584 function make_cc_bcc_visible() {
585         document.getElementById("ctdl-cc-bcc-buttons").style.display = "none";
586         document.getElementById("ctdl-compose-cc-label").style.display = "block";
587         document.getElementById("ctdl-compose-cc-field").style.display = "block";
588         document.getElementById("ctdl-compose-bcc-label").style.display = "block";
589         document.getElementById("ctdl-compose-bcc-field").style.display = "block";
590 }
591
592
593 // Helper function for mail_send_messages() to extract and decode metadata values.
594 function msm_field(element_name, separator) {
595         let s1 = document.getElementById(element_name).innerHTML;
596         let s2 = s1.replaceAll("|",separator);          // Replace "|" with "!" because "|" is a field separator in Citadel
597         let s3 = decodeURI(s2);
598         let s4 = document.createElement("textarea");    // This One Weird Trick Unescapes All HTML Entities
599         s4.innerHTML = s3;
600         let s5 = s4.value;
601         return(s5);
602 }
603
604
605 // Save the posted message to the server
606 function mail_send_message() {
607
608         document.body.style.cursor = "wait";
609         deactivate_uploads();
610         let url = "/ctdl/r/" + escapeHTMLURI(current_room)
611                 + "/dummy_name_for_new_mail"
612                 + "?wefw="      + msm_field("ctdl-mc-references", "!")                          // references (if present)
613                 + "&subj="      + msm_field("ctdl-compose-subject-field", " ")                  // subject (if present)
614                 + "&mailto="    + msm_field("ctdl-compose-to-field", ",")                       // To: (required)
615                 + "&mailcc="    + msm_field("ctdl-compose-cc-field", ",")                       // Cc: (if present)
616                 + "&mailbcc="   + msm_field("ctdl-compose-bcc-field", ",")                      // Bcc: (if present)
617         ;
618         if (uploads.length > 0) {
619                 url += "&att=";
620                 for (let i=0; i<uploads.length; ++i) {
621                         url += uploads[i]["ref"];
622                         if (i != uploads.length - 1) {
623                                 url += ",";
624                         }
625                 }
626         }
627         body_text = "<html><body>" + document.getElementById("ctdl-editor-body").innerHTML + "</body></html>\r\n";
628
629         var request = new XMLHttpRequest();
630         request.open("PUT", url, true);
631         request.setRequestHeader("Content-type", "text/html");
632         request.onreadystatechange = function() {
633                 if (request.readyState == 4) {
634                         document.body.style.cursor = "default";
635                         if (Math.trunc(request.status / 100) == 2) {
636                                 headers = request.getAllResponseHeaders().split("\n");
637                                 for (var i in headers) {
638                                         if (headers[i].startsWith("etag: ")) {
639                                                 new_msg_num = headers[i].split(" ")[1];
640                                         }
641                                 }
642
643                                 // successfully saving a message means the attachments are now gone from the server.
644                                 uploads = [];
645
646                                 // After saving the message, go back to the mailbox view.
647                                 gotoroom(current_room);
648
649                         }
650                         else {
651                                 error_message = request.responseText;
652                                 if (error_message.length == 0) {
653                                         error_message = _("An error has occurred.");
654                                 }
655                                 alert(error_message);                                           // editor remains open
656                         }
657                 }
658         };
659         request.send(body_text);
660 }