Source of “pachyletV2.js”.
3196 lines, 95.6 KBytes.   Last modified 8:49 pm, 1st November 2015 PST.
1 // Emacs settings: -*- mode: Fundamental; tab-width: 4; -*- 2 3 //////////////////////////////////////////////////////////////////////////// 4 // // 5 // Pachylet: Andrew's Web Mail Interface, Version 2 // 6 // // 7 // Copyright (c) 2002-2014 // 8 // // 9 // See http://birrell.org/pachylet/help.php // 10 // // 11 // Client-side script // 12 // // 13 //////////////////////////////////////////////////////////////////////////// 14 15 16 function showAndHide(show, hide, display) { 17 // Show one element and hide the other 18 // 19 document.getElementById(show).style.display = display; 20 document.getElementById(hide).style.display = "none"; 21 return false; 22 } 23 24 25 function Scrollbar(id) { 26 // Constructor for a Javascript-managed scrollbar object. 27 // 28 // The outer DIV has the given ID, and contains the scroller element, 29 // whose id is <id>Scroller. The scroller element's className is set 30 // to "scrollDragging" while it is being dragged, and restored after. 31 // The scroller element's "position" attribute is forced to "relative". 32 // 33 // The scrollbar state is maintained in its attributes "visible" (the 34 // fraction of the scrollee currently visible) and "pos" (the current 35 // position in the document); both are fractions, [0..1]. Both are 36 // read-only to the client, except through calls of the "set" method. 37 // 38 // Actions in the scrollbar are reported to the client by calls of the 39 // "scrollTo", "scrollDone", and "page" methods. The client adjusts 40 // the scrollbar appearance by calls of the "set" method, spontaneously 41 // or in response to calls of "scrollTo", "scrollDone", or "page". 42 // If the scrollbar's height changes, the client must call "set". 43 // 44 this.scrollbarElt = document.getElementById(id); 45 this.scrollerElt = document.getElementById(id+"Scroller"); 46 this.nonHighlightClassName = this.scrollerElt.className; 47 this.scrollerElt.style.position = "relative"; 48 var scrollbar = this; // for function closures 49 this.scrollbarElt.onclick = 50 function(event) { return scrollbar.pageClick(event); }; 51 this.scrollerElt.onmousedown = 52 function(event) { return scrollbar.dragStart(event); }; 53 this.scrollerElt.ontouchstart = 54 function(event) { return scrollbar.touchStart(event); }; 55 this.scrollerElt.ontouchmove = 56 function(event) { return scrollbar.touchMove(event); }; 57 this.scrollerElt.ontouchend = 58 function(event) { return scrollbar.touchEnd(event); }; 59 this.visible = 0; 60 this.set(1.0, 0.0); 61 } 62 63 Scrollbar.prototype.set = function(newVisible, newPos) { 64 // Adjust scrollbar given visibility and position fractions in [0..1], 65 // and current scrollbar element's height. 66 // 67 if (newVisible < 0) newVisible = 0; 68 if (newVisible > 1) newVisible = 1; 69 if (newPos < 0) newPos = 0; 70 if (newPos > 1) newPos = 1; 71 var newScrollbarH = this.scrollbarElt.clientHeight; 72 if (newVisible != this.visible || newPos != this.pos || 73 newScrollbarH != this.scrollbarH) { 74 this.visible = newVisible; 75 this.pos = newPos; 76 this.scrollbarH = newScrollbarH; 77 this.scrollerH = Math.max(45, 78 Math.round(this.scrollbarH * this.visible)); 79 this.scrollerT = 80 Math.round((this.scrollbarH - this.scrollerH) * this.pos); 81 this.scrollerElt.style.opacity = (this.visible == 1 ? 0.5 : 1.0); 82 this.scrollerElt.style.height = "" + 83 (this.scrollerH - 2*this.scrollerElt.clientTop) + "px"; 84 this.scrollerElt.style.top = "" + this.scrollerT + "px"; 85 } 86 } 87 88 Scrollbar.prototype.pageClick = function(event) { 89 // Called from onclick in the scrollbar. Reports to the client as 90 // appropriate by calling the "page" method. 91 // 92 if (!event) event = window.event; // IE versus the rest 93 var mouseY = getMousePos(event).y; 94 var scrollerY = getElementPos(this.scrollerElt).y; 95 if (mouseY < scrollerY) { 96 this.page(-1); 97 } else if (mouseY >= scrollerY + this.scrollerH) { 98 this.page(+1); 99 } 100 return false; 101 } 102 103 Scrollbar.prototype.dragStart = function(event) { 104 // Called from onmousedown (or through touchStart) in scroller. 105 // Sets up dragging state, and visual feedback. 106 // 107 if (!event) event = window.event; // IE versus the rest 108 var scrollbar = this; 109 this.yBase = event.clientY - this.scrollerT; 110 document.onmousemove = 111 function(event) { return scrollbar.dragMove(event); }; 112 document.onmouseup = 113 function(event) { return scrollbar.dragStop(event); }; 114 document.onmouseout = 115 function(event) { return scrollbar.dragOut(event); }; 116 this.scrollerElt.className = "scrollDragging"; 117 this.dragging = true; 118 return false; 119 } 120 121 Scrollbar.prototype.dragMove = function(event) { 122 // Called on onmousemove (or through toucheMove) in scroller. Reports 123 // to the client as appropriate by calling the "scrollTo" method. Does 124 // not itself adjust the scrollbar's appearance or position, except for 125 // removing the dragging visual feedback. 126 // 127 if (!event) event = window.event; // IE versus the rest 128 if (this.dragging) { 129 var newTop = Math.max(0, Math.min(this.scrollbarH - this.scrollerH, 130 event.clientY - this.yBase)); 131 var newPos = (this.scrollbarH == this.scrollerH ? 0.0 : 132 newTop / (this.scrollbarH - this.scrollerH)); 133 if (newPos != this.pos) this.scrollTo(newPos); 134 } 135 return false; 136 } 137 138 Scrollbar.prototype.dragStop = function(event) { 139 // onmouseup event, or equivalent after mouseout, or through touchEnd. 140 // Reports to the client as appropriate by calling the "scrollDone" 141 // method. 142 // 143 if (!event) event = window.event; // IE versus the rest 144 if (this.dragging) { 145 this.scrollerElt.className = this.nonHighlightClassName; 146 document.onmousemove = null; 147 document.onmouseup = null; 148 document.onmouseout = null; 149 this.dragging = false; 150 this.scrollDone(); 151 } 152 return false; 153 } 154 155 Scrollbar.prototype.dragOut = function(event) { 156 // onmouseout event 157 if (!event) event = window.event; // IE versus the rest 158 if (!event.relatedTarget && !event.toElement) { 159 // It didn't go anywhere, so it must have left completely 160 this.dragStop(event); 161 } 162 return false; 163 } 164 165 // Touch-screen functions 166 // 167 168 Scrollbar.prototype.touchStart = function(event) { 169 // Called from ontouchstart in scroller 170 var touches = event.targetTouches; 171 if (touches.length == 1) { 172 event.clientY = touches[0].clientY; 173 this.dragStart(event); 174 } 175 return false; 176 } 177 178 Scrollbar.prototype.touchMove = function(event) { 179 var touches = event.targetTouches; 180 if (touches.length == 1) { 181 event.clientY = touches[0].clientY; 182 this.dragMove(event); 183 } 184 return false; 185 } 186 187 Scrollbar.prototype.touchEnd = function(event) { 188 var touches = event.targetTouches; 189 if (touches.length == 0) { 190 this.dragStop(event); 191 } 192 return false; 193 } 194 195 // Output functions, intended for reporting to the client 196 // 197 198 Scrollbar.prototype.page = function(direction) { 199 // Called on a paging click in the scrollbar, with direction -1 or +1 200 // to decrease or increase the position value. 201 // 202 // Should call the "set" method as appropriate with the new position. 203 // 204 this.set(this.visible, this.pos + direction * this.visible); 205 } 206 207 Scrollbar.prototype.scrollTo = function(newPos) { 208 // Called when scroller is being dragged to a new position. 209 // 210 // Should call the "set" method as appropriate with the new position. 211 // 212 this.set(this.visible, newPos); 213 } 214 215 Scrollbar.prototype.scrollDone = function() { 216 // Called at end of scroller drag. 217 // 218 } 219 220 221 // 222 // Data types 223 // 224 225 function Query(folder) { 226 // Constructor for Query object 227 this.folder = folder; // Restrict to folder, "" for "all except trash" 228 this.words = ""; // Search for words 229 this.findIn = "A"; // Restrict to msg part, "A" for "anywhere" 230 this.dateFrom = "B"; // Start date, "B" for beginning of time 231 this.dateTo = "E"; // End date, "E" for end of time 232 this.acct = 0; // Restrict to account, 0 for "any" 233 this.unread = false; // Restrict to unread messages 234 this.filter = false; // "filter" bit for saved queries 235 } 236 237 function State(query) { 238 // Constructor for main UI State object 239 this.query = query; 240 this.selected = -1; // selected message 241 this.offset = 0; // TOC scroll offset, from end 242 this.tocPageSize = 10; // TOC page size 243 this.totalUnread = -1; // Count of unread messages, if known 244 this.oldFolder = ""; // Prior folder, useful as a default 245 this.sorter = sortByFirst; // Sort order for contacts 246 this.draftFilter = ""; // filter for draft contacts list 247 } 248 249 function Toc() { 250 // Constructor for TOC object 251 this.total = -1; 252 this.lines = new Array(); 253 } 254 255 function Contact(id, first, last, nickname, email, address, 256 home, work, mobile) { 257 // Constructor for Contact object 258 this.id = id; 259 this.first = first; 260 this.last = last; 261 this.nickname = nickname; 262 this.email = email; 263 this.address = address; 264 this.home = home; 265 this.work = work; 266 this.mobile = mobile; 267 } 268 269 function Button(id) { 270 // Constructor for button objects. 271 // Assigns new object to field of "btns". 272 this.element = document.getElementById(this.id = id + "Btn"); 273 if (!this.element) alert("Missing button: " + id); 274 this.active = this.element.innerHTML; 275 this.disabled = "<div class=disabledBtn>" + 276 this.element.firstChild.innerHTML + "</div>"; 277 this.enabled = true; 278 btns[id] = this; 279 } 280 281 Button.prototype.enable = function() { 282 if (!this.enabled) this.element.innerHTML = this.active; 283 this.enabled = true; 284 } 285 286 Button.prototype.disable = function() { 287 if (this.enabled) this.element.innerHTML = this.disabled; 288 this.enabled = false; 289 } 290 291 292 // 293 // Global state 294 // 295 296 // User-meaningful interaction state 297 var user = ""; // logged-in user 298 var theState; // User interaction state 299 var readOnly = false; // if server is configured to be read-only 300 301 // Caches, truth on the server 302 var savedQueries; // User's saved queries, AKA smart folders 303 var theAccounts; // User's accounts 304 var theToc = new Toc(); // Current TOC contents 305 var msgCache = new Cache(50); // Cache of message contents 306 var theContacts = null; // Contacts list, indexed by id as string 307 308 // Transient UI state 309 var contactsChanged; // flag to flush msgCache on contactsDone 310 var contactsKeeping; // flag for "keep" operation on email address 311 var labelOps = 0; // count of pending label operations 312 var deferredOp = null; // Deferred query or scan because of pending ops 313 var deferredCallback; 314 315 // Widget state 316 var shownElement = null; // Pop-up dialog state 317 var btns = new Object(); // Main button bar elements 318 var findFolderFixed; // Fixed part of findFolder selector 319 var moveFolderFixed; // Fixed part of moveFolder selector 320 var tocScrollbar = null; // Scrollbar object for TOC 321 var noMessageSelected = 322 "<b>No message selected</b><br>&nbsp;"; 323 var viewingUnsentFolder = 324 "<b>Click on a message to resume composing it</b><br>&nbsp;"; 325 var noMessagesAtAll = 326 "<b>&nbsp;</b><br>&nbsp;"; 327 var readingContents = 328 "<b>Reading ...</b><br>&nbsp;"; 329 330 function setTitle() { 331 // Update window's title bar 332 // 333 document.title = "Pachylet" + 334 (user == "" ? "" : " (" + user + ") " ) + 335 (theState.totalUnread <= 0 ? "" : 336 theState.totalUnread + " unread message" + 337 (theState.totalUnread == 1 ? "" : "s")); 338 } 339 340 341 // 342 // Modal message screens 343 // 344 345 var flashTimer = null; 346 347 function showModalizer(modalizerId) { 348 // Make the modalizer overlay visible 349 // Also takes down pop-ups, and hides selectors (for IE) 350 // 351 if (flashTimer) window.clearTimeout(flashTimer); 352 toggle(null); 353 document.getElementById("foldersNormal").style.visibility = "hidden"; 354 document.getElementById("foldersSmart").style.visibility = "hidden"; 355 document.getElementById("accounts").style.visibility = "hidden"; 356 var modalizer = document.getElementById(modalizerId); 357 modalizer.style.display = "block"; 358 var topY = getElementPos(modalizer).y; 359 var bottomY = getElementPos(document.getElementById("creditLine")).y; 360 modalizer.style.height = "" + (bottomY-topY) + "px"; 361 } 362 363 function hideModalizer(modalizerId) { 364 // Remove the modalizer overlay 365 // 366 document.getElementById("foldersNormal").style.visibility = "visible"; 367 document.getElementById("foldersSmart").style.visibility = "visible"; 368 document.getElementById("accounts").style.visibility = "visible"; 369 document.getElementById(modalizerId).style.display = "none"; 370 } 371 372 function flash() { 373 // Use modalizer to flash the screen 374 // 375 showModalizer("modalizer"); 376 flashTimer = window.setTimeout( 377 function() { hideModalizer("modalizer"); }, 250); 378 } 379 380 function reportModally(str, promptID, screenID) { 381 // Put up a modal report screen 382 // 383 showModalizer("modalizer"); 384 document.getElementById(promptID).innerHTML = htmlspecials(str); 385 document.getElementById(screenID).style.display = "block"; 386 window.scrollTo(0,0); 387 } 388 389 function modalDone(screenID) { 390 // Exit from a modal report screen 391 // 392 hideModalizer("modalizer"); 393 document.getElementById(screenID).style.display = "none"; 394 } 395 396 function reportProgress(str) { 397 // Report progress modally; cleared by a later call of "progressDone". 398 // 399 reportModally(str, "progressPrompt", "progressScreen"); 400 } 401 402 function progressDone() { 403 // Exit progress report 404 // 405 modalDone("progressScreen"); 406 return false; 407 } 408 409 function noAction() { 410 } 411 412 var reportAction = noAction; 413 414 function reportSuccess(str, action) { 415 // Report success modally; cleared by a user click; then do action 416 // 417 reportAction = action; 418 reportModally(str, "successPrompt", "successScreen"); 419 } 420 421 function successDone() { 422 // Exit success report 423 // 424 modalDone("successScreen"); 425 reportAction(); 426 reportAction = noAction; 427 return false; 428 } 429 430 function reportError(str) { 431 // Report an error modally; cleared by a user click 432 // 433 reportModally(str, "errorPrompt", "errorScreen"); 434 } 435 436 function errorDone() { 437 // exit from error report 438 // 439 modalDone("errorScreen"); 440 return false; 441 } 442 443 function askConfirm(str, action) { 444 // Ask for confirmation modally, and on "yes" do the action 445 // 446 reportAction = action; 447 reportModally(str, "confirmPrompt", "confirmScreen"); 448 } 449 450 function confirmDone(yes) { 451 // exit from confirmation screen 452 // "reportAction" action should be set as appropriate 453 // 454 modalDone("confirmScreen"); 455 if (yes) reportAction(); 456 reportAction = noAction; 457 return false; 458 } 459 460 461 // 462 // Pop-up dialogs 463 // 464 465 function toggle(id) { 466 // Show or hide a pop-up dialog, hiding any others. Use id=null 467 // to hide all dialogs. 468 // 469 if (shownElement) shownElement.style.visibility = 'hidden'; 470 if (id) { 471 var element = document.getElementById(id); 472 if (element == shownElement) { 473 shownElement = null; 474 } else { 475 element.style.visibility = 'visible'; 476 if (id == "find") { 477 setFind(theState.query); 478 } else if (id == "resend") { 479 document.getElementById("resendTo").focus(); 480 } else if (id == "moveCopy") { 481 setOption("moveFolder", theState.oldFolder); 482 } else if (id == "editContact") { 483 document.getElementById("ecFirst").focus(); 484 } 485 shownElement = element; 486 } 487 } else { 488 shownElement = null; 489 } 490 return false; 491 } 492 493 function closeDlog(id) { 494 // Ensure a pop-up dialog is closed 495 var element = document.getElementById(id); 496 if (element == shownElement) toggle(null); 497 } 498 499 500 // 501 // Screen switching 502 // 503 504 function showOtherScreen(other) { 505 // hide main screen and show something else 506 toggle(null); 507 document.getElementById("mainScreen").style.display = "none"; 508 document.getElementById("creditExtra").innerHTML = ""; 509 document.getElementById(other).style.display = "block"; 510 document.getElementById("theBody").className = "otherScreen"; 511 } 512 513 function showMainScreen(other) { 514 // return to main screen from something else (except loginScreen) 515 toggle(null); 516 document.getElementById("theBody").className = "mainScreen"; 517 document.getElementById(other).style.display = "none"; 518 document.getElementById("mainScreen").style.display = "block"; 519 // restore prompt in creditExtra, if any 520 if (theState.selected > 0) { 521 var msgContent = msgCache.read(""+theState.selected); 522 if (msgContent) showTruncatedPrompt(msgContent); 523 } 524 } 525 526 527 // 528 // Making server-side requests 529 // 530 531 function argsForOp(op, offset, selected, dest, startAt, draftArg) { 532 // Return authenticated args for given operation 533 var argUnread = (theState.query.unread ? "Y" : "N"); 534 var argFilter = (theState.query.filter ? "Y" : "N"); 535 var argSelected = (selected ? selected : theState.selected); 536 var argOffset = (offset || offset == 0 ? offset : theState.offset); 537 var argDest = (dest ? dest : ""); 538 var argStartAt = (startAt ? startAt : 0); 539 // Argument defaulting matches that in pachyletV2.php, just to keep 540 // the URL shorter 541 return "op=" + op + 542 "&user=" + encodeURIComponent(user) + 543 (theState.query.folder == "" ? "" : 544 ("&folder=" + encodeURIComponent(theState.query.folder))) + 545 (theState.query.words == "" ? "" : 546 ("&words=" + encodeURIComponent(theState.query.words))) + 547 (theState.query.findIn == "A" ? "" : 548 ("&findIn=" + theState.query.findIn)) + 549 (theState.query.dateFrom == "B" ? "" : 550 ("&dateFrom=" + theState.query.dateFrom)) + 551 (theState.query.dateTo == "E" ? "" : 552 ("&dateTo=" + theState.query.dateTo)) + 553 (theState.query.acct == 0 ? "" : 554 ("&acct=" + theState.query.acct)) + 555 (argUnread == "N" ? "" : ("&unread=" + argUnread)) + 556 (argFilter == "N" ? "" : ("&filter=" + argFilter)) + 557 (argSelected == -1 ? "" : ("&selected=" + argSelected)) + 558 (argOffset == 0 ? "" : ("&offset=" + argOffset)) + 559 (theState.tocPageSize == 10 ? "" : 560 ("&pageSize=" + theState.tocPageSize)) + 561 (argDest == "" ? "" : ("&dest=" + encodeURIComponent(argDest))) + 562 (argStartAt == 0 ? "" : ("&startAt=" + argStartAt)) + 563 (draftArg ? 564 "&draftId=" + draftArg.id + 565 "&msgfrom=" + encodeURIComponent(draftArg.msgfrom) + 566 "&msgto=" + encodeURIComponent(draftArg.msgto) + 567 "&msgcc=" + encodeURIComponent(draftArg.msgcc) + 568 "&subject=" + encodeURIComponent(draftArg.subject) + 569 "&content=" + encodeURIComponent(draftArg.content) : ""); 570 } 571 572 var serverUrl = "pachyletV2.php"; 573 574 function urlForOp(op, offset, selected, dest, startAt, draftArg) { 575 // Return authenticated URL for given operation 576 return serverUrl + "?" + 577 argsForOp(op, offset, selected, dest, startAt, draftArg); 578 } 579 580 function Getter(op) { 581 // Constructor for HTTP requests 582 // Out-of-date "theState" indicates user has logged out since request 583 // Out-of-date "theToc" indicates query has changed since request 584 // Out-of-date "msgCache" indicates msgCache has been flushed 585 // Selected values of "op" are flagged as label ops, so as to defer 586 // later jumpTo or scan until async label ops complete. 587 this.state = theState; 588 this.toc = theToc; 589 this.msgCache = msgCache; 590 this.op = op; 591 this.labelOp = (op == "move" || op == "copy" || 592 op == "moveAll" || op == "copyAll" || 593 op == "markRead" || op == "markUnread" || 594 op == "markAllRead" || op == "markAllUnread"); 595 this.contactOp = (op == "saveContact" || op == "deleteContact"); 596 this.folderOp = (op == "createFolder" || op == "deleteFolder" || 597 op == "saveQuery" || op == "deleteQuery"); 598 this.settingsOp = (op == "saveSettings" || op == "saveAccount" || 599 op == "forgetAccount"); 600 this.composeOp = (op == "compose" || op == "reply" || 601 op == "replyAll" || op == "forward" || op == "reopenDraft"); 602 this.sendOp = (op == "sendDraft" || op == "saveDraft" || 603 op == "deleteDraft"); 604 } 605 606 Getter.prototype.handleFailure = function(xmlhttp) { 607 // Fix up on failure of server request 608 if ((this.op == "login") && 609 this.state == theState) hideModalizer("loggingIn"); 610 if (this.labelOp) doneLabelOp(); 611 progressDone(); 612 var prompt = (!xmlhttp.status ? "no status" : 613 "status = " + xmlhttp.status); 614 if (this.state != theState) { 615 // User has logged out since the request - ignore the response 616 } else if (this.op == "saveDraft" && this.draft.id != curDraftId()) { 617 // saveDraft on a draft we've abandoned 618 } else if (this.toc == theToc && (this.op == "fetchMail" || 619 this.op == "getMsgs" || 620 this.op == "scan")) { 621 setTocPrompt('HTTP request failed (' + prompt + 622 '). Select a folder or click "find"'); 623 } else { 624 if (this.composeOp) { 625 returnFromDraft(); 626 } else if (this.sendOp) { 627 showDraftScreen(); 628 } 629 alert("HTTP request" + 630 (this.op ? (" \"" + this.op + "\"") : "") + " failed."); 631 } 632 } 633 634 Getter.prototype.handleResult = function(xmlhttp) { 635 // Process response from server 636 // alert(xmlhttp.responseText); 637 if (this.labelOp) doneLabelOp(); 638 if (this.contactOp || this.folderOp || this.settingsOp) { 639 progressDone(); 640 } 641 var responseXML = xmlhttp.responseXML; 642 var doc = (responseXML ? responseXML.documentElement : null); 643 if (this.state != theState) { 644 // User has logged out since the request - ignore the response 645 } else if (!doc) { 646 alert("Internal error: response isn't valid XML\n\n" + 647 xmlhttp.responseText); 648 } else if (doc.nodeName == "loginFailed") { 649 doLogout(); 650 reportError("Login failed: " + doc.getAttribute("status")); 651 } else if (doc.nodeName == "user") { 652 // User details in response to "login" 653 hideModalizer("loggingIn"); 654 setTitle(); 655 readOnly = doc.getAttribute("ro"); 656 if (readOnly) { 657 btns.contacts.disable(); 658 btns.fetch.disable(); 659 btns.folders.disable(); 660 btns.settings.disable(); 661 } 662 receiveAccounts(doc); 663 var recvdFolders = receiveFolders(doc); 664 var recvdQueries = receiveQueries(doc); 665 updateAccountsUI(); 666 var orderFoldersSel = document.getElementById("foldersOrder"); 667 var normalFoldersSel = document.getElementById("foldersNormal"); 668 var smartFoldersSel = document.getElementById("foldersSmart"); 669 var findFolderSel = document.getElementById("findFolder"); 670 var moveFolderSel = document.getElementById("moveFolder"); 671 truncateOptions("foldersNormal", 0); 672 truncateOptions("foldersSmart", 0); 673 truncateOptions("foldersOrder", 0); 674 for (var i = 0; i < recvdQueries.length; i++) { 675 appendOption(orderFoldersSel, recvdQueries[i], recvdQueries[i]); 676 } 677 var fSort = recvdFolders.sort(caseSort); // modified recvdFolders 678 for (var i = 0; i < fSort.length; i++) { 679 appendOption(normalFoldersSel, fSort[i], fSort[i]); 680 } 681 if (recvdFolders.length > 0) normalFoldersSel.selectedIndex = 0; 682 var qSort = recvdQueries.sort(caseSort); // modifies recvdQueries 683 for (var i = 0; i < qSort.length; i++) { 684 appendOption(smartFoldersSel, qSort[i], qSort[i]); 685 } 686 if (recvdQueries.length > 0) smartFoldersSel.selectedIndex = 0; 687 var fq = recvdFolders.concat(recvdQueries).sort(caseSort); 688 for (var i = 0; i < fq.length; i++) { 689 appendOption(findFolderSel, fq[i], fq[i]); 690 appendOption(moveFolderSel, fq[i], fq[i]); 691 } 692 setOpeners(); 693 showMainScreen("loginScreen"); 694 tocScrollbar = newTocScrollbar(); 695 jumpTo(theState.query); 696 } else if (doc.nodeName == "contactSaved") { 697 id = doc.getAttribute("id"); 698 if (contactsKeeping) { 699 contactsChanged = true; // flush msgCache in contactsDone 700 theContacts = null; // refresh contacts next time 701 contactsDone(); 702 } else if (!theContacts || !theContacts[id]) { 703 theContacts = null; // refresh contacts in doContacts 704 doContacts(); 705 contactsChanged = true; // flush msgCache in contactsDone 706 } else { 707 contactsChanged = true; // flush msgCache in contactsDone 708 renderContacts(); 709 } 710 } else if (doc.nodeName == "contactDeleted") { 711 id = doc.getAttribute("id"); 712 contactsChanged = true; // flush msgCache in contactsDone 713 theContacts[id] = null; 714 renderContacts(); 715 } else if (doc.nodeName == "folderCreated") { 716 var folder = decodeURIComponent(doc.getAttribute("folder")); 717 insertOption("foldersNormal", 0, folder, folder, true); 718 propagateFolderCreation(folder); 719 } else if (doc.nodeName == "folderDeleted") { 720 var folder = decodeURIComponent(doc.getAttribute("folder")); 721 propagateFolderDeletion(folder); 722 } else if (doc.nodeName == "querySaved") { 723 var folder = decodeURIComponent(doc.getAttribute("folder")); 724 if (!existsOption("foldersSmart", folder)) { 725 insertOption("foldersSmart", 0, folder, folder, true); 726 propagateFolderCreation(folder); 727 appendOption(document.getElementById("foldersOrder"), 728 folder, folder); 729 } 730 } else if (doc.nodeName == "queryDeleted") { 731 var folder = decodeURIComponent(doc.getAttribute("folder")); 732 propagateFolderDeletion(folder); 733 deleteOption("foldersOrder", folder); 734 } else if (doc.nodeName == "promoted" || 735 doc.nodeName == "demoted") { 736 document.getElementById("promoteBtn").disabled = false; 737 document.getElementById("demoteBtn").disabled = false; 738 } else if (doc.nodeName == "settingsSaved") { 739 // Response to saveSettings 740 var tz = doc.getAttribute("displaytz"); 741 for (var id in theAccounts) { 742 theAccounts[id].displaytz = tz; 743 } 744 } else if (doc.nodeName == "accountSaved") { 745 receiveAccount(doc); 746 updateAccountsUI(); 747 } else if (doc.nodeName == "accountForgotten") { 748 theAccounts[doc.getAttribute("id")] = null; 749 updateAccountsUI(); 750 } else if (this.toc != theToc) { 751 // Query has changed - ignore the response 752 } else if (doc.nodeName == "scanned") { 753 var found = doc.getAttribute("folder"); 754 if (found == "") { 755 doOpen("Inbox"); 756 } else { 757 doOpen(found); 758 } 759 } else if (doc.nodeName == "toc") { 760 // Query summary and TOC lines, in response to "getMsgs" 761 var recvdToc = receiveToc(doc); 762 if (recvdToc.prefetch) pachyGet("getMsg", null, recvdToc.prefetch); 763 if (theState.selected < 0) { 764 // first read after abandoning previous TOC 765 if (theToc.total > 0 && 766 theState.query.folder != "Unsent") { 767 if (!readOnly) btns.file.enable(); 768 document.getElementById("moveAllBtn").disabled = false; 769 document.getElementById("copyAllBtn").disabled = false; 770 if (!readOnly) btns.mark.enable(); 771 } 772 show(recvdToc.selected, recvdToc.selOffset); 773 prefetchToc(); 774 } else { 775 // Scrolling or pre-reading within existing TOC 776 showToc(); 777 } 778 setTitle(); 779 } else if (doc.nodeName == "msg") { 780 receiveMsg(this, doc); 781 } else if (doc.nodeName == "rawHeader" || 782 doc.nodeName == "rawMessage") { 783 var id = parseInt(doc.getAttribute("id")); 784 if (id == theState.selected) { 785 var startAt = parseInt(doc.getAttribute("startAt")); 786 var content = getBulkData(doc); 787 var tempHdr = "<b>Raw " + 788 (doc.nodeName == "rawHeader" ? "Header:" : "Message:") + 789 " </b>"; 790 if (startAt != 0) { 791 tempHdr += "<a href=\"#\" " + 792 "onClick=\"return showAltMsg(" + id + "," + 793 startAt + ", '')\" " + 794 "title=\"Return to attached message\"" + 795 ">sub-message #" + startAt + "</a> of "; 796 } 797 tempHdr += "<a href=\"#\" onClick=\"return " + 798 "showSelectedMessage()\" title=\"Return to message #" + id + 799 "\">message #" + id + "</a>"; 800 showContents(tempHdr, 801 "<div class=fixedNoWrap>" + 802 htmlspecials(content) + 803 "</div>"); 804 } 805 } else if (doc.nodeName == "done") { 806 // Response to delete/move/copy/mark 807 } else if (doc.nodeName == "unknownOp") { 808 alert("Unknown operation: " + doc.getAttribute("op")); 809 } else { 810 this.callback(doc); 811 } 812 } 813 814 function checkResult(doc, name) { 815 // Verify that result has the correct top-level nodename. For use 816 // in callbacks from handleResult. 817 // 818 if (doc.nodeName == name) return true; 819 alert("Unexpected result " + doc.nodeName); 820 return false; 821 } 822 823 function dummyCallback(doc) { 824 // Dummy completion callback, for operations that need nothing. 825 } 826 827 function legacyCallback(doc) { 828 // Callback from legacy pachyGet: the doc.nodeName should have been 829 // recognized in "handleResult". Reports as an error. 830 // 831 checkResult(doc, ""); 832 } 833 834 function pachyGet2(op, callback, offset, selected, dest, startAt) { 835 // Call the client-side script to do something, asynchronously, 836 // with a GET request. 837 // 838 var getter = new Getter(op); 839 if (getter.labelOp) { 840 if (labelOps == 0) { 841 document.getElementById("writing").style.visibility = "visible"; 842 } 843 labelOps++; 844 } 845 if (getter.contactOp) reportProgress("Updating contacts ..."); 846 if (getter.folderOp) reportProgress("Updating folders ..."); 847 if (getter.settingsOp) reportProgress("Updating settings ..."); 848 getter.url = urlForOp(op, offset, selected, dest, startAt); 849 getter.callback = (callback ? callback : dummyCallback); 850 initiateXMLHttp(getter); 851 } 852 853 function pachyGet(op, offset, selected, dest, startAt) { 854 // Legacy GET operations, without a callback 855 // 856 pachyGet2(op, legacyCallback, offset, selected, dest, startAt); 857 } 858 859 function pachyPostPwd(op, callback, dest) { 860 // Call the client-side script to do an operation involving a password, 861 // with a POST request. 862 // 863 var getter = new Getter(op); 864 getter.url = serverUrl; 865 getter.postData = argsForOp(op, null, null, dest); 866 getter.callback = (callback ? callback : dummyCallback); 867 initiateXMLHttp(getter); 868 } 869 870 function pachyPostDraft(op, callback, draft) { 871 // Call the client-side script to do a send-like operation, with a 872 // POST request. 873 // 874 var getter = new Getter(op); 875 getter.url = serverUrl; 876 getter.postData = argsForOp(op, null, null, null, null, draft); 877 getter.callback = (callback ? callback : dummyCallback); 878 getter.draft = draft; 879 initiateXMLHttp(getter); 880 } 881 882 function doneLabelOp() { 883 // Completion of a label op 884 labelOps--; 885 if (labelOps < 0) alert("negative label op count"); 886 if (labelOps == 0) { 887 var writing = document.getElementById("writing"); 888 if (writing) writing.style.visibility = "hidden"; 889 if (deferredOp) { 890 var op = deferredOp; 891 deferredOp = null; 892 pachyGet(op); 893 } 894 } 895 } 896 897 898 // 899 // Login screen 900 // 901 902 function doLogin() { 903 user = utf8(document.getElementById("loginUser").value); 904 var loginPwd = utf8(document.getElementById("loginPwd").value); 905 document.getElementById("loginPwd").value = ""; 906 document.getElementById("loginUser").blur(); 907 document.getElementById("loginPwd").blur(); 908 pachyPostPwd("login", null, loginPwd); 909 loginPwd = ""; 910 showModalizer("loggingIn"); 911 return false; 912 } 913 914 function autoLogin() { 915 // If appropriate, log the user in automatically 916 // 917 var autoUser = getCookie("pachydkuser"); 918 if (autoUser && autoUser == user) doLogin(); 919 } 920 921 function receiveAccount(acctXML) { 922 // Update "theAccounts" with newly received account data 923 var id = parseInt(acctXML.getAttribute("id")); 924 var acct = new Object(); 925 acct.server = decodeURIComponent(acctXML.getAttribute("server")); 926 acct.user = decodeURIComponent(acctXML.getAttribute("user")); 927 acct.type = acctXML.getAttribute("type"); 928 acct.msgfrom = decodeURIComponent(acctXML.getAttribute("msgfrom")); 929 acct.person = decodeURIComponent(acctXML.getAttribute("person")); 930 acct.dodelete = (acctXML.getAttribute("dodelete") == "Y"); 931 acct.displaytz = acctXML.getAttribute("displaytz"); 932 theAccounts[id] = acct; 933 } 934 935 function receiveAccounts(doc) { 936 // Initialize theAccounts based on response from server 937 theAccounts = new Array(); 938 var acctsXML = doc.getElementsByTagName("account"); 939 for (var i = 0; i < acctsXML.length; i++) { 940 receiveAccount(acctsXML[i]); 941 } 942 setupDraftFrom(); 943 } 944 945 function receiveFolders(doc) { 946 // Return array of folder names received from the server 947 var recvdFolders = new Array(); 948 var folders = doc.getElementsByTagName("folder"); 949 for (var i = 0; i < folders.length; i++) { 950 recvdFolders[i] = decodeURIComponent( 951 folders[i].getAttribute("folder")); 952 } 953 return recvdFolders; 954 } 955 956 function receiveQueries(doc) { 957 // Set savedQueries with result received from server. 958 // Also returns array of saved query names 959 var recvdQueries = new Array(); 960 var queries = doc.getElementsByTagName("query"); 961 savedQueries = new Object(); 962 for (var i = 0; i < queries.length; i++) { 963 var recvdName = decodeURIComponent( 964 queries[i].getAttribute("name")); 965 var recvdQ = new Query(""); 966 recvdQ.filter = (queries[i].getAttribute("filter") == "Y"); 967 recvdQ.keepUnread = (queries[i].getAttribute("unread") == "Y"); 968 // recvdQ.moveTo = decodeURIComponent( 969 // queries[i].getAttribute("folder")); 970 recvdQ.words = decodeURIComponent( 971 queries[i].getAttribute("words")); 972 recvdQ.findIn = queries[i].getAttribute("findIn"); 973 recvdQ.dateFrom = queries[i].getAttribute("dateFrom"); 974 recvdQ.dateTo = queries[i].getAttribute("dateTo"); 975 recvdQ.acct = queries[i].getAttribute("acct"); 976 recvdQueries[i] = recvdName; 977 savedQueries[recvdName] = recvdQ; 978 } 979 return recvdQueries; 980 } 981 982 function doLogoutUI() { 983 // Clean up the UI after logout 984 truncateOptions("findFolder", findFolderFixed); 985 truncateOptions("moveFolder", moveFolderFixed); 986 truncateOptions("foldersNormal", 0); 987 truncateOptions("foldersSmart", 0); 988 truncateOptions("foldersOrder", 0); 989 document.getElementById("promoteBtn").disabled = false; 990 document.getElementById("demoteBtn").disabled = false; 991 truncateOptions("findAcct", 1); 992 truncateOptions("eqAcct", 1); 993 truncateOptions("accounts", 0); 994 setOpeners(); 995 var prevUser = user; user = ""; setTitle(); user = prevUser; 996 showContents(noMessageSelected); 997 setTocPrompt("Logged out"); 998 document.getElementById("contactsScreen").style.display = "none"; 999 document.getElementById("foldersNormal").style.visibility = "visible"; 1000 document.getElementById("foldersSmart").style.visibility = "visible"; 1001 document.getElementById("foldersScreen").style.display = "none"; 1002 document.getElementById("accounts").style.visibility = "visible"; 1003 document.getElementById("settingsScreen").style.display = "none"; 1004 showOtherScreen("loginScreen"); 1005 hideModalizer("loggingIn"); 1006 } 1007 1008 function doLogoutAtServer() { 1009 // Ask server to erase our authentication cookie 1010 // 1011 reportProgress("Logging out"); 1012 pachyGet2("logout", doneLogout); 1013 } 1014 1015 function doneLogout(doc) { 1016 // Completion of doLogoutAtServer 1017 // 1018 progressDone(); 1019 } 1020 1021 function doLogout() { 1022 // Logout, or cancel in-progress login 1023 var oldPage = theState.tocPageSize; 1024 theState = new State(new Query("Inbox")); 1025 theState.tocPageSize = oldPage; 1026 savedQueries = null; 1027 theAccounts = null; 1028 theToc = new Toc(); 1029 theContacts = null; 1030 msgCache = new Cache(50); 1031 deferredOp = null; 1032 argUser = getQueryArg("user"); 1033 if (argUser) { 1034 user = argUser; 1035 document.getElementById("loginUser").value = user; 1036 } 1037 doLogoutUI(); 1038 doLogoutAtServer(); 1039 return false; 1040 } 1041 1042 1043 // 1044 // TOC operations 1045 // 1046 1047 function setTocPrompt(prompt) { 1048 // Set a user prompt on the TOC area, suitably padded 1049 document.getElementById("toc").innerHTML = 1050 "<b>" + prompt + "</b><br><br><br><br><br><br><br><br><br><br><br>"; 1051 } 1052 1053 function getToc(fetch) { 1054 // Execute a query on the server to fetch theState.tocPageSize TOC lines 1055 // Can get deferred because of outstanding labelOps. 1056 var op = (fetch ? "fetchMail" : "getMsgs"); 1057 if (theToc.total < 0) { 1058 // First use of query 1059 setTocPrompt((theState.query.folder == "" ? "All except Trash" : 1060 htmlspecials(theState.query.folder)) + ": " + 1061 (fetch ? "fetching new mail and " : "") + 1062 "finding matching messages " + 1063 (labelOps > 0 ? "(waiting) " : "") + 1064 "..."); 1065 } 1066 if (labelOps > 0) { 1067 deferredOp = op; 1068 } else { 1069 pachyGet(op); 1070 } 1071 } 1072 1073 function receiveToc(doc) { 1074 // Receive TOC result from server, and return details in an object 1075 // No UI side-effects: shared with iPhone 1076 var recvdToc = new Object(); 1077 var tocLines = doc.getElementsByTagName("tocLine"); 1078 var offset = parseInt(doc.getAttribute("offset")); 1079 recvdToc.selected = parseInt(doc.getAttribute("selected")); 1080 for (var i = 0; i < tocLines.length; i++) { 1081 if (!theToc.lines[i+offset]) { 1082 var recvd = tocLines[i]; 1083 var tocLine = new Object(); 1084 tocLine.id = parseInt(recvd.getAttribute("id")); 1085 tocLine.date = 1086 decodeURIComponent(recvd.getAttribute("date")); 1087 tocLine.from = 1088 decodeURIComponent(recvd.getAttribute("from")); 1089 tocLine.subject = 1090 decodeURIComponent(recvd.getAttribute("subject")); 1091 tocLine.unread = (recvd.getAttribute("unread") == "Y"); 1092 theToc.lines[i+offset] = tocLine; 1093 if (tocLine.id == recvdToc.selected) { 1094 recvdToc.selOffset = i+offset; 1095 } 1096 if (theState.query.folder != "Unsent" && 1097 i+offset == theState.selOffset-1) { 1098 recvdToc.prefetch = tocLine.id; 1099 } 1100 } 1101 } 1102 if (theState.selected < 0) { 1103 // first read after abandoning previous TOC 1104 theState.offset = offset; 1105 theToc.total = parseInt(doc.getAttribute("total")); 1106 theState.totalUnread = parseInt(doc.getAttribute("totalUnread")); 1107 } 1108 return recvdToc; 1109 } 1110 1111 function disableMsgBtns() { 1112 // Disable buttons that require a message to be selected 1113 btns.resend.disable(); 1114 btns.forward.disable(); 1115 btns.replyAll.disable(); 1116 btns.reply.disable(); 1117 btns.drop.disable(); 1118 btns.trash.disable(); 1119 document.getElementById("deleteBtn").disabled = true; 1120 document.getElementById("moveBtn").disabled = true; 1121 document.getElementById("copyBtn").disabled = true; 1122 document.getElementById("markUnreadBtn").disabled = true; 1123 } 1124 1125 function abandonToc() { 1126 // Discard TOC in preparation for new query 1127 // Implicitly discards responses to related requests (in handleResult) 1128 theState.offset = 0; 1129 theState.selected = -1; 1130 theToc = new Toc(); // discard old TOC data, and also old requests 1131 showContents(noMessagesAtAll); 1132 tocScrollbar.set(1.0, 0.0); 1133 disableMsgBtns(); 1134 btns.next.disable(); 1135 btns.file.disable(); 1136 document.getElementById("moveAllBtn").disabled = true; 1137 document.getElementById("copyAllBtn").disabled = true; 1138 btns.mark.disable(); 1139 } 1140 1141 function jumpTo(query, fetch, dontClose) { 1142 // Execute the given query, optionally first fetching new mail, 1143 // and normally closing any dialogs. 1144 if (!dontClose) toggle(null); 1145 if (query.folder != theState.query.folder) { 1146 theState.oldFolder = theState.query.folder; 1147 } 1148 theState.query = query; 1149 abandonToc(); 1150 getToc(fetch); 1151 } 1152 1153 function fetchMail() { 1154 // Manual prod at incorporating new mail 1155 jumpTo(theState.query, true); 1156 return false; 1157 } 1158 1159 function fixFindPrompt(id, off) { 1160 // fix up a single prompt in "find" dialog 1161 document.getElementById(id + "Prompt").className = 1162 ( off ? "findPromptOff" : "findPromptOn"); 1163 document.getElementById(id + "Clear").style.visibility = 1164 ( off ? "hidden" : "inherit"); 1165 } 1166 1167 function fixFindPrompts() { 1168 // Respond to change in "find" dialog widgets 1169 fixFindPrompt("findFolder", getOption("findFolder") == ""); 1170 fixFindPrompt("findIn", getOption("findIn") == "A"); 1171 fixFindPrompt("findFrom", getOption("findFrom") == "B"); 1172 fixFindPrompt("findTo", getOption("findTo") == "E"); 1173 fixFindPrompt("findAcct", getOption("findAcct") == "0"); 1174 fixFindPrompt("findUnread", !getCheckbox("findUnread")); 1175 document.getElementById("findWords").focus(); 1176 } 1177 1178 function findPromptClear(id) { 1179 if (id == "findUnread") { 1180 setCheckbox(id, false); 1181 } else { 1182 setOptSelection(id, 0); 1183 } 1184 fixFindPrompts(); 1185 return false; 1186 } 1187 1188 function setFind(query) { 1189 // Set fields in the "find" dialog according to given query, and focus 1190 setOption("findFolder", query.folder); 1191 document.getElementById("findWords").value = query.words; 1192 setOption("findIn", query.findIn); 1193 setOption("findFrom", query.dateFrom); 1194 setOption("findTo", query.dateTo); 1195 setOption("findAcct", query.acct); 1196 setCheckbox("findUnread", query.unread); 1197 fixFindPrompts(); 1198 } 1199 1200 function findClear() { 1201 // "Clear" button in "find" dialog 1202 setFind(new Query("")); 1203 return false; 1204 } 1205 1206 function findClose() { 1207 // Close "find" dialog for "doFind", if appropriate 1208 if (!getCheckbox("findStayOpen")) toggle("find"); 1209 } 1210 1211 function doFind() { 1212 // "Find" button in "find" dialog 1213 findClose(); 1214 var query = new Query(""); 1215 query.folder = getOption("findFolder"); 1216 if ((!query.folder && query.folder != "") || query.folder == "-") { 1217 reportError("No folder selected"); 1218 } else { 1219 query.words = document.getElementById("findWords").value; 1220 query.findIn = getOption("findIn"); 1221 query.dateFrom = getOption("findFrom"); 1222 query.dateTo = getOption("findTo"); 1223 query.acct = getOption("findAcct"); 1224 query.unread = getCheckbox("findUnread"); 1225 jumpTo(query, false, true); 1226 } 1227 return false; 1228 } 1229 1230 function doOpen(folder, fetch) { 1231 // Show given folder's messages, optionally first fetching new mail 1232 jumpTo(new Query(folder), fetch); 1233 return false; 1234 } 1235 1236 function doScan() { 1237 // Scan smart folders for unread messages 1238 toggle(null); 1239 abandonToc(); 1240 setTocPrompt("Scanning ..."); 1241 if (labelOps > 0) { 1242 deferredOp = "scan"; 1243 } else { 1244 pachyGet("scan"); 1245 } 1246 return false; 1247 } 1248 1249 function showToc() { 1250 // Show the TOC at its current scroll offset 1251 var last = theToc.total - theState.offset; 1252 var first = last - theState.tocPageSize + 1; 1253 if (first <= 0) first = 1; 1254 var temp = "<b>" + 1255 (theState.query.folder == "" ? "All except Trash" : 1256 htmlspecials(theState.query.folder)) + ": " + 1257 (theToc.total == 0 ? "no" : 1258 (last - first + 1 == theToc.total ? "all " : 1259 first + "-" + last + " of ") + theToc.total) + 1260 " matching messages</b><br>"; 1261 var gotEverything = true; 1262 for (var i = theState.offset + theState.tocPageSize - 1; 1263 i >= theState.offset; i--) { 1264 var tocLine = theToc.lines[i]; 1265 if (tocLine) { 1266 if (tocLine.id == theState.selected) { 1267 temp += "&gt; " + 1268 tocLine.date + "&nbsp; " + 1269 "<span title=\"Showing message #" + 1270 tocLine.id + "\">" + 1271 tocLine.from + "&nbsp; " + tocLine.subject + 1272 "</span>"; 1273 } else { 1274 temp += (tocLine.unread ? "? " : "&nbsp; ") + 1275 tocLine.date + "&nbsp; " + 1276 "<a href=\"#\" onClick=\"return show(" + tocLine.id + 1277 "," + i + ")\" title=\"Show message #" + 1278 tocLine.id + "\">" + 1279 tocLine.from + "&nbsp; " + tocLine.subject + 1280 "</a>"; 1281 } 1282 } else if (i < theToc.total) { 1283 temp += "&nbsp; &nbsp;Message " + (theToc.total-i) + " ..."; 1284 gotEverything = false; 1285 } 1286 temp += "<br>"; 1287 } 1288 document.getElementById("toc").innerHTML = temp; 1289 tocScrollbar.set( 1290 (theToc.total == 0 ? 1.0 : (last-first+1)/theToc.total), 1291 (first == 1 ? 0.0 : (first-1)/(theToc.total-(last-first+1)))); 1292 return gotEverything; 1293 } 1294 1295 function tocPageKnown(offset) { 1296 // Return true iff TOC page starting at "offset" is cached 1297 if (theToc.total < 0) { 1298 return false; 1299 } else { 1300 var known = true; 1301 for (var i = offset + theState.tocPageSize - 1; i >= offset; i--) { 1302 if (i < theToc.total && !theToc.lines[i]) { 1303 known = false; 1304 break; 1305 } 1306 } 1307 return known; 1308 } 1309 } 1310 1311 function prefetchToc() { 1312 var nextBefore = theState.offset + theState.tocPageSize; 1313 if (nextBefore >= theToc.total) nextBefore = theToc.total-1; 1314 if (nextBefore > theState.offset && !tocPageKnown(nextBefore)) { 1315 pachyGet("getMsgs", nextBefore); 1316 } 1317 var nextAfter = theState.offset - theState.tocPageSize; 1318 if (nextAfter < 0) nextAfter = 0; 1319 if (nextAfter < theState.offset && !tocPageKnown(nextAfter)) { 1320 pachyGet("getMsgs", nextAfter); 1321 } 1322 } 1323 1324 function scrollTocTo(dest, deferGet) { 1325 // Scroll within the TOC bounds; NOOP before query has been executed 1326 var deferred = false; 1327 if (theToc.total >= 0) { 1328 // If we have executed the current query before 1329 if (dest > theToc.total - theState.tocPageSize) 1330 dest = theToc.total - theState.tocPageSize; 1331 if (dest < 0) dest = 0; 1332 if (dest != theState.offset) { 1333 theState.offset = dest; 1334 if (!showToc()) { 1335 if (deferGet) deferred = true; else getToc(); 1336 } 1337 } 1338 if (!deferGet) prefetchToc(); 1339 } 1340 return deferred; 1341 } 1342 1343 function goBack() { 1344 scrollTocTo(theState.offset + theState.tocPageSize); 1345 return false; 1346 } 1347 1348 function goForward() { 1349 scrollTocTo(theState.offset - theState.tocPageSize); 1350 return false; 1351 } 1352 1353 function goDelta(delta) { 1354 // the actual delta values are confused: Safari seems to use 12 for 1355 // one click, IE uses 120, and Opera uses 3. 1356 scrollTocTo(theState.offset + (delta > 0 ? 1 : -1)); 1357 return false; 1358 } 1359 1360 function goToStart() { 1361 scrollTocTo(theToc.total - theState.tocPageSize); 1362 return false; 1363 } 1364 1365 function goToEnd() { 1366 scrollTocTo(0); 1367 return false; 1368 } 1369 1370 function tocScrollbarScrollTo(newPos) { 1371 this.deferred |= scrollTocTo(theToc.total - theState.tocPageSize - 1372 Math.round((theToc.total - theState.tocPageSize) * newPos), 1373 true); 1374 } 1375 1376 function tocScrollbarDone() { 1377 if (this.deferred) { 1378 getToc(); 1379 prefetchToc(); 1380 this.deferred = false; 1381 } 1382 } 1383 1384 function tocScrollbarPage(direction) { 1385 scrollTocTo(theState.offset - direction * theState.tocPageSize); 1386 } 1387 1388 function newTocScrollbar() { 1389 // Create the TOC scrollbar, overriding the action methods 1390 var res = new Scrollbar("tocScrollbar"); 1391 res.scrollTo = tocScrollbarScrollTo; 1392 res.scrollDone = tocScrollbarDone; 1393 res.page = tocScrollbarPage; 1394 res.deferred = false; 1395 addMouseWheel("tocWrapper", goDelta); 1396 return res; 1397 } 1398 1399 1400 // 1401 // Message display 1402 // 1403 1404 // Summary of the main message display functions: 1405 // 1406 // showContents ... places given HTML in the message display area 1407 // showMessageContents ... displays a message object (which might be a 1408 // sub-message); calls showContents; initiates message 1409 // pre-fetch if appropriate. 1410 // showSelectedMessage ... shows the currently selected message, from 1411 // cache or by fetching it. Does no TOC manipulation. 1412 // show ... makes the given message ID be the one we're trying to 1413 // display. Calls showSelectedMessage and adjusts the 1414 // TOC appropriately. 1415 // showAltMsg ... initiates read of a sub-message or alternative part 1416 // receiveMsg ... takes message response from server, and builds message 1417 // object; caches it if appropriate, and displays it if 1418 // appropriate, by calling showMessageContents. 1419 // 1420 // Note that "show" is the only thing that changes the shown message ID. The 1421 // others gets used to display sub-messages, alternative views, or 1422 // miscellanous prompts within that message ID. 1423 1424 function showContents(head, body, extraDone) { 1425 // Display some HTML in the contents area 1426 document.getElementById("msgHead").innerHTML = head; 1427 document.getElementById("msgBody").innerHTML = (body ? body : ""); 1428 if (!extraDone) document.getElementById("creditExtra").innerHTML = ""; 1429 } 1430 1431 function showTruncatedPrompt(msgContent) { 1432 // Display "truncated" prompt in creditExtra and return prompt suitable 1433 // for inclusion in header display 1434 var truncated; 1435 if (msgContent.truncated) { 1436 var clickHere = "<a href=\"#\" " + 1437 "onClick=\"return showAltMsg(" + msgContent.id + "," + 1438 msgContent.startAt + ", '" + 1439 msgContent.selectedPart + "', true)\" " + 1440 "title=\"Show full message\"" + 1441 ">View all " + msgContent.fullLength + " bytes</a>"; 1442 truncated = "<br><b>Truncated: </b>" + 1443 "Showing first " + msgContent.sentLength + " bytes. " + 1444 clickHere; 1445 document.getElementById("creditExtra").innerHTML = 1446 "&nbsp; (" + clickHere + ")"; 1447 } else { 1448 truncated = ""; 1449 document.getElementById("creditExtra").innerHTML = ""; 1450 } 1451 return truncated; 1452 } 1453 1454 function showMessageContents(msgContent) { 1455 // Display received message contents, and pre-fetch next message 1456 showContents(msgContent.header + 1457 (msgContent.startAt == 0 && 1458 msgContent.folders != theState.query.folder ? 1459 "<br><b>Folders: &nbsp;</b>" + msgContent.folders : "") + 1460 showTruncatedPrompt(msgContent) + "<br><br>", 1461 msgContent.body, true); 1462 if (theState.selOffset > 0) { 1463 var nextLine = theToc.lines[theState.selOffset - 1]; 1464 if (nextLine && !msgCache.read(""+nextLine.id)) { 1465 pachyGet("getMsg", null, nextLine.id); 1466 } 1467 } 1468 } 1469 1470 function showSelectedMessage(tocLine) { 1471 // Show the selected message if it's cached, or fetch it. 1472 // "tocLine" is optional hint for some of the message's header. 1473 // 1474 // Assumes this is the selected message and that TOC is set up properly. 1475 // 1476 var content = msgCache.read(""+theState.selected); 1477 if (content) { 1478 showMessageContents(content); 1479 } else { 1480 var prompt = "<b>Reading ...</b><br>"; 1481 if (tocLine) { 1482 prompt += "<b>Subject: &nbsp;</b>" + tocLine.subject; 1483 } else { 1484 prompt += "&nbsp;"; 1485 } 1486 showContents(prompt); 1487 pachyGet("getMsg"); 1488 } 1489 return false; 1490 } 1491 1492 function show(id, offset) { 1493 // Scroll to message in TOC, and display it (perhaps asynchronously) 1494 if (theState.query.folder == "Unsent") { 1495 theState.selected = 0; 1496 theState.selOffset = 0; 1497 } else { 1498 theState.selected = id; 1499 theState.selOffset = offset; 1500 } 1501 if (offset < theState.offset) { 1502 scrollTocTo(theState.offset - theState.tocPageSize); 1503 } else if (offset >= theState.offset + theState.tocPageSize) { 1504 scrollTocTo(theState.offset + theState.tocPageSize); 1505 } else { 1506 if (!showToc()) getToc(); 1507 } 1508 if (theState.query.folder == "Unsent") { 1509 if (id == 0) { 1510 showContents(theToc.total > 0 ? viewingUnsentFolder : 1511 noMessagesAtAll); 1512 } else { 1513 doReopenDraft(id); 1514 } 1515 } else if (theState.selected <= 0) { 1516 showContents(theToc.total > 0 ? noMessageSelected : 1517 noMessagesAtAll); 1518 } else { 1519 var tocLine = theToc.lines[theState.selOffset]; 1520 if (tocLine.unread) { 1521 pachyGet("markRead"); 1522 theState.totalUnread--; 1523 setTitle(); 1524 } 1525 tocLine.unread = false; 1526 showSelectedMessage(tocLine); 1527 } 1528 if (theState.selected <= 0) { 1529 disableMsgBtns(); 1530 if (theToc.total == 0) { 1531 btns.file.disable(); 1532 document.getElementById("moveAllBtn").disabled = true; 1533 document.getElementById("copyAllBtn").disabled = true; 1534 closeDlog("moveCopy"); 1535 btns.mark.disable(); 1536 } 1537 } else { 1538 btns.resend.enable(); 1539 btns.forward.enable(); 1540 btns.replyAll.enable(); 1541 btns.reply.enable(); 1542 if (!readOnly) btns.drop.enable(); 1543 if (btns.trash.deleting) { 1544 btns.trash.enabled = false; 1545 btns.trash.deleting = false; 1546 } 1547 if (!readOnly) btns.trash.enable(); 1548 document.getElementById("deleteBtn").disabled = false; 1549 document.getElementById("moveBtn").disabled = false; 1550 document.getElementById("copyBtn").disabled = false; 1551 document.getElementById("markUnreadBtn").disabled = false; 1552 } 1553 if (theState.query.folder == "Trash") { 1554 btns.drop.disable(); 1555 btns.trash.element.innerHTML = 1556 '<a href="#" title="Permanently delete this message' + 1557 ' (or all matching messages)" ' + 1558 'onClick="return toggle(\'delete\')">Delete...</a>'; 1559 btns.trash.enabled = true; 1560 btns.trash.deleting = true; 1561 } 1562 if (theState.selOffset > 0 || 1563 (theState.selected <= 0 && theToc.total > 0)) { 1564 if (btns.next.scanning) { 1565 btns.next.enabled = false; 1566 btns.next.scanning = false; 1567 } 1568 btns.next.enable(); 1569 } else if (theState.totalUnread > 0 || true) { 1570 if (!btns.next.enabled || !btns.next.scanning) { 1571 // Patch in the "scan" HTML 1572 btns.next.element.innerHTML = btns.scan.active; 1573 btns.next.enabled = true; 1574 btns.next.scanning = true; 1575 } 1576 } else { 1577 btns.next.disable(); 1578 btns.next.scanning = false; 1579 } 1580 if (theState.totalUnread > 0 || true) { 1581 btns.scan.enable(); 1582 } else { 1583 btns.scan.disable(); 1584 } 1585 return false; 1586 } 1587 1588 function showAltMsg(id, startAt, chosenPartNo, full) { 1589 // Show an attached message, or non-default part of main message 1590 showContents(readingContents); 1591 pachyGet((full ? "getFullMsg" : "getMsg"), null, null, 1592 chosenPartNo, startAt); 1593 return false; 1594 } 1595 1596 function showHeader(id, startAt) { 1597 // Show a message's header 1598 showContents(readingContents); 1599 pachyGet("getRawHeader", null, null, null, startAt); 1600 return false; 1601 } 1602 1603 function showRawMessage(id, startAt) { 1604 // Show re-assembled raw message 1605 showContents(readingContents); 1606 pachyGet("getRawMessage", null, null, null, startAt); 1607 return false; 1608 } 1609 1610 function getBulkData(doc) { 1611 // Retrieve bulk data encoded in "content" tags 1612 // Overcomes a Firefox 1.5 bug restricting text runs to 4096 chars 1613 var content = ""; 1614 var fragments = doc.getElementsByTagName("content"); 1615 for (var i = 0; i < fragments.length; i++) { 1616 var child = fragments[i].firstChild; 1617 if (child) content += child.nodeValue; 1618 } 1619 return content; 1620 } 1621 1622 function getPartClick(url) { 1623 // return HTL fragment for handling a click on a getPart "A" element 1624 // 1625 return "href=\"" + url + "\""; 1626 } 1627 1628 function receiveMsg(thisReq, doc) { 1629 // Receive a message response from the server, cache and/or display it 1630 var msg = new Object(); 1631 msg.id = parseInt(doc.getAttribute("id")); 1632 msg.folders = decodeURIComponent(doc.getAttribute("folders")); 1633 msg.startAt = parseInt(doc.getAttribute("startAt")); 1634 msg.selectedPart = doc.getAttribute("selectedPart"); 1635 msg.header = doc.getAttribute("header"); 1636 msg.bodyPartID = parseInt(doc.getAttribute("bodyPartID")); 1637 msg.bodyPartURL = decodeURIComponent(doc.getAttribute("bodyPartURL")); 1638 msg.fullLength = parseInt(doc.getAttribute("fullLength")); 1639 msg.sentLength = parseInt(doc.getAttribute("sentLength")); 1640 msg.msgfrom = decodeURIComponent(doc.getAttribute("from")); 1641 msg.subject = decodeURIComponent(doc.getAttribute("subject")); 1642 msg.date = decodeURIComponent(doc.getAttribute("date")); 1643 msg.msgto = decodeURIComponent(doc.getAttribute("to")); 1644 msg.msgcc = decodeURIComponent(doc.getAttribute("cc")); 1645 msg.content = getBulkData(doc); 1646 msg.parts = doc.getElementsByTagName("part"); 1647 if (thisReq.msgCache == msgCache && 1648 (msg.startAt != 0 || 1649 msg.selectedPart != "" || 1650 !msgCache.read(""+msg.id) || 1651 thisReq.op == "getFullMsg")) { 1652 var tempHdr = ""; 1653 if (msg.startAt != 0) { 1654 tempHdr += "<b>Sub-message: </b>\#" + msg.startAt + " of " + 1655 "<a href=\"#\" onClick=\"return showSelectedMessage()\" " + 1656 "title=\"Show message #" + msg.id + "\">" + 1657 "message #" + msg.id + "</a>" + 1658 (msg.selectedPart != "" ? 1659 ", showing part #"+msg.selectedPart : 1660 "") + 1661 "<br>"; 1662 } 1663 tempHdr += "<b>From: &nbsp; &nbsp; </b>" + msg.msgfrom + 1664 "<br><b>Subject: &nbsp;</b>" + msg.subject + 1665 "<br><b>Date: &nbsp; &nbsp; </b>" + msg.date + 1666 "<br><b>To: &nbsp; &nbsp; &nbsp; </b>" + msg.msgto + 1667 "<br><b>cc: &nbsp; &nbsp; &nbsp; </b>" + msg.msgcc; 1668 var alternatives = new Array(); 1669 var attachments = new Array(); 1670 for (var i = 0; i < msg.parts.length; i++) { 1671 var part = msg.parts[i]; 1672 var partNo = part.getAttribute("partNo"); 1673 var partID = part.getAttribute("partID"); 1674 var type = htmlspecials(part.getAttribute("type")); 1675 // var name = htmlspecials(decodeURIComponent( 1676 // part.getAttribute("name"))); 1677 // The above fails if the unescaped text isn't valid UTF-8 1678 // The following is more robust, if a bit wierd in its results 1679 var name = htmlspecials(unescape( 1680 part.getAttribute("name"))); 1681 var length = part.getAttribute("length"); 1682 var partClass = part.getAttribute("class"); 1683 var partURL = htmlspecials(decodeURIComponent( 1684 part.getAttribute("url"))); 1685 if (partClass != "Unused") { 1686 if (type == "message/rfc822") { 1687 attachments[attachments.length] = "<a href=\"#\" " + 1688 "onClick=\"return showAltMsg(" + msg.id + "," + 1689 partID + ", '')\" " + 1690 "title=\"Show attached message\">" + 1691 "Sub-message #" + partID + "</a>"; 1692 } else if (partClass == "Alternative") { 1693 alternatives[alternatives.length] = "<a href=\"#\" " + 1694 "onClick=\"return showAltMsg(" + msg.id + "," + 1695 msg.startAt + ", '" + partNo + "')\" " + 1696 "title=\"Show alternative version\">" + 1697 type + "</a>"; 1698 } else { 1699 var onPartClick = getPartClick(partURL); 1700 attachments[attachments.length] = "<a " + onPartClick + 1701 " title=\"Open attachment\">" + 1702 name + (name == "" ? "" : " ") + "(" + 1703 type + ", " + length + " Bytes)</a>"; 1704 } 1705 } 1706 } 1707 var viewPad = ""; 1708 viewPad = "&nbsp; &nbsp; "; 1709 alternatives[alternatives.length] = "<a href=\"#\" " + 1710 "onClick=\"return showHeader(" + msg.id + "," + 1711 msg.startAt + ")\" title=\"Show raw message header\"" + 1712 ">raw header</a>"; 1713 alternatives[alternatives.length] = "<a href=\"#\" " + 1714 "onClick=\"return showRawMessage(" + msg.id + "," + 1715 msg.startAt + ")\" title=\"Show raw message\"" + 1716 ">raw message</a>"; 1717 if (msg.bodyPartID > 0) { 1718 var bodyPartURL = htmlspecials(msg.bodyPartURL) 1719 var onPartClick = getPartClick(bodyPartURL); 1720 alternatives[alternatives.length] = "<a " + onPartClick + 1721 " title=\"Open message body as web page\">web page</a>"; 1722 } 1723 alternatives[alternatives.length] = "<a href=\"" + 1724 "detached.html?user=" + htmlspecials(user) + 1725 "&amp;m=" + msg.id + "\">" + 1726 "detach</a>"; 1727 if (alternatives.length > 0) { 1728 tempHdr += "<br><b>View: " + viewPad + "</b>"; 1729 for (var i = 0; i < alternatives.length; i++) { 1730 if (i > 0) tempHdr += " &hellip; "; 1731 tempHdr += alternatives[i]; 1732 } 1733 } 1734 for (var i = 0; i < attachments.length; i++) { 1735 tempHdr += "<br><b>Attached: </b>"; 1736 tempHdr += attachments[i]; 1737 } 1738 var tempMsg = new Object(); 1739 tempMsg.id = msg.id; 1740 tempMsg.header = tempHdr; 1741 tempMsg.folders = msg.folders; 1742 tempMsg.body = msg.content; 1743 tempMsg.sentLength = msg.sentLength; 1744 tempMsg.fullLength = msg.fullLength; 1745 tempMsg.truncated = (msg.sentLength < msg.fullLength); 1746 tempMsg.startAt = msg.startAt; 1747 tempMsg.selectedPart = msg.selectedPart; 1748 if (msg.startAt == 0) msgCache.write(""+msg.id, tempMsg); 1749 if (msg.id == theState.selected) showMessageContents(tempMsg); 1750 } 1751 } 1752 1753 function doNext() { 1754 if (theToc.total >= 0) { 1755 var thisMsgOffset = (theState.selected > 0 ? theState.selOffset : 1756 theState.offset + theState.tocPageSize); 1757 if (thisMsgOffset > theToc.total) thisMsgOffset = theToc.total; 1758 var nextMsgOffset = thisMsgOffset - 1; 1759 if (nextMsgOffset >= 0) { 1760 var nextLine = theToc.lines[nextMsgOffset]; 1761 if (!nextLine) { 1762 alert("TOC not yet arrived: try again"); 1763 } else { 1764 if (theState.selOffset + theState.tocPageSize > 1765 theToc.total) scrollTocTo(theState.offset); 1766 show(nextLine.id, nextMsgOffset); 1767 } 1768 } 1769 } 1770 return false; 1771 } 1772 1773 function doPrev() { 1774 if (theToc.total >= 0) { 1775 var thisMsgOffset = (theState.selected > 0 ? theState.selOffset : 1776 theState.offset - 1); 1777 if (thisMsgOffset > theToc.total) thisMsgOffset = theToc.total; 1778 var nextMsgOffset = thisMsgOffset + 1; 1779 if (nextMsgOffset < theToc.total) { 1780 var nextLine = theToc.lines[nextMsgOffset]; 1781 if (!nextLine) { 1782 alert("TOC not yet arrived: try again"); 1783 } else { 1784 if (theState.selOffset + theState.tocPageSize > 1785 theToc.total) scrollTocTo(theState.offset); 1786 show(nextLine.id, nextMsgOffset); 1787 } 1788 } 1789 } 1790 return false; 1791 } 1792 1793 1794 // 1795 // Label updates 1796 // 1797 1798 function doDelete() { 1799 // Permanently delete selected message 1800 if (theState.selected <= 0) { 1801 reportError("No message selected"); 1802 } else { 1803 pachyGet("delete"); 1804 // Remove from our cached TOC, and repaint 1805 theToc.lines.splice(theState.selOffset, 1); 1806 theToc.total--; 1807 if (theToc.total == 0) { 1808 show(0, 0); 1809 } else { 1810 if (theState.selOffset == 0) theState.selOffset = 1; 1811 doNext(); 1812 } 1813 } 1814 return false; 1815 } 1816 1817 function doDeleteAll() { 1818 // Delete all matching messages 1819 if (theToc.total >= 0) { 1820 pachyGet("deleteAll"); 1821 theToc.lines = new Array(); 1822 theToc.total = 0; 1823 show(0,0); 1824 } 1825 } 1826 1827 function confirmDelete() { 1828 // Ask for confirmation of "delete selected" operation 1829 toggle("delete"); 1830 if (theState.selected <= 0) { 1831 reportError("No message selected"); 1832 } else { 1833 askConfirm("Permanently delete the" + 1834 " selected message? This cannot be undone.", 1835 doDelete); 1836 } 1837 return false; 1838 } 1839 1840 function confirmDeleteAll() { 1841 // Ask for confirmation of "delete all" operation 1842 toggle("delete"); 1843 askConfirm("Permanently delete all " + theToc.total + 1844 " matching messages? This cannot be undone.", 1845 doDeleteAll); 1846 return false; 1847 } 1848 1849 function doMoveCopy(moveOrCopy, dest) { 1850 // Move or copy to dest, or selected folder in moveCopy dlog 1851 if (!dest) { 1852 dest = getOption("moveFolder"); 1853 if (dest && dest != "-") theState.oldFolder = dest; 1854 if (!getCheckbox("moveCopyStayOpen")) toggle("moveCopy"); 1855 } 1856 if (theState.selected <= 0) { 1857 reportError("No message selected"); 1858 } else { 1859 if (!dest || dest == "-") { 1860 reportError("No destination folder selected"); 1861 } else if (dest == theState.query.folder) { 1862 reportError("You can't move or copy a message to the " + 1863 "folder you're currently looking at"); 1864 } else { 1865 pachyGet((moveOrCopy ? "move" : "copy"), null, null, dest); 1866 if (moveOrCopy) { 1867 // Remove from our cached TOC, and repaint. 1868 // The repaint happens inside "show", called below 1869 theToc.lines.splice(theState.selOffset, 1); 1870 theToc.total--; 1871 } 1872 var msgContent = msgCache.read(""+theState.selected); 1873 if (msgContent) { 1874 // patch up the "folders" field of our message cache 1875 var folders = msgContent.folders.split(", "); 1876 if (moveOrCopy) { 1877 for (var i = 0; i < folders.length; i++) { 1878 if (folders[i] == theState.query.folder) { 1879 folders.splice(i, 1); 1880 break; 1881 } 1882 } 1883 } 1884 var j; 1885 for (j = 0; j < folders.length; j++) { 1886 if (folders[j] == dest) break; 1887 } 1888 folders[j] = dest; 1889 msgContent.folders = folders.join(", "); 1890 } 1891 if (theToc.total == 0) { 1892 show(0, 0); 1893 } else { 1894 // Move to next, or redisplay selected if at bottom of TOC. 1895 // Always redisplay, to repaint TOC and changed "folders". 1896 if (theState.selOffset == 0) theState.selOffset = 1; 1897 doNext(); 1898 } 1899 } 1900 } 1901 return false; 1902 } 1903 1904 function doMoveAll() { 1905 // Move all to selected folder in moveCopy dlog 1906 if (theToc.total >= 0) { 1907 var dest = getOption("moveFolder"); 1908 if (!dest || dest == "-") { 1909 reportError("No destination folder selected"); 1910 } else { 1911 pachyGet("moveAll", null, null, dest); 1912 theToc.lines = new Array(); 1913 theToc.total = 0; 1914 msgCache = new Cache(50); // flush "folders" fields 1915 show(0,0); 1916 } 1917 } 1918 } 1919 1920 function doCopyAll() { 1921 // Copy all to selected folder in moveCopy dlog 1922 if (theToc.total >= 0) { 1923 var dest = getOption("moveFolder"); 1924 if (!dest || dest == "-") { 1925 reportError("No destination folder selected"); 1926 } else { 1927 var dest = getOption("moveFolder"); 1928 pachyGet("copyAll", null, null, dest); 1929 msgCache = new Cache(50); // flush "folders" fields 1930 jumpTo(theState.query); // ugly, but synchronizes with labelOps 1931 } 1932 } 1933 } 1934 1935 function confirmMoveCopy(moveOrCopy) { 1936 // Ask for confirmation of move or copy all operation 1937 var dest = getOption("moveFolder"); 1938 toggle("moveCopy"); 1939 if (!dest || dest == "-") { 1940 reportError("No destination folder selected"); 1941 } else { 1942 askConfirm("Really " + (moveOrCopy ? "move" : "copy") + 1943 " all " + theToc.total + 1944 " matching messages to \"" + dest + 1945 "\"?", 1946 (moveOrCopy ? doMoveAll : doCopyAll)); 1947 } 1948 return false; 1949 } 1950 1951 function doMark(unread, all) { 1952 // Mark selected/all as unread/read 1953 toggle("mark"); 1954 if (theToc.total >= 0) { 1955 if (!all && theState.selected <= 0) { 1956 reportError("No message selected"); 1957 } else { 1958 pachyGet((unread ? (all ? "markAllUnread" : "markUnread") : 1959 (all ? "markAllRead" : "markRead"))); 1960 if (all) { 1961 jumpTo(theState.query); // to recompute unread count 1962 // ... which also calls abandonToc, so we needn't update it 1963 } else { 1964 theState.totalUnread += (unread ? 1 : 0) - 1965 (theToc.lines[theState.selOffset].unread ? 1 : 0); 1966 theToc.lines[theState.selOffset].unread = unread; 1967 if (theState.selOffset > 0) doNext(); else show(0,0); 1968 showToc(); 1969 } 1970 setTitle(); 1971 } 1972 } 1973 return false; 1974 } 1975 1976 1977 // 1978 // Contacts Screen 1979 // 1980 1981 function showContactsScreen(keeping) { 1982 showOtherScreen("contactsScreen"); 1983 contactsChanged = false; 1984 contactsKeeping = keeping; 1985 } 1986 1987 function ensureContactsRead() { 1988 // Deferred reading of the contacts list from the server 1989 // 1990 if (!theContacts) { 1991 document.getElementById("contacts").innerHTML = "Reading ..."; 1992 document.getElementById("recipientList").innerHTML = "Reading ..."; 1993 pachyGet2("getContacts", gotContacts); 1994 } 1995 } 1996 1997 function gotContacts(doc) { 1998 // Completion of "getContacts" 1999 // 2000 if (checkResult(doc, "contacts")) { 2001 theContacts = new Array(); 2002 var contacts = doc.getElementsByTagName("contact"); 2003 for (var i = 0; i < contacts.length; i++) { 2004 var contact = contacts[i]; 2005 var local = new Contact( 2006 contact.getAttribute("id"), 2007 decodeURIComponent(contact.getAttribute("first")), 2008 decodeURIComponent(contact.getAttribute("last")), 2009 decodeURIComponent(contact.getAttribute("nickname")), 2010 decodeURIComponent(contact.getAttribute("email")), 2011 decodeURIComponent(contact.getAttribute("address")), 2012 decodeURIComponent(contact.getAttribute("home")), 2013 decodeURIComponent(contact.getAttribute("work")), 2014 decodeURIComponent(contact.getAttribute("mobile"))) 2015 theContacts[local.id] = local; 2016 } 2017 renderContacts(); 2018 } 2019 } 2020 2021 function doContacts() { 2022 showContactsScreen(false); 2023 document.getElementById("contactsNew").style.visibility = "visible"; 2024 document.getElementById("contactsDone").style.visibility = "visible"; 2025 document.getElementById("contacts").style.display = "block"; 2026 ensureContactsRead(); 2027 return false; 2028 } 2029 2030 function contactsDone() { 2031 if (contactsChanged) { 2032 contactsChanged = false; 2033 msgCache = new Cache(50); 2034 } 2035 showMainScreen("contactsScreen"); 2036 return false; 2037 } 2038 2039 function renderContacts() { 2040 // Render the contact list: once for the (non-iPhone) contacts screen 2041 // (constructed in "temp"), and once for the draft screen (constructed 2042 // in "temp2"). 2043 // 2044 var tempContacts = theContacts.slice(0).sort(theState.sorter); 2045 var filter = theState.draftFilter; 2046 var re = new RegExp((filter == "" ? ".*" : filter), "i"); 2047 2048 var temp = "<table style=\"width: 100%\" cellspacing=0>" + 2049 "<tr><td>Sort by: " + "<a href=\"#\" onClick=" + 2050 "\"return sortContacts(sortByFirst)\">first</a> ... " + 2051 "<a href=\"#\" " + 2052 "onClick=\"return sortContacts(sortByLast)\">last</a>" + 2053 "</td><td>Email Address</td></tr>" + 2054 "<tr><td></td><td>Postal Address</td></tr>" + 2055 "<tr><td></td><td>Home Phone</td>" + 2056 "<td>Work Phone</td>" + 2057 "<td>Mobile Phone</td></tr>"; 2058 var temp2 = ""; 2059 var lines = 5; 2060 var first2 = true; 2061 2062 for (var id in tempContacts) { 2063 if (lines == 5) { 2064 temp += "<tr><td colspan=5><hr></td></tr>"; 2065 lines = 0; 2066 } 2067 var contact = tempContacts[id]; 2068 if (contact) { 2069 var cName = contact.first + " " + contact.last; 2070 var cEmail = " <" + contact.email + ">"; 2071 temp += "<tr><td class=nowrap>" + 2072 htmlspecials(cName + 2073 (contact.nickname == "" ? "" : " (" + 2074 contact.nickname + ")")) + 2075 "</td><td class=nowrap colspan=3>" + 2076 htmlspecials(contact.email) + 2077 "</td><td style=\"text-align: right\"><a href=\"#\" " + 2078 "onClick=\"return doContactEditPopup('" + 2079 contact.id + "')\">" + 2080 "Edit</a>&nbsp;&nbsp;&nbsp;<a href=\"#\" " + 2081 "onClick=\"return confirmContactDelete('" + 2082 contact.id + "')\">" + 2083 "Delete</a></td></tr>" + 2084 "<tr><td></td><td class=nowrap colspan=3>" + 2085 htmlspecials(contact.address) + 2086 "</td></tr><tr><td></td><td>" + 2087 htmlspecials(contact.home) + "</td><td>" + 2088 htmlspecials(contact.work) + "</td><td>" + 2089 htmlspecials(contact.mobile) + "</td></tr>"; 2090 lines++; 2091 if (filter == "" || 2092 contact.first.match(re) || 2093 contact.last.match(re) || 2094 contact.nickname.match(re) || 2095 contact.email.match(re)) { 2096 if (!first2) temp2 += "<hr>"; 2097 temp2 += "<a href=\"#\" onclick=\"return addRecipient('" + 2098 contact.id + "')\">" + htmlspecials(cName) + "<br>" + 2099 htmlspecials(cEmail) + "</a>"; 2100 first2 = false; 2101 } 2102 } 2103 } 2104 temp += "</table>"; 2105 var elt = document.getElementById("contacts"); 2106 if (elt) elt.innerHTML = temp; // not on iPhone 2107 document.getElementById("recipientList").innerHTML = temp2; 2108 return false; 2109 } 2110 2111 function sortByFirst(a, b) { 2112 if (!a) return b; 2113 if (!b) return a; 2114 return caseSort(a.first + " " + a.last + " " + a.nickname, 2115 b.first + " " + b.last + " " + b.nickname); 2116 } 2117 2118 function sortByLast(a, b) { 2119 if (!a) return b; 2120 if (!b) return a; 2121 return caseSort(a.last + " " + a.first + " " + a.nickname, 2122 b.last + " " + b.first + " " + b.nickname); 2123 } 2124 2125 function sortContacts(sorter) { 2126 // Sort and re-render the contacts array 2127 // 2128 theState.sorter = sorter; 2129 if (theContacts) renderContacts(); 2130 return false; 2131 } 2132 2133 function doContactCreatePopup() { 2134 toggle(null); 2135 document.getElementById("ecPrompt").innerHTML = 2136 "Create a New Contact Record"; 2137 document.getElementById("ecId").value = "0"; 2138 document.getElementById("ecFirst").value = ""; 2139 document.getElementById("ecLast").value = ""; 2140 document.getElementById("ecNickname").value = ""; 2141 document.getElementById("ecEmail").value = ""; 2142 document.getElementById("ecAddress").value = ""; 2143 document.getElementById("ecHome").value = ""; 2144 document.getElementById("ecWork").value = ""; 2145 document.getElementById("ecMobile").value = ""; 2146 toggle("editContact"); 2147 return false; 2148 } 2149 2150 function keepContact(email, person) { 2151 showContactsScreen(true); 2152 document.getElementById("contactsNew").style.visibility = "hidden"; 2153 document.getElementById("contactsDone").style.visibility = "hidden"; 2154 document.getElementById("contacts").style.display = "none"; 2155 doContactCreatePopup(); 2156 document.getElementById("ecId").value = "0"; 2157 document.getElementById("ecFirst").value = decodeURIComponent(person); 2158 document.getElementById("ecEmail").value = decodeURIComponent(email); 2159 return false; 2160 } 2161 2162 function doContactEditPopup(id) { 2163 toggle(null); 2164 var contact = theContacts[id]; 2165 document.getElementById("ecPrompt").innerHTML = 2166 "Edit Contact Record #" + id; 2167 document.getElementById("ecId").value = id; 2168 document.getElementById("ecFirst").value = contact.first; 2169 document.getElementById("ecLast").value = contact.last; 2170 document.getElementById("ecNickname").value = contact.nickname; 2171 document.getElementById("ecEmail").value = contact.email; 2172 document.getElementById("ecAddress").value = contact.address; 2173 document.getElementById("ecHome").value = contact.home; 2174 document.getElementById("ecWork").value = contact.work; 2175 document.getElementById("ecMobile").value = contact.mobile; 2176 toggle("editContact"); 2177 document.getElementById("ecFirst").focus(); 2178 return false; 2179 } 2180 2181 function doContactSave() { 2182 var contact = new Contact( 2183 document.getElementById("ecId").value, 2184 document.getElementById("ecFirst").value, 2185 document.getElementById("ecLast").value, 2186 document.getElementById("ecNickname").value, 2187 document.getElementById("ecEmail").value, 2188 document.getElementById("ecAddress").value, 2189 document.getElementById("ecHome").value, 2190 document.getElementById("ecWork").value, 2191 document.getElementById("ecMobile").value); 2192 var dest = contact.id + "\n" + 2193 contact.first + "\n" + 2194 contact.last + "\n" + 2195 contact.nickname + "\n" + 2196 contact.email + "\n" + 2197 contact.address + "\n" + 2198 contact.home + "\n" + 2199 contact.work + "\n" + 2200 contact.mobile; 2201 toggle("editContact"); 2202 if (contact.id != "0") theContacts[contact.id] = contact; 2203 pachyGet("saveContact", null, null, dest); 2204 return false; 2205 } 2206 2207 function doContactCancel() { 2208 // Cancel out of the create/edit dialog 2209 toggle("editContact"); 2210 if (contactsKeeping) contactsDone(); 2211 return false; 2212 } 2213 2214 function doContactDelete(id) { 2215 pachyGet("deleteContact", null, null, id); 2216 } 2217 2218 function confirmContactDelete(id) { 2219 // Get confirmation, then delete contact record. 2220 // Uses function closure to transport the "id" parameter 2221 askConfirm("Really delete contact #" + id + "?", 2222 function() { doContactDelete(id); }); 2223 return false; 2224 } 2225 2226 2227 // 2228 // Message composition operations 2229 // 2230 2231 function setupDraftFrom(long) { 2232 // Set the draftFrom selector using theAccounts 2233 // 2234 // "long" uses longer selector prompts, for non-iPhone 2235 // 2236 truncateOptions('draftFrom', 0); 2237 elt = document.getElementById('draftFrom'); 2238 var dup = new Array(); 2239 for (var id in theAccounts) { 2240 var acct = theAccounts[id]; 2241 var value = acct.person + " <" + acct.msgfrom + ">"; 2242 // value must match values used in pachylib.php/buildDraft 2243 var prompt = (long ? value : acct.msgfrom); 2244 if (!dup[prompt]) { 2245 appendOption(elt, prompt, value); 2246 dup[prompt] = true; 2247 } 2248 } 2249 } 2250 2251 function curDraftId() { 2252 // Return just the draftId of the current, if any, draft 2253 // 2254 return document.getElementById('draftId').value; 2255 } 2256 2257 function curDraft() { 2258 // Return an object containing the current draft state, not including 2259 // attachments. 2260 // 2261 return { 2262 id: curDraftId(), 2263 msgfrom: getOption('draftFrom'), 2264 msgto: document.getElementById('draftTo').value, 2265 msgcc: document.getElementById('draftCc').value, 2266 subject: document.getElementById('draftSubject').value, 2267 content: document.getElementById('draftBody').value 2268 }; 2269 } 2270 2271 function installAttachments(draft) { 2272 // Install attachment list from given draft object into the UI 2273 // 2274 var elt = document.getElementById('draftAtt'); 2275 if (elt) { 2276 var attachments = draft.attachments; 2277 var attTxt = ""; 2278 if (draft.attachMsg != "none") { 2279 attTxt += "Message #" + draft.attachMsg; 2280 } 2281 for (var i = 0; i < attachments.length; i++) { 2282 var att = attachments[i]; 2283 if (attTxt != "") attTxt += "<br>\n"; 2284 attTxt += htmlspecials(att.name) + 2285 " (" + att.length + ")" + 2286 " <a href=\"#\" onclick=\"return attachDelete(" + 2287 draft.id + ", " + att.part + ")\"" + 2288 " title=\"Remove this attachment\">remove</a>"; 2289 } 2290 if (attTxt == "") attTxt = "none"; 2291 elt.innerHTML = attTxt; 2292 } 2293 } 2294 2295 function installDraft(draft) { 2296 // Install given draft object into the UI, including attachments 2297 // 2298 var title = document.getElementById('draftTitle'); 2299 if (title) title.innerHTML = "&nbsp;Draft message #" + draft.id; 2300 document.getElementById('draftId').value = draft.id; 2301 setOption('draftFrom', draft.msgfrom); 2302 document.getElementById('draftTo').value = draft.msgto; 2303 document.getElementById('draftCc').value = draft.msgcc; 2304 document.getElementById('draftSubject').value = draft.subject; 2305 document.getElementById('draftBody').value = draft.content; 2306 document.getElementById('draftSavedAt').innerHTML = "&nbsp;"; 2307 installAttachments(draft); 2308 } 2309 2310 function receiveAttachments(doc) { 2311 // Receive attachment list from server, and return as an object 2312 // 2313 var attachments = new Array(); 2314 var fragments = doc.getElementsByTagName("att"); 2315 for (var i = 0; i < fragments.length; i++) { 2316 var att = fragments[i]; 2317 attachments[attachments.length] = { 2318 part: att.getAttribute("part"), 2319 length: att.getAttribute("length"), 2320 name: att.getAttribute("name") 2321 }; 2322 } 2323 return { 2324 id: parseInt(doc.getAttribute("id")), 2325 attachMsg: decodeURIComponent(doc.getAttribute("attachMsg")), 2326 attachments: attachments 2327 }; 2328 } 2329 2330 function receiveDraft(doc) { 2331 // Receive draft contents from server, and return as an object 2332 // 2333 var draft = receiveAttachments(doc); 2334 draft.msgfrom = decodeURIComponent(doc.getAttribute("from")); 2335 draft.msgto = decodeURIComponent(doc.getAttribute("to")); 2336 draft.msgcc = decodeURIComponent(doc.getAttribute("cc")); 2337 draft.subject = decodeURIComponent(doc.getAttribute("subject")); 2338 draft.content = getBulkData(doc); 2339 return draft; 2340 } 2341 2342 function openDraft(op, draftId) { 2343 // Commence opening a draft, new or old 2344 // 2345 installDraft({ 2346 id: 0, 2347 msgFrom: curDraft().msgfrom, 2348 msgto: "", 2349 msgcc: "", 2350 subject: "", 2351 attachments: new Array(), 2352 content: "" 2353 }); 2354 showOtherScreen("draftScreen"); 2355 showComposingScreen(draftId ? "Reading draft ..." : 2356 "Composing draft ..."); 2357 pachyGet2(op, gotDraft, null, null, draftId); 2358 ensureContactsRead(); 2359 } 2360 2361 function showDraftScreen() { 2362 // Switch to the draft screen, from modal state 2363 // 2364 progressDone(); 2365 } 2366 2367 function showComposingScreen(prompt) { 2368 // Move to modal state from the draft screen 2369 // 2370 reportProgress(prompt); 2371 } 2372 2373 function returnFromDraft() { 2374 // Exit from the draft screen (which might be modal at the time) 2375 // 2376 progressDone(); 2377 showMainScreen("draftScreen"); 2378 if (theState.query.folder == 'Unsent') { 2379 jumpTo(theState.query); // re-evaluate query after send/discard 2380 } 2381 return false; 2382 } 2383 2384 var lastSavedDraft = null; 2385 // The "draft" object that we last sent to the server for saving, 2386 // or the original one we loaded from the server. ".completed" is true 2387 // iff we know the server has (or had) this content. "savedAt" is 2388 // the time we started the save (or completed the load). 2389 2390 function reportSavedTime(why) { 2391 // Display the time of a successful load or save 2392 document.getElementById('draftSavedAt').innerHTML = 2393 why + " at " + lastSavedDraft.savedAt.toLocaleTimeString().replace( 2394 /^([0-9]*:[0-9]*:[0-9]*).*$/, '$1'); 2395 lastSavedDraft.completed = true; 2396 } 2397 2398 function gotDraft(doc) { 2399 // Completion of openDraft: compose/reply/replyAll/forward/reopenDraft 2400 // 2401 if (checkResult(doc, "draft")) { 2402 var draft = receiveDraft(doc); 2403 if (draft.id == 0) { 2404 alert("Draft not found"); 2405 returnFromDraft(); 2406 } else { 2407 installDraft(draft); 2408 showDraftScreen(); 2409 lastSavedDraft = draft; 2410 lastSavedDraft.savedAt = new Date(); 2411 reportSavedTime("Loaded"); 2412 startAutoSave(); 2413 } 2414 } else { 2415 returnFromDraft(); 2416 } 2417 } 2418 2419 function doComposition(op) { 2420 // Compose/Reply/Forward buttons 2421 // 2422 if (op != "compose" && theState.selected <= 0) { 2423 reportError("No message selected"); 2424 } else { 2425 openDraft(op); 2426 } 2427 return false; 2428 } 2429 2430 function doReopenDraft(draftId) { 2431 // Resume editing of unsent draft 2432 // 2433 openDraft("reopenDraft", draftId); 2434 return false; 2435 } 2436 2437 function doResend() { 2438 // Implement "resend" 2439 // 2440 var dest = document.getElementById("resendTo").value; 2441 toggle("resend"); 2442 if (theState.selected <= 0) { 2443 reportError("No message selected"); 2444 } else if (dest == "") { 2445 reportError("No recipients"); 2446 } else { 2447 pachyGet("resend", null, null, dest); 2448 } 2449 return false; 2450 } 2451 2452 var findPoller = null; 2453 var findFlip = ""; 2454 2455 function draftContactsFind() { 2456 // Search with the draft contacts list 2457 // 2458 var filter = 2459 document.getElementById('draftContactsFilter').value; 2460 if (filter != theState.draftFilter) { 2461 theState.draftFilter = filter; 2462 if (theContacts) renderContacts(); 2463 } 2464 var elt = document.getElementById("draftContactsFlip"); 2465 if (elt) elt.innerHTML = (findFlip = (findFlip == "" ? "." : "")); 2466 return false; 2467 } 2468 2469 function draftContactsAll() { 2470 // Clear the search filter 2471 // 2472 document.getElementById('draftContactsFilter').value = ""; 2473 draftContactsFind(); 2474 document.getElementById('draftContactsFilter').focus(); 2475 return false; 2476 } 2477 2478 function draftFindFocus() { 2479 findPoller = setInterval(draftContactsFind, 250); 2480 } 2481 2482 function draftFindBlur() { 2483 if (findPoller) clearInterval(findPoller); 2484 findPoller = null; 2485 } 2486 2487 function uniqueContact(id, value) { 2488 // Return true if given value uniquely identifies the contact "id" 2489 // 2490 if (!value) return false; 2491 value = value.trim().toLowerCase(); 2492 if(value == "") return false; 2493 for (var other in theContacts) { 2494 var otherC = theContacts[other]; 2495 if (other != id && otherC) { 2496 var otherN = otherC.nickname.trim().toLowerCase(); 2497 var otherF = otherC.first.trim().toLowerCase(); 2498 var otherL = otherC.last.trim().toLowerCase(); 2499 if (otherN == value) return false; 2500 if (otherF == value) return false; 2501 if (otherL == value) return false; 2502 if (otherF + " " + otherL == value) return false; 2503 if (otherF + "_" + otherL == value) return false; 2504 } 2505 } 2506 return true; 2507 } 2508 2509 function addRecipientDone() { 2510 // Cleanup after addRecipient 2511 // 2512 document.getElementById('draftContactsFilter').focus(); 2513 } 2514 2515 function addRecipient(id) { 2516 // Add a recipient from the contact list. 2517 // 2518 // Tries to use the simplest unambiguous name. 2519 // 2520 var contact = theContacts[id]; 2521 if (contact) { 2522 var firstLast = contact.first + " " + contact.last; 2523 var addr = (uniqueContact(id, contact.nickname) ? contact.nickname : 2524 (uniqueContact(id, contact.first) ? contact.first : 2525 (uniqueContact(id, contact.last) ? contact.last : 2526 (uniqueContact(id, firstLast) ? firstLast : 2527 contact.first + " " + contact.last + " <" + contact.email + 2528 ">")))); 2529 var draftTo = document.getElementById('draftTo').value; 2530 document.getElementById('draftTo').value = draftTo + 2531 (draftTo == "" ? "" : ", ") + addr; 2532 saveDraft(); 2533 } 2534 addRecipientDone(); 2535 return false; 2536 } 2537 2538 function attach() { 2539 // Open the "attach" pop-up dialog, modally 2540 // 2541 showModalizer("modalizer"); // also takes down pop-ups 2542 toggle("attach"); // appears above the modalizer 2543 return false; 2544 } 2545 2546 function doAttach() { 2547 // onSubmit handler for the file attachment form. 2548 // Computes requisite extra fields. 2549 // 2550 document.getElementById("attachUser").value = user; 2551 document.getElementById("attachFrame").onload = attachDone; 2552 showComposingScreen("Uploading the file ..."); 2553 return true; 2554 } 2555 2556 function attachDone() { 2557 // onload handler for the attachment upload iframe 2558 // 2559 document.getElementById("attachFrame").onload = function() { }; 2560 showComposingScreen("Enumerating the attachments ..."); 2561 pachyGet2("reopenDraft", gotAttachEnum, null, null, curDraftId()); 2562 } 2563 2564 function gotAttachEnum(doc) { 2565 // Completion of attachment enumeration after adding/deleting. 2566 // 2567 // Unlike gotDraft, errors leave us on the draft screen, with a 2568 // potentially out-of-date list of attachments. 2569 // 2570 if (checkResult(doc, "draft")) { 2571 var draft = receiveAttachments(doc); 2572 if (draft.id == 0) { 2573 alert("Draft not found"); 2574 } else { 2575 installAttachments(draft); 2576 } 2577 } 2578 showDraftScreen(); 2579 } 2580 2581 function cancelAttach() { 2582 // Close the "attach" pop-up dialog, and remove the modal setting 2583 // 2584 closeDlog("attach"); 2585 hideModalizer("modalizer"); 2586 return false; 2587 } 2588 2589 function attachDelete(draftId, part) { 2590 // Remove an attachment from a draft 2591 // 2592 showComposingScreen("Deleting the attachment ..."); 2593 pachyGet2("attachDelete", gotAttachEnum, null, null, draftId, part); 2594 return false; 2595 } 2596 2597 function checkSendDone(doc) { 2598 if (checkResult(doc, "sendDone")) { 2599 var err = doc.getAttribute("err"); 2600 if (err) { 2601 alert("send/save/discard failed: " + 2602 decodeURIComponent(err)); 2603 return false; 2604 } else { 2605 return true; 2606 } 2607 } else { 2608 return false; 2609 } 2610 } 2611 2612 function saveIfNeeded(callback) { 2613 // If draft has changed, save it, update lastSavedDraft and its time, 2614 // and return true. Completion of save request calls callback. 2615 // 2616 var draft = curDraft(); 2617 if (!lastSavedDraft.completed || 2618 draft.msgfrom != lastSavedDraft.msgfrom || 2619 draft.msgto != lastSavedDraft.msgto || 2620 draft.msgcc != lastSavedDraft.msgcc || 2621 draft.subject != lastSavedDraft.subject || 2622 draft.content != lastSavedDraft.content) { 2623 lastSavedDraft = draft; 2624 lastSavedDraft.savedAt = new Date(); 2625 pachyPostDraft("saveDraft", callback, draft); 2626 return true; 2627 } else { 2628 return false; 2629 } 2630 } 2631 2632 function startAutoSave() { 2633 // Arrange to call autoSaveDraft a while from now 2634 // 2635 var id = lastSavedDraft.id; 2636 setTimeout(function() { 2637 autoSaveDraft(id); 2638 }, 60000); 2639 } 2640 2641 function autoSaveDraft(id) { 2642 // Timed saving of draft, if modified 2643 if (document.getElementById("draftScreen").style.display == "block" && 2644 lastSavedDraft.id == id) { 2645 if (!saveIfNeeded(autoSaveDone)) startAutoSave(); 2646 } 2647 } 2648 2649 function autoSaveDone(doc) { 2650 // completion of background save operation 2651 // 2652 if (checkSendDone(doc)) { 2653 reportSavedTime("Auto-saved"); 2654 startAutoSave(); 2655 } else { 2656 alert("Auto-save failed; further auto-saves are disabled"); 2657 } 2658 } 2659 2660 function saveDraft() { 2661 // Manual click on "Save" button; also save after addRecipient 2662 // 2663 saveIfNeeded(saveDone); 2664 return false; 2665 } 2666 2667 function draftChange() { 2668 // Called on change event from one of the draft editting fields 2669 // 2670 saveIfNeeded(saveDone); 2671 return true; 2672 } 2673 2674 function saveDone(doc) { 2675 // completion of saveDraft or draftChange 2676 if (checkSendDone(doc)) { 2677 reportSavedTime("Saved"); 2678 } else { 2679 alert("Save failed"); 2680 } 2681 } 2682 2683 function confirmSendDraft() { 2684 // Ask for confirmation, then send the draft 2685 // 2686 askConfirm("Send this message now?", sendDraft); 2687 return false; 2688 } 2689 2690 function sendDraft() { 2691 // Send the draft 2692 // 2693 pachyPostDraft("sendDraft", doneWithDraft, curDraft()); 2694 showComposingScreen("Sending ..."); 2695 } 2696 2697 function ackAbandonDraft() { 2698 // Get acknowledgement from user, then abandon the draft 2699 // 2700 reportSuccess("The draft will remain in your Unsent folder", 2701 abandonDraft); 2702 return false; 2703 } 2704 2705 function abandonDraft() { 2706 if (saveIfNeeded(doneWithDraft)) { 2707 showComposingScreen("Saving ..."); 2708 } else { 2709 returnFromDraft(); 2710 } 2711 } 2712 2713 function confirmDeleteDraft() { 2714 // Ask for confirmation, then delete the draft 2715 // 2716 askConfirm("Permanently discard this draft message?", deleteDraft); 2717 return false; 2718 } 2719 2720 function deleteDraft() { 2721 // Delete the draft 2722 // 2723 pachyPostDraft("deleteDraft", doneWithDraft, curDraft()); 2724 showComposingScreen("Deleting ..."); 2725 } 2726 2727 function doneWithDraft(doc) { 2728 // completion of sendDraft/abandonDraft/deleteDraft 2729 // 2730 if (checkSendDone(doc)) { 2731 returnFromDraft(); 2732 } else { 2733 showDraftScreen(); 2734 } 2735 } 2736 2737 2738 // 2739 // Folders Screen 2740 // 2741 2742 function doFolders() { 2743 // switch to folders screen 2744 showOtherScreen("foldersScreen"); 2745 return false; 2746 } 2747 2748 function foldersDone() { 2749 document.getElementById("foldersNormal").style.visibility = "visible"; 2750 document.getElementById("foldersSmart").style.visibility = "visible"; 2751 showMainScreen("foldersScreen"); 2752 return false; 2753 } 2754 2755 function doFolderCreatePopup() { 2756 // Display the createFolder popup 2757 document.getElementById("foldersNormal").style.visibility = "hidden"; 2758 toggle("createFolder"); 2759 document.getElementById("createFolderName").value = ""; 2760 document.getElementById("createFolderName").focus(); 2761 return false; 2762 } 2763 2764 function setOpeners() { 2765 // Set the folder opening links on the main screen, using the already 2766 // set up selectors 2767 var options = document.getElementById("findFolder").options; 2768 var temp = ""; 2769 for (var i = findFolderFixed; i < options.length; i++) { 2770 var text = options[i].text; 2771 var value = options[i].value; 2772 temp += "<a href=\"#\" " + 2773 "onClick=\"return doOpen('" + htmlspecials(value) 2774 + "')\">" + htmlspecials(text) + "</a>"; 2775 } 2776 document.getElementById("fqOpeners").innerHTML = temp; 2777 } 2778 2779 function propagateFolderCreation(folder) { 2780 // Update other stuff to reflect new folder 2781 insertOption("findFolder", findFolderFixed, folder, folder, false); 2782 insertOption("moveFolder", moveFolderFixed, folder, folder, false); 2783 setOpeners(); 2784 theState.oldFolder = folder; 2785 } 2786 2787 function propagateFolderDeletion(folder) { 2788 // Update other stuff to reflect folder deletion 2789 deleteOption("findFolder", folder); 2790 deleteOption("moveFolder", folder); 2791 setOpeners(); 2792 } 2793 2794 function doFolderSave() { 2795 // Save from the createFolder popup 2796 var folder = document.getElementById("createFolderName").value; 2797 document.getElementById("foldersNormal").style.visibility = "visible"; 2798 toggle("createFolder"); 2799 pachyGet("createFolder", null, null, folder); 2800 return false; 2801 } 2802 2803 function doFolderDelete() { 2804 var folder = getOption("foldersNormal", true); 2805 pachyGet("deleteFolder", null, null, folder); 2806 } 2807 2808 function doEqCreatePopup() { 2809 // Display the editQuery popup in "create" mode 2810 document.getElementById("eqName1").value = ""; 2811 setCheckbox("eqFilter", true); 2812 document.getElementById("eqWords").value = theState.query.words; 2813 setOption("eqFindIn", theState.query.findIn); 2814 setOption("eqDateFrom", theState.query.dateFrom); 2815 setOption("eqDateTo", theState.query.dateTo); 2816 setOption("eqAcct", theState.query.acct); 2817 setCheckbox("eqUnread", true); 2818 document.getElementById("foldersNormal").style.visibility = "hidden"; 2819 document.getElementById("foldersSmart").style.visibility = "hidden"; 2820 toggle("editQuery"); 2821 document.getElementById("eqPrompt2").style.display="none"; 2822 document.getElementById("eqPrompt1").style.display="inline"; 2823 document.getElementById("eqName2").style.display="none"; 2824 document.getElementById("eqName1").style.display="inline"; 2825 document.getElementById("eqName1").focus(); 2826 return false; 2827 } 2828 2829 function doEqEditPopup() { 2830 // Display the editQuery popup in "edit" mode 2831 var folder = getOption("foldersSmart"); 2832 if (folder == null) { 2833 reportError("No smart folder selected"); 2834 } else { 2835 var theQ = savedQueries[folder]; 2836 document.getElementById("eqName1").value = folder; 2837 setCheckbox("eqFilter", theQ.filter); 2838 document.getElementById("eqWords").value = theQ.words; 2839 setOption("eqFindIn", theQ.findIn); 2840 setOption("eqDateFrom", theQ.dateFrom); 2841 setOption("eqDateTo", theQ.dateTo); 2842 setOption("eqAcct", theQ.acct); 2843 setCheckbox("eqUnread", theQ.keepUnread); 2844 document.getElementById("foldersNormal").style.visibility = 2845 "hidden"; 2846 document.getElementById("foldersSmart").style.visibility = 2847 "hidden"; 2848 toggle("editQuery"); 2849 document.getElementById("eqPrompt1").style.display="none"; 2850 document.getElementById("eqPrompt2").style.display="inline"; 2851 document.getElementById("eqName1").style.display="none"; 2852 document.getElementById("eqName2").style.display="inline"; 2853 document.getElementById("eqName2").innerHTML = 2854 "<b>" + htmlspecials(folder) + "</b>"; 2855 document.getElementById("eqWords").focus(); 2856 } 2857 return false; 2858 } 2859 2860 function existsOption(id, value) { 2861 // Return true iff given option exists in given selector 2862 var selector = document.getElementById(id); 2863 var options = selector.options; 2864 for (var i = 0; i < options.length; i++) { 2865 if (options[i].value == value) return true; 2866 } 2867 return false; 2868 } 2869 2870 function doEqSave() { 2871 // Save from the editQuery popup 2872 var folder = document.getElementById("eqName1").value; 2873 document.getElementById("foldersNormal").style.visibility = "visible"; 2874 document.getElementById("foldersSmart").style.visibility = "visible"; 2875 toggle("editQuery"); 2876 var theQ = new Query(""); 2877 theQ.words = document.getElementById("eqWords").value; 2878 theQ.findIn = getOption("eqFindIn"); 2879 theQ.dateFrom = getOption("eqDateFrom"); 2880 theQ.dateTo = getOption("eqDateTo"); 2881 theQ.acct = getOption("eqAcct"); 2882 theQ.filter = getCheckbox("eqFilter"); 2883 theQ.keepUnread = getCheckbox("eqUnread"); 2884 savedQueries[folder] = theQ; 2885 // Temporarily patch into theState, to keep pachyGet simple 2886 var savedQ = theState.query; 2887 theQ.unread = theQ.keepUnread; 2888 theState.query = theQ; 2889 pachyGet("saveQuery", null, null, folder); 2890 theQ.unread = false; 2891 theState.query = savedQ; 2892 return false; 2893 } 2894 2895 function doQueryDelete() { 2896 var folder = getOption("foldersSmart", true); 2897 pachyGet("deleteQuery", null, null, folder); 2898 } 2899 2900 function doFolderCancel(popup) { 2901 if (popup == "orderQuery") { 2902 setOption("foldersSmart", getOption("foldersOrder")); 2903 } 2904 document.getElementById("foldersNormal").style.visibility = "visible"; 2905 document.getElementById("foldersSmart").style.visibility = "visible"; 2906 return toggle(popup); 2907 } 2908 2909 function confirmFolderDelete(smart) { 2910 var folder = getOption(smart ? "foldersSmart" : "foldersNormal"); 2911 if (folder == null) { 2912 reportError("No folder selected"); 2913 } else { 2914 askConfirm("Really delete folder \"" + folder + "\"? " + 2915 "(All its messages will be moved to \"Dropped\")", 2916 (smart ? doQueryDelete : doFolderDelete)); 2917 } 2918 return false; 2919 } 2920 2921 function doOrderPopup() { 2922 setOption("foldersOrder", getOption("foldersSmart")); 2923 document.getElementById("foldersNormal").style.visibility = "hidden"; 2924 document.getElementById("foldersSmart").style.visibility = "hidden"; 2925 return toggle("orderQuery"); 2926 } 2927 2928 function doPromote() { 2929 var selector = document.getElementById("foldersOrder"); 2930 var selected = selector.selectedIndex; 2931 if (selected < 0) { 2932 reportError("No smart folder selected"); 2933 } else if (selected <= 0) { 2934 // already at the top 2935 } else { 2936 var target = selector.options[selected]; 2937 var victim = selector.options[selected-1]; 2938 var to = new Option(target.text, target.value); 2939 var vo = new Option(victim.text, victim.value); 2940 selector.options[selected] = vo; 2941 selector.options[selected-1] = to; 2942 selector.selectedIndex = selected-1; 2943 document.getElementById("promoteBtn").disabled = true; 2944 document.getElementById("demoteBtn").disabled = true; 2945 pachyGet("promoteQuery", null, null, to.value); 2946 } 2947 return false; 2948 } 2949 2950 function doDemote() { 2951 var selector = document.getElementById("foldersOrder"); 2952 var selected = selector.selectedIndex; 2953 if (selected < 0) { 2954 reportError("No smart folder selected"); 2955 } else if (selected+1 >= selector.options.length) { 2956 // already at the bottom 2957 } else { 2958 var target = selector.options[selected]; 2959 var victim = selector.options[selected+1]; 2960 var to = new Option(target.text, target.value); 2961 var vo = new Option(victim.text, victim.value); 2962 selector.options[selected] = vo; 2963 selector.options[selected+1] = to; 2964 selector.selectedIndex = selected+1; 2965 document.getElementById("promoteBtn").disabled = true; 2966 document.getElementById("demoteBtn").disabled = true; 2967 pachyGet("demoteQuery", null, null, to.value); 2968 } 2969 return false; 2970 } 2971 2972 function doQueryFind() { 2973 foldersDone(); 2974 var savedQName = getOption("foldersSmart"); 2975 if (savedQName == null) { 2976 reportError("No smart folder selected"); 2977 } else { 2978 jumpTo(savedQueries[savedQName]); 2979 } 2980 return false; 2981 } 2982 2983 2984 // 2985 // Settings Screen 2986 // 2987 2988 function doSettings() { 2989 // switch to settings screen 2990 showOtherScreen("settingsScreen"); 2991 return false; 2992 } 2993 2994 function settingsDone() { 2995 document.getElementById("accounts").style.visibility = "visible"; 2996 showMainScreen("settingsScreen"); 2997 return false; 2998 } 2999 3000 function doEditSettingsPopup() { 3001 // Display the editSettings popup 3002 var acct = null; 3003 for (var id in theAccounts) { 3004 acct = theAccounts[id]; 3005 break; 3006 } 3007 document.getElementById("settingsPwd").value = ""; 3008 setOption("settingsTZ", acct.displaytz); 3009 document.getElementById("accounts").style.visibility = "hidden"; 3010 toggle("editSettings"); 3011 return false; 3012 } 3013 3014 function doSettingsSave() { 3015 // We're running over SSL so we don't need to encrypt the new key, 3016 // which is just as well, because we don't have a suitable secret. 3017 // 3018 var newPachyPwd = document.getElementById("settingsPwd").value; 3019 document.getElementById("settingsPwd").value = ""; 3020 var dest = document.getElementById("settingsTZ").value + "\n" + 3021 "\n" + utf8(newPachyPwd); 3022 newPachyPwd = ""; 3023 document.getElementById("accounts").style.visibility = "visible"; 3024 toggle("editSettings"); 3025 pachyPostPwd("saveSettings", null, dest); 3026 return false; 3027 } 3028 3029 function doSettingsCancel() { 3030 document.getElementById("settingsPwd").value = ""; 3031 document.getElementById("accounts").style.visibility = "visible"; 3032 toggle("editSettings"); 3033 return false; 3034 } 3035 3036 function doAccountCreatePopup() { 3037 // Display the editAccount popup 3038 setOption("acctType", "POP3"); 3039 document.getElementById("acctId").value = 0; 3040 document.getElementById("acctServer").value = ""; 3041 document.getElementById("acctUser").value = ""; 3042 document.getElementById("acctPwd").value = ""; 3043 document.getElementById("acctMsgfrom").value = ""; 3044 document.getElementById("acctPerson").value = ""; 3045 setCheckbox("acctDoDelete", true); 3046 document.getElementById("acctPwdPrompt").style.visibility = "hidden"; 3047 document.getElementById("accounts").style.visibility = "hidden"; 3048 document.getElementById("acctPrompt2").style.display = "none"; 3049 document.getElementById("acctPrompt1").style.display = "inline"; 3050 toggle("editAccount"); 3051 return false; 3052 } 3053 3054 function doAccountEditPopup() { 3055 // Display the editAccount popup 3056 var id = getOption("accounts"); 3057 if (id == null) { 3058 reportError("No account selected"); 3059 } else { 3060 var acct = theAccounts[id]; 3061 setOption("acctType", acct.type); 3062 document.getElementById("acctId").value = id; 3063 document.getElementById("acctServer").value = acct.server; 3064 document.getElementById("acctUser").value = acct.user; 3065 document.getElementById("acctPwd").value = ""; 3066 document.getElementById("acctMsgfrom").value = acct.msgfrom; 3067 document.getElementById("acctPerson").value = acct.person; 3068 setCheckbox("acctDoDelete", acct.dodelete); 3069 document.getElementById("acctPwdPrompt").style.visibility = 3070 "inherit"; 3071 document.getElementById("accounts").style.visibility = "hidden"; 3072 document.getElementById("acctPrompt1").style.display = "none"; 3073 document.getElementById("acctPrompt2").style.display = "inline"; 3074 toggle("editAccount"); 3075 } 3076 return false; 3077 } 3078 3079 function doAccountSave() { 3080 // We're running over SSL so we don't need to encrypt the new key, 3081 // which is just as well, because we don't have a suitable secret. 3082 // 3083 var acctPwd = document.getElementById("acctPwd").value; 3084 document.getElementById("acctPwd").value = ""; 3085 var dest = document.getElementById("acctId").value + "\n" + 3086 document.getElementById("acctServer").value + "\n" + 3087 document.getElementById("acctUser").value + "\n" + 3088 "\n" + utf8(acctPwd) + "\n" + 3089 getOption("acctType") + "\n" + 3090 document.getElementById("acctMsgfrom").value + "\n" + 3091 document.getElementById("acctPerson").value + "\n" + 3092 (getCheckbox("acctDoDelete") ? "Y" : "N"); 3093 acctPwd = ""; 3094 document.getElementById("accounts").style.visibility = "visible"; 3095 toggle("editAccount"); 3096 pachyPostPwd("saveAccount", null, dest); 3097 return false; 3098 } 3099 3100 function doAccountCancel() { 3101 document.getElementById("acctPwd").value = ""; 3102 document.getElementById("accounts").style.visibility = "visible"; 3103 toggle("editAccount"); 3104 return false; 3105 } 3106 3107 function doAccountDelete(id) { 3108 pachyGet("forgetAccount", null, null, id); 3109 } 3110 3111 function confirmAccountDelete() { 3112 var id = getOption("accounts"); 3113 if (id == null) { 3114 reportError("No account selected"); 3115 } else { 3116 askConfirm("Really forget the account \"" + 3117 theAccounts[id].msgfrom + "\"?", 3118 function() { doAccountDelete(id); }); 3119 } 3120 return false; 3121 } 3122 3123 function updateAccountsUI() { 3124 // Update UI to match current state of "theAccounts" 3125 var findAcctSel = document.getElementById("findAcct"); 3126 var eqAcctSel = document.getElementById("eqAcct"); 3127 var accountsSel = document.getElementById("accounts"); 3128 truncateOptions("findAcct", 1); 3129 if (eqAcctSel) truncateOptions("eqAcct", 1); 3130 if (accountsSel) truncateOptions("accounts", 0); 3131 for (var id in theAccounts) { 3132 var acct = theAccounts[id]; 3133 if (acct) { 3134 var prompt = acct.msgfrom + " (" + 3135 (acct.type == "SEND" ? "\"From\" only" : 3136 acct.server) + ")"; 3137 appendOption(findAcctSel, acct.msgfrom, ""+id); 3138 if (eqAcctSel) appendOption(eqAcctSel, prompt, ""+id); 3139 if (accountsSel) appendOption(accountsSel, prompt, ""+id); 3140 } 3141 } 3142 if (accountsSel) accountsSel.selectedIndex = 0; 3143 } 3144 3145 3146 // 3147 // Initialisation 3148 // 3149 3150 function switchVersion(version) { 3151 // Switch to the mobile or to the desktop version of the UI 3152 // 3153 var argUser = getQueryArg("user"); 3154 document.location.replace("./?" + encodeURIComponent(version) + 3155 (argUser ? "&user=" + encodeURIComponent(argUser) : "")); 3156 return false; 3157 } 3158 3159 function init() { 3160 if (!document.getElementById) return; 3161 if (!("ontouchstart" in document.documentElement)) { 3162 document.documentElement.className += " noTouch"; 3163 } else { 3164 document.documentElement.className += " touch"; 3165 } 3166 findFolderFixed = document.getElementById("findFolder").options.length; 3167 moveFolderFixed = document.getElementById("moveFolder").options.length; 3168 new Button("contacts"); 3169 new Button("resend"); 3170 new Button("compose"); 3171 new Button("fetch"); 3172 new Button("next"); 3173 new Button("find"); 3174 new Button("folders"); 3175 new Button("settings"); 3176 new Button("logout"); 3177 new Button("forward"); 3178 new Button("replyAll"); 3179 new Button("reply"); 3180 new Button("scan"); 3181 new Button("drop"); 3182 new Button("trash"); 3183 new Button("file"); 3184 new Button("mark"); 3185 user = getQueryArg("user"); 3186 if (!user) user = getCookie("pachydkuser"); 3187 if (!user) user = ""; 3188 document.getElementById("loginUser").value = user; 3189 if (user == "") { 3190 document.getElementById("loginUser").focus(); 3191 } else { 3192 document.getElementById("loginPwd").focus(); 3193 } 3194 theState = new State(new Query("Inbox")); 3195 autoLogin(); 3196 }
End of listing