Source of “pachysql.php”.
2038 lines, 62.6 KBytes.   Last modified 8:14 am, 19th April 2017 PDT.
1 <?php // 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.html // 10 // // 11 // HTML-independent, mostly SQL, stuff // 12 // // 13 //////////////////////////////////////////////////////////////////////////// 14 15 16 // 17 // Local environment and design constants (see also HTML section) 18 // 19 20 define("C_program", "Pachylet"); 21 define("C_version", "2.2"); 22 define("C_dbHost", "localhost"); // Pachylet mysql server host name 23 define("C_database", "pachylet"); // database name within the server 24 define("C_maxDBLength", "10000"); // limit on content stored in DB 25 define("C_partsDir", "../parts"); // file directory for big msg parts 26 define("C_pwdDir", "../pachypwd"); // user password directory 27 define("C_dkCookie", "pachydk"); // cookie name for derived key 28 define("C_dkUser", "pachydkuser"); // cookie name for DK matching user 29 define("C_sendmail", "/usr/sbin/sendmail"); // file name of sendmail 30 define("C_localDomain", "UNKNOWN"); // default host name for email addrs 31 32 define("C_partIV", "1234567890123456"); // IV for encrypting part files 33 // We use a different key for each file, so it's OK to use a fixed IV 34 35 define("C_incoming", "Incoming"); // new mail before filter is applied 36 define("C_unsent", "Unsent"); // unsent drafts 37 define("C_inbox", "Inbox"); // inbox label and prompt 38 define("C_dropped", "Dropped"); // dropped label and prompt 39 define("C_trash", "Trash"); // trash label and prompt 40 define("C_unread", "Unread"); // "unread" flag as a label 41 define("C_all", "All except trash"); // prompt for searching all folders 42 43 define("C_ftIndex", "127.0.0.1:9306"); // Sphinx full-text index serer 44 define("C_indexer", "/usr/bin/indexer");// Sphinx indexer program 45 46 $readOnly = file_exists("/etc/pachyletRO"); 47 48 49 // 50 // Timing and error reporting 51 // 52 53 openlog(C_program, LOG_PID, LOG_DAEMON); 54 55 function writeLog($s) { 56 syslog(LOG_INFO, $s); 57 } 58 59 function elapsed($startTime) { 60 // Return a string with msec elapsed time since $startTime 61 $stopTime = microtime(); 62 list($startUsec, $startSec) = explode(" ", $startTime); 63 list($stopUsec, $stopSec) = explode(" ", $stopTime); 64 // Subtract the seconds before assembling the float, to avoid requiring 65 // too much float precision. 66 return sprintf("%01.3f", 67 (float)($stopSec - $startSec) + 68 ((float)$stopUsec - (float)$startUsec)); 69 } 70 71 function ignoreError($errno, $errmsg, $filename, $linenum, $vars) { 72 // Suppress errors, during POP/IMAP or MySql logon 73 } 74 75 function onError($errno, $errmsg, $filename, $linenum, $vars) { 76 if (preg_match("#Invalid mailbox list#i", $errmsg) || 77 preg_match("#Unterminated mailbox#i", $errmsg) || 78 preg_match("#Must use comma to separate addresses#i", $errmsg) || 79 preg_match("#Unterminated at-domain-list#i", $errmsg) || 80 preg_match("#Unexpected characters at end of address#i", $errmsg) || 81 preg_match("#Message has unknown MIME version#i", $errmsg)) { 82 // Ignore it 83 } else { 84 // Report error to user. Produces invalid XML 85 die("<p><hr/>Pachylet internal error:\n" . 86 "<blockquote>\n" . 87 "$errmsg<br/><br/>\nAt line $linenum of $filename\n" . 88 "</blockquote><hr/></p>"); 89 } 90 } 91 92 set_error_handler("onError"); // convert errors to HTML 93 94 95 // 96 // Low-level database operations 97 // 98 99 $pachyConnection = null; 100 101 function wrapSqlArg($value) { 102 return "'" . mysql_escape_string($value) . "'"; 103 } 104 105 function connectDB($user, $h2) { 106 // Open connection to C_database on the mysql server 107 // 108 // Connection is held in a global, mostly for historical reasons 109 // 110 // User names are restricted to alphanumeric; they get used in table 111 // names. 112 // 113 global $pachyConnection; 114 if (!preg_match("/[a-z0-9]+/i", $user)) return false; 115 $oldErr = set_error_handler("ignoreError"); 116 $connection = mysql_connect(C_dbHost, wrapSqlArg($user), $h2); 117 set_error_handler($oldErr); 118 if (!$connection) return $connection; 119 $rc = mysql_select_db(C_database, $connection); 120 if ($rc) $pachyConnection = $connection; 121 return ($rc); 122 } 123 124 function canAccess($user) { 125 // Return true iff connected user has appropriate DB entries 126 // 127 global $pachyConnection; 128 $rc = mysql_query("select * from accts$user limit 1", $pachyConnection); 129 return ($rc ? true : false); 130 } 131 132 function canManage() { 133 // Return true iff connected user has management rights 134 // 135 global $pachyConnection; 136 $rc = mysql_query("select * from mysql.user limit 1", $pachyConnection); 137 return ($rc ? true : false); 138 } 139 140 $sqlTiming = false; 141 142 function doSqlQuery($prompt, $query) { 143 // Execute a SQL query on $pachyConnection and return the result. 144 // Reports and exits on errors. 145 // 146 global $pachyConnection, $sqlTiming; 147 if ($sqlTiming) $startTime = microtime(); 148 $rc = mysql_query($query, $pachyConnection); 149 if (!$rc) { 150 $err = mysql_error($pachyConnection); 151 writeLog("Query for $prompt failed $err"); 152 die("Query for $prompt failed: $err\n<br>Query = $query"); 153 } 154 if ($sqlTiming) { 155 echo $prompt . " took " . elapsed($startTime) . " sec;<br>\n"; 156 // echo htmlspecialchars($query) . "<br>\n"; 157 } 158 return $rc; 159 } 160 161 function pachyInsertID() { 162 // Return last inserted ID on $pachyConnection 163 // 164 global $pachyConnection; 165 return mysql_insert_id($pachyConnection); 166 } 167 168 function pachyAffectedRows() { 169 // Return number of rows modified by last query on $pachyConnection 170 // 171 global $pachyConnection; 172 return mysql_affected_rows($pachyConnection); 173 } 174 175 function wrapParams(&$params) { 176 // Wrap parameter array as quote-escaped, NL delimited string; or NULL 177 unset($str); 178 if (is_array($params)) { 179 foreach ($params as $param) { 180 $str[] = $param->attribute; 181 $str[] = $param->value; 182 } 183 } 184 return (isset($str) ? wrapSqlArg(implode("\n", $str)) : "NULL"); 185 } 186 187 $lockCount = 0; 188 189 function lockDB($user) { 190 // Acquire exclusive access to user's part of the database 191 global $lockCount; 192 if ($lockCount == 0) { 193 doSqlQuery("acquire locks", 194 "lock tables accts$user write, " . 195 "msgs$user write, parts$user write, labels$user write, " . 196 "queries$user write, contacts$user write, " . 197 "hits$user write, fthits$user write"); 198 } 199 $lockCount++; 200 } 201 202 function unlockDB($user) { 203 global $lockCount; 204 $lockCount--; 205 if ($lockCount == 0) doSqlQuery("release locks", "unlock tables"); 206 } 207 208 209 // 210 // Reading the account database. 211 // Note that the "pwd" option requires RC4, which is in pachylib.php 212 // 213 214 function getAcct($user, $h2 = NULL, $acctid = 1) { 215 // Get specific account details for the user. 216 // If no such account, get primary account 217 // Note that the account password is encrypted with the H2 hash 218 $rows = doSqlQuery("Get account details for $user", 219 "select acctid, user, pwd, pwdiv, " . 220 "server, type, dodelete, msgfrom, person, displaytz, udate " . 221 "from accts$user where acctid = $acctid"); 222 if ($acct = mysql_fetch_object($rows)) { 223 if (!is_null($h2)) { 224 keyRC4withIV($acct->pwdiv, $h2); 225 $acct->pwd = cryptRC4($acct->pwd); 226 } 227 } else if ($acctid != 1) { 228 $acct = getAcct($user, $h2, 1); 229 } 230 mysql_free_result($rows); 231 return $acct; 232 } 233 234 function getAccts($user, $h2 = NULL) { 235 // Get all account details for the user. 236 // Note that the account password is encrypted with the H2 hash 237 $rows = doSqlQuery("Get accounts for $user", 238 "select acctid, user, pwd, pwdiv, " . 239 "server, type, dodelete, msgfrom, person, displaytz " . 240 "from accts$user"); 241 $accts = array(); 242 while ($acct = mysql_fetch_object($rows)) { 243 if (!is_null($h2)) { 244 keyRC4withIV($acct->pwdiv, $h2); 245 $acct->pwd = cryptRC4($acct->pwd); 246 } 247 $accts[] = $acct; 248 } 249 mysql_free_result($rows); 250 return $accts; 251 } 252 253 254 // 255 // Reading message details and parts 256 // 257 258 function getMsgDetails($user, $id) { 259 // Get the message details for given id 260 $rows = doSqlQuery("get msg details for $id", 261 "select acctid, md5, msgid, udate, subject, msgfrom, msgto, " . 262 "msgcc, unread from msgs$user where id = $id limit 1"); 263 return mysql_fetch_object($rows); 264 } 265 266 function getMsgDate($user, $id) { 267 // Get the udate field of the given message's details 268 $rows = doSqlQuery("get msg udate for $id", 269 "select udate from msgs$user where id = $id limit 1"); 270 $row = mysql_fetch_object($rows); 271 if (!$row) die("getMsgDate for invalid id $id"); 272 return $row->udate; 273 } 274 275 function getMsgParts($user, $id, $startAt = 0) { 276 // Fetch list of parts of $id from the database 277 // Note that this doesn't include all of the columns 278 $parts = doSqlQuery("enumerate parts of $id", 279 "select part, type, subtype, contentid, " . 280 "dparameters, parameters, children, length " . 281 "from parts$user where id = $id " . 282 ($startAt > 0 ? "and part >= $startAt " : "") . 283 "order by part" ); 284 return $parts; 285 } 286 287 function getPart($user, $partID) { 288 // Fetch given part from the database, including the content column 289 $thePart = doSqlQuery("get part $partID", 290 "select id, part, filekey, mac, imapnum, type, subtype, " . 291 "encoding, description, contentid, disposition, dparameters, " . 292 "parameters, children, length, content " . 293 "from parts$user " . 294 "where part = $partID" ); 295 $row = mysql_fetch_object($thePart); 296 return $row; 297 } 298 299 300 // 301 // Executing queries. Results are cached in hits$user 302 // 303 // External entry points to this section are getHits, deleteCachedHits; 304 // the other functions are private. 305 // 306 307 function queryDate($date, $endof) { 308 // Return Unix time corresponding to date letter, but 0 for "B" and "E" 309 // Note that mktime compensates for the strange cases 310 if ($date == "B" || $date == "E") { 311 return 0; 312 } else if ($date == "H") { 313 return time() - 2 * 24 * 60 * 60; 314 } else if ($date == "W") { 315 return time() - 7 * 24 * 60 * 60; 316 } else if ($date == "M" || $date == "Q") { 317 $day = date("j"); 318 $month = date("n") - ($date == "M" ? 1 : 3); 319 $year = date("Y"); 320 if ($month < 1) { $year -=1; $month += 12; } 321 return gmmktime(0, 0, 0, $month, $day, $year); 322 } else if ($date == "Y") { 323 $day = date("j"); 324 $month = date("n"); 325 $year = date("Y") - 1; 326 return gmmktime(0, 0, 0, $month, $day, $year); 327 } else { 328 $year = 1995 + ord($date) - ord("a"); 329 if ($endof) $year++; 330 if ($year < 0 || $year > 2037) return 0; 331 return gmmktime(0, 0, 0, 1, 1, $year); 332 } 333 } 334 335 function queryWhere($user, &$query, $unread = false) { 336 // Return SQL "WHERE" fragment for the current query 337 // If $unread, restrict to unread msgs, regardless of $query 338 $datefrom = queryDate($query->datefrom, false); 339 $dateto = queryDate($query->dateto, true); 340 if ($dateto <= $datefrom) { 341 $dateto = 0; 342 $query->dateto = "E"; 343 } 344 return "msgs$user.id = labels$user.id " . 345 (isset($query->id) ? "and msgs$user.id >= $query->id " : "") . 346 ($query->f == "" ? "and (label <> " . wrapSqlArg(C_trash) . ") " . 347 "and (label <> " . wrapSqlArg(C_unsent) . ") " : 348 "and (label = " . wrapSqlArg($query->f) . ") ") . 349 ($query->f == "" ? "and (label <> '" . C_unread . "') " : "") . 350 " " . 351 ($datefrom == 0 ? "" : "and msgs$user.udate >= $datefrom ") . 352 ($dateto == 0 ? "" : "and msgs$user.udate < $dateto ") . 353 ($query->acctid == 0 ? "" : 354 "and msgs$user.acctid = $query->acctid ") . 355 ($query->t == "" ? "" : "and fthits$user.id = msgs$user.id ") . 356 ((isset($query->unread) || $unread) ? "and (unread = 'Y')" : ""); 357 } 358 359 function queryToText(&$query) { 360 // Return plain text for the query, as tag for the hit cache 361 // 362 $q[] = "1"; 363 $q[] = $query->f; 364 $q[] = $query->t; 365 $q[] = $query->findin; 366 $q[] = (isset($query->unread) ? 'Y' : 'N'); 367 $q[] = $query->datefrom; 368 $q[] = $query->dateto; 369 $q[] = $query->acctid; 370 return implode("\n", $q); 371 } 372 373 function getCachedQuery($user) { 374 // Return text of query that's cached in hits$user, or NULL 375 // 376 $rows = doSqlQuery("get cached query", 377 "select cachedquery from accts$user limit 1"); 378 $row = mysql_fetch_object($rows); 379 return $row->cachedquery; 380 } 381 382 function setCachedQuery($user, $qText) { 383 // Record qText as being the query in the hit cache 384 // 385 $prev = getCachedQuery($user); 386 if ($prev != $qText) { 387 doSqlQuery("set cached query", 388 "update accts$user set cachedquery = " . 389 (is_null($qText) ? "NULL" : wrapSqlArg($qText))); 390 } 391 } 392 393 function getFTHits($user, $t, $findin) { 394 // Populate fthits$user with results from full-text search. 395 // 396 $hits = "fthits$user"; 397 $ftConn = mysql_connect(C_ftIndex); // no authentication in Sphinx 398 if (!$ftConn) { 399 die("Failed to connect to full-text index server"); 400 } 401 // $findin = A (all), H (header), S (subject), F (from), T (to or cc) 402 if ($findin == "S") $t = "@subject $t"; 403 else if ($findin == "F") $t = "@msgfrom $t"; 404 else if ($findin == "T") $t = "@msgtocc $t"; 405 $ftQuery = 406 "select msgid from $user, ${user}Delta " . 407 "where match(" . wrapSqlArg($t) . ") " . 408 ($findin == "H" ? "and header = 1 " : "") . 409 "group by msgid limit 100000 " . 410 "option max_matches=100000, ranker='none'"; 411 $rows = mysql_query($ftQuery, $ftConn); 412 if ($rows) { 413 while ($row = mysql_fetch_object($rows)) $ids[] = $row->msgid; 414 } // else query failed; probably syntax error in query: ignore it 415 doSqlQuery("erase fthits", "delete from $hits"); 416 if (isset($ids)) { 417 doSqlQuery("insert into $hits", 418 "insert into $hits values (" . implode("),(", $ids) . ")"); 419 } 420 } 421 422 function getHits($user, &$query) { 423 // Get the id's of the targeted messages and store them in hit cache 424 // 425 // We lock the database for the remainder of this execution. 426 // 427 lockDB($user); 428 $qText = queryToText($query); 429 if (getCachedQuery($user) != $qText) { 430 if ($query->t != "") getFTHits($user, $query->t, $query->findin); 431 setCachedQuery($user, NULL); 432 doSqlQuery("erase hits$user", "delete from hits$user"); 433 doSqlQuery("find hits in $query->f", 434 "insert ignore into hits$user " . 435 "select msgs$user.id, msgs$user.udate " . 436 "from msgs$user, labels$user" . 437 ($query->t == "" ? " " : ", fthits$user ") . 438 "where " . queryWhere($user, $query)); 439 setCachedQuery($user, $qText); 440 } 441 } 442 443 function deleteCachedHit($user, $id) { 444 // Ensure that message $id isn't in the hit cache 445 // 446 $udate = getMsgDate($user, $id); // better key for hits$user 447 $rows = doSqlQuery("check cached hit $id", 448 "select id from hits$user where udate = $udate and id = $id"); 449 if (mysql_fetch_object($rows)) { 450 doSqlQuery("delete cached hit $id", 451 "delete from hits$user where udate = $udate and id = $id"); 452 } 453 } 454 455 456 // 457 // Examining the hits: select(Next)Message, getMsgIDs, countQueryMsgs 458 // 459 460 function countMsgs($user) { 461 // Return the number of messages matching the query 462 $rows = doSqlQuery("count hits", 463 "select count(*) as total from hits$user"); 464 $row = mysql_fetch_object($rows); 465 return $row->total; 466 } 467 468 function getMsgLoc($user, $udate, $id) { 469 // Count messages greater than ($udate, $id) matching the query 470 $rows = doSqlQuery("locate $id", 471 "select count(*) as total from hits$user where " . 472 "(udate > $udate or ( (udate = $udate) and (id > $id)))"); 473 $row = mysql_fetch_object($rows); 474 return $row->total; 475 } 476 477 function getMsgList($user, &$offset, $limit, &$total) { 478 // Get the list of messages 479 // Sets $total to the total matching messages 480 // Note that &$offset might get updated to make it in range 481 $total = countMsgs($user); 482 if ($limit > $total) $limit = $total; 483 if ($offset > $total-$limit) $offset = $total-$limit; 484 if ($offset < 0) $offset = 0; 485 $msgs = doSqlQuery("getMsgList", 486 "select " . 487 "hits$user.id, hits$user.udate, subject, msgfrom, " . 488 "msgto, msgcc, unread " . 489 "from msgs$user, hits$user where msgs$user.id = hits$user.id " . 490 "order by hits$user.udate desc, hits$user.id desc " . 491 "limit $offset, $limit"); 492 return $msgs; 493 } 494 495 function getMsgAt($user, $offset) { 496 // Like getMsgList, but gets just the id and udate of the 497 // single message at the given offset. Returns resulting object, 498 // or NULL if no message at that offset. 499 $msgs = doSqlQuery("find msg at $offset", 500 "select id, udate from hits$user order by udate desc, id desc " . 501 "limit $offset, 1"); 502 return mysql_fetch_object($msgs); 503 } 504 505 function getNextMsg($user, $id, $udate, $unread) { 506 // Get first message after ($id, $udate), using given query params 507 // If $unread, restrict to unread mssgs, regardless of $query 508 $hdrs = doSqlQuery("find next after $udate, $id", 509 "select hits$user.id, hits$user.udate from hits$user" . 510 ($unread ? ", labels$user" : "") . " where " . 511 ($unread ? "label = " . wrapSqlArg(C_unread) . 512 " and labels$user.id = hits$user.id" . 513 " and " : "") . 514 "(hits$user.udate > $udate or " . 515 "( (hits$user.udate = $udate) and (hits$user.id > $id))) " . 516 "order by hits$user.udate, hits$user.id limit 1" ); 517 return mysql_fetch_object($hdrs); // guaranteed to be the only one 518 } 519 520 function moreUnread($user, $id, $offset, $pageSize) { 521 // Return true iff the "nextUnread" operation would find a message 522 if ($id == 0) { 523 // Act as if message preceding the current page was selected 524 $msg = getMsgAt($user, $offset + $pageSize); 525 if ($msg) { 526 $id = $msg->id; 527 $udate = $msg->udate; 528 } else { 529 // No preceding message, i.e. start of hit list 530 $udate = 0; 531 } 532 } else { 533 $udate = getMsgDate($user, $id); 534 } 535 $hdr = getNextMsg($user, $id, $udate, true); 536 return ($hdr ? true : false); 537 } 538 539 function selectNextMessage($user, &$query, &$id, &$offset, $pageSize, 540 $doNextUnread, $sticky) { 541 // Select an appropriate next message and scroll offset. 542 // 543 // Input parameters: 544 // $user is the user 545 // $query is the query 546 // $id specifies current selection: 547 // > 0 is selected message 548 // = 0 means nothing is selected 549 // $offset is current scroll offset (>=0) 550 // $pageSize is the quantum for scrolling 551 // $doNextUnread chooses next unread message, if any 552 // $sticky selects last msg when no more messages 553 // 554 // Outputs: 555 // $id is the new selected message (>0) or 0 for no selection 556 // $offset is the new scroll offset (>=0) 557 558 getHits($user, $query); 559 560 if ($id > 0) { 561 $udate = getMsgDate($user, $id); 562 } else { 563 // Act as if message preceding the current page was selected 564 $msg = getMsgAt($user, $offset + $pageSize); 565 if ($msg) { 566 $id = $msg->id; 567 $udate = $msg->udate; 568 } else { 569 $id = 0; 570 $udate = -1; 571 } 572 } 573 574 $hdr = getNextMsg($user, $id, $udate, $doNextUnread); 575 576 if ($hdr) { 577 $id = $hdr->id; 578 $loc = getMsgLoc($user, $hdr->udate, $id); 579 $offset = floor($loc/$pageSize) * $pageSize; 580 } else { 581 if ($sticky && ($msg = getMsgAt($user, 0))) { 582 $id = $msg->id; 583 } else { 584 $id = 0; 585 } 586 $offset = 0; 587 } 588 589 } 590 591 function selectMessage($user, &$query, &$id, &$offset, $pageSize, 592 &$total, &$lastIsSelected, &$moreToRead) { 593 // If $id < 0, select an appropriate message and scroll position. 594 // In any case, return the appropriate message list, and auxiliary 595 // details. 596 // 597 // Input parameters: 598 // $user is the user 599 // $query is the query 600 // $id specifies current selection: 601 // > 0 is selected message 602 // = 0 means nothing is selected 603 // < 0 requests auto-selection 604 // $offset specifies current scroll position 605 // $pageSize is the quantum for scrolling 606 // 607 // Outputs: 608 // $id is the new selected message (>0) or 0 for no selection 609 // $offset is the new scroll position (>=0) 610 // $total is the total number of messages matching the query 611 // $lastIsSelected means the last message is now selected 612 // Result is the list of messages to display, at most $pageSize 613 // $moreToRead is count of unread messages after selected one 614 615 $autoSelect = ($id < 0); 616 617 getHits($user, $query); 618 619 if ($autoSelect) { 620 // scroll to page containing first unread message, or else last page 621 $hdr = getNextMsg($user, 0, -1, true); // get first unread 622 if ($hdr) { 623 $loc = getMsgLoc($user, $hdr->udate, $hdr->id); 624 $offset = floor($loc/$pageSize) * $pageSize; 625 } else { 626 $offset = 0; 627 } 628 } 629 630 if ($offset < 0) die("Unexpected offset < 0"); 631 632 // Get the message list 633 $msgs = getMsgList($user, $offset, $pageSize, $total); 634 $msgCount = mysql_num_rows($msgs); 635 636 if ($msgCount == 0) { 637 $id = 0; 638 $lastIsSelected = true; 639 } else { 640 $lastMsg = mysql_fetch_object($msgs); 641 if ($autoSelect) { 642 // If there are unread messages in the list, select message 643 // preceding first unread message, or nothing if first 644 // message is unread, or nothing if none are unread. 645 mysql_data_seek($msgs, $msgCount-1); // seek to earliest message 646 $firstMsg = mysql_fetch_object($msgs); 647 $id = 0; 648 if ($firstMsg->unread == 'N') { 649 $readMsg = $firstMsg; 650 for ($i = 2; $i <= $msgCount; $i++) { 651 mysql_data_seek($msgs, $msgCount-$i); 652 $hdr = mysql_fetch_object($msgs); 653 if ($hdr->unread == 'N') { 654 $readMsg = $hdr; 655 } else { 656 $id = $readMsg->id; 657 break; 658 } 659 } 660 } 661 } 662 $lastIsSelected = ($offset == 0 && $id == $lastMsg->id); 663 } 664 665 if ($id < 0) die("Unexpected m < 0"); 666 667 $moreToRead = moreUnread($user, $id, $offset, $pageSize); 668 669 return $msgs; 670 } 671 672 function getMsgIds($user, &$query) { 673 // Return an array containing id's of all messages matching the query 674 getHits($user, $query); 675 $msgs = doSqlQuery("getMsgIds in $query->f", 676 "select id from hits$user order by udate, id"); 677 $res = array(); 678 while ($msg = mysql_fetch_object($msgs)) $res[] = $msg->id; 679 mysql_free_result($msgs); 680 return $res; 681 } 682 683 function countQueryMsgs($user, &$query) { 684 // Return the count of messages matching the query 685 getHits($user, $query); 686 return countMsgs($user); 687 } 688 689 690 // 691 // Saved queries 692 // 693 694 function saveQuery($user, &$query) { 695 // Save given query. If it has a qid, replace existing entry 696 // else create a new one. 697 // Returns the (new) id of the saved query 698 $update = isset($query->qid); 699 $rc = doSqlQuery("insert query $query->name", 700 ($update ? "update" : "insert into") . " queries$user set " . 701 "name = " . wrapSqlArg($query->name) . 702 (isset($query->scan) ? 703 ", scan = " . wrapSqlArg($query->scan) : "") . 704 ", filter = " . wrapSqlArg((isset($query->filter) ? 'Y' : 'N')) . 705 ", f = " . wrapSqlArg($query->f) . 706 ", t = " . wrapSqlArg($query->t) . 707 ", findin = " . wrapSqlArg($query->findin) . 708 ", unread = " . wrapSqlArg((isset($query->unread) ? 'Y' : 'N')) . 709 ", datefrom = " . wrapSqlArg($query->datefrom) . 710 ", dateto = " . wrapSqlArg($query->dateto) . 711 ", acctid = " . wrapSqlArg($query->acctid) . 712 ", udate = " . time() . 713 ($update ? " where qid = " . wrapSqlArg($query->qid) : "") ); 714 if (!$update) addLabel($user, $query->name, 0, 'N'); 715 return ($update ? $query->qid : pachyInsertID()); 716 } 717 718 function moveQuery($user, $qid, $scan) { 719 // Replace the "scan" field of a query 720 $rc = doSqlQuery("change query scan value", 721 "update queries$user set scan = $scan, udate = " . time() . 722 " where qid = $qid"); 723 } 724 725 function deleteQuery($user, $name) { 726 // Delete given query from database 727 if ($name != C_dropped) { 728 $query = new stdClass(); 729 $query->f = $name; 730 $query->t = ""; 731 $query->findin = "A"; 732 $query->datefrom = "B"; 733 $query->dateto = "E"; 734 $query->acctid = 0; 735 moveAllMsgs($user, $query, C_dropped); 736 $rc = doSqlQuery("forget query $name", 737 "delete from queries$user where name = " . wrapSqlArg($name)); 738 } 739 } 740 741 function getQuery($user, $name) { 742 // Return saved query with given name 743 $rows = doSqlQuery("get query $name", 744 "select qid, scan, name, filter, f, t, findin, unread, " . 745 "datefrom, dateto, acctid " . 746 "from queries$user where name = ". wrapSqlArg($name) . 747 " limit 1" ); 748 if ($row = mysql_fetch_object($rows)) { 749 if ($row->filter != 'Y') unset($row->filter); 750 if ($row->unread != 'Y') unset($row->unread); 751 return $row; 752 } else { 753 return false; 754 } 755 } 756 757 function getNextQuery($user, $scan) { 758 // Get query with scan > $scan, if any 759 $rows = doSqlQuery("get next query $scan", 760 "select qid, scan, name from queries$user" . 761 " where scan > $scan limit 1"); 762 return mysql_fetch_object($rows); 763 } 764 765 function promoteQuery($user, $name) { 766 // Promote given query (i.e. scan it earlier) 767 // 768 // Note that getQueries keeps scan fields sequential from 1. 769 // 770 // Promote works by giving this query the same scan value as 771 // its predecessor, then increasing its predecessor's scan value. 772 // If we crash between the operations, getQueries will fix things 773 // up, resolving the conflict by qid. 774 $query = getQuery($user, $name); 775 if ($query && $query->scan > 1) { 776 $pred = getNextQuery($user, $query->scan-2); 777 if ($pred && $pred->qid != $query->qid) { 778 moveQuery($user, $query->qid, $query->scan-1); 779 moveQuery($user, $pred->qid, $query->scan); 780 } 781 } 782 } 783 784 function demoteQuery($user, $name) { 785 // Demote given query (equivalent to promoting its successor) 786 // There are more efficient techniques, but who cares? 787 $query = getQuery($user, $name); 788 if ($query) { 789 $succ = getNextQuery($user, $query->scan); 790 if ($succ) promoteQuery($user, $succ->name); 791 } 792 } 793 794 function appendQueryNames($user, &$folders) { 795 // Append to $folders the names for the saved queries, in scan order 796 // First, fix up invariants on the "scan" field 797 // 798 $rows = doSqlQuery("get query list", 799 "select qid, scan from queries$user order by scan, qid"); 800 $nextScan = 1; 801 while ($row = mysql_fetch_object($rows)) { 802 // Fix up the scan values to be sequential from 1. 803 // This fixes up for a crash during promote 804 // or demote, and also restores the "sequential" 805 // invariant from any starting state. Promote relies on this. 806 // Note that we sorted by (scan,qid) in the query 807 if ($row->scan != $nextScan) moveQuery($user, $row->qid, $nextScan); 808 $nextScan++; 809 } 810 $rows = doSqlQuery("get query list", 811 "select name from queries$user order by scan"); 812 while ($row = mysql_fetch_object($rows)) $folders[] = $row->name; 813 mysql_free_result($rows); 814 } 815 816 function findUnreadFolder($user, $after) { 817 // Return a folder with unread messages, or "" if none. 818 // 819 // Considers only inbox and saved query folders. The search starts 820 // after folder $after (or at inbox if $after is ""), and continues 821 // cyclically to $after, excluding $after. 822 // 823 $found = ""; 824 $folders[] = C_inbox; 825 appendQueryNames($user, $folders); 826 $count = count($folders); 827 for ($afterPos = 0; $afterPos < $count; $afterPos++) { 828 if ($folders[$afterPos] == $after) break; 829 } 830 // $afterPos is index of $after, or is $count if $after not found (in 831 // which case we search all the folders). 832 for ($pos = $afterPos+1; ; $pos++) { 833 if ($pos == $afterPos) break; // required if $afterPos==$count 834 if ($pos >= $count) $pos = 0; 835 if ($pos == $afterPos) break; // required if $afterPos==0 836 $f = $folders[$pos]; 837 if (testUnreadMsgsWithLabel($user, $f)) return $f; 838 } 839 return ""; 840 } 841 842 function applyInboxFilter($user) { 843 // Apply user's filter to messages in "incoming" 844 // This is intended to be very fast if "incoming" is empty. 845 // 846 if (countMsgsWithLabel($user, C_incoming, 1) > 0) { 847 $queries = array(); 848 appendQueryNames($user, $queries); 849 $queries[] = C_inbox; // move remainder to inbox 850 foreach ($queries as $name) { 851 if ($name == C_inbox) { 852 $query = new stdClass(); 853 $query->t = ""; 854 $query->findin = "A"; 855 $query->datefrom = "B"; 856 $query->dateto = "E"; 857 $query->acctid = 0; 858 $doFilter = true; 859 $keepUnread = true; 860 } else { 861 $query = getQuery($user, $name); 862 $doFilter = isset($query->filter); 863 $keepUnread = isset($query->unread); 864 unset($query->unread); 865 unset($query->id); 866 } 867 $query->f = C_incoming; 868 if ($doFilter) { 869 $ids = getMsgIds($user, $query); 870 foreach ($ids as $id) { 871 if (!$keepUnread) addLabel($user, C_unread, $id, 'N'); 872 moveMsg($user, C_incoming, $name, $id); 873 } 874 } 875 } 876 } 877 } 878 879 880 // 881 // Reading contents from the database 882 // 883 884 function getParam($params, $key) { 885 // Given parameters from getMsgParts or getPart, lookup a parameter key 886 // Returns the value, or NULL if the key isn't present 887 if (!is_null($params)) { 888 $lines = explode("\n", $params); 889 $i = 0; 890 while ($i < count($lines)) { 891 if ($lines[$i] == $key) return $lines[$i+1]; 892 $i += 2; 893 } 894 } 895 return NULL; 896 } 897 898 function safeName($name) { 899 // Return safe name for an attachment: no executables 900 if (is_null($name)) return NULL; 901 return preg_replace('#\.exe|\.bat|\.vbs|\.pif|\.scr|\.shs|\.com#i', 902 '.bad', basename($name)); 903 } 904 905 function getPartName($part) { 906 // Return an appropriate name for $part, being a row from getParts 907 $name = getParam($part->parameters, "NAME"); 908 if (is_null($name)) $name = getParam($part->dparameters, "FILENAME"); 909 return safeName($name); 910 } 911 912 function getPartCharset($part) { 913 // Return the encoding of $part, being a row from getParts 914 $charset = strtoupper(getParam($part->parameters, "CHARSET")); 915 if (is_null($charset)) $charset = "US-ASCII"; 916 return $charset; 917 } 918 919 function getPartType($part) { 920 // Return a MIME type for $part, using mime.types if needed 921 if (isset($part->type)) { 922 $type = strtolower("$part->type/$part->subtype"); 923 } else { 924 $type = "application/octet-stream"; 925 } 926 if ($type == "application/octet-stream") { 927 $name = getPartName($part); 928 if (!is_null($name)) { 929 $ext = strrchr($name, '.'); 930 if ($ext) { 931 $ext = substr($ext, 1); 932 $mimeTypes = file_get_contents("/etc/mime.types"); 933 if ($mimeTypes) { 934 $matches = array(); 935 if (preg_match("%\n([^\n\s#]*)\s+$ext\n%i", $mimeTypes, 936 $matches)) { 937 $type = strtolower($matches[1]); 938 } 939 } 940 } 941 } 942 } 943 return $type; 944 } 945 946 function replaceAll($pattern, $replacement, $target) { 947 // Iterate replacement while it's making a difference 948 while (true) { 949 $new = preg_replace($pattern, $replacement, $target); 950 if ($new == $target) break; 951 $target = $new; 952 } 953 return $new; 954 } 955 956 function addTab($str, $width) { 957 // Add spaces equivalent to a tab at end of $str 958 return $str . str_repeat(" ", ($width-((strlen($str)-1) % $width))); 959 } 960 961 function fixupWhitespace($str, $width) { 962 // Returns $str after replacing all forms of newline with \n and 963 // all tabs with appropriate real spaces. 964 $str = preg_replace("#\r\n?#", "\n", $str); 965 $str = replaceAll("#(\n[^\t\n]*)\t#e", 966 "addTab(stripslashes('\\1'), $width)", $str); 967 return $str; 968 } 969 970 function getPartPathname($user, $partID) { 971 // Construct pathname for a part, given its part ID. 972 return C_partsDir . "/$user/" . $partID; 973 } 974 975 function getPartContent($user, $part) { 976 // Fetch the raw content of a given part from the database and any file. 977 $length = $part->length; 978 if ($length > 0) { 979 $pathname = getPartPathname($user, $part->part); 980 $content = file_get_contents($pathname); 981 $key = $part->filekey; 982 if (!is_null($key)) { // part file is encrypted 983 $mac = $part->mac; 984 if (is_null($mac)) die("unexpected null MAC"); 985 $plain = decryptContent($content, $key, C_partIV); 986 $newMac = macContent(($plain ? $plain : $content), $key); 987 // compute a MAC anyway, to avoid timing attacks 988 if (!$plain || $newMac != $mac) { 989 writeLog( 990 "user $user incorrect decryption of $part->part"); 991 $content = "Incorrect decryption: consult expert"; 992 } else { 993 writeLog("user $user decrypted $part->part"); 994 $content = $plain; 995 } 996 } 997 } else { 998 $content = $part->content; 999 } 1000 return $content; 1001 } 1002 1003 1004 // 1005 // Database operations for labels and folders 1006 // 1007 1008 // Every message is in one of three states, at all times: 1009 // 1010 // (a) has no labels at all, and is being created 1011 // (b) has at least one label, not including C_unread 1012 // (c) has no labels, and is ready for deletion 1013 // 1014 // Labels are adjusted by calls to "addLabel" or "moveMsg". 1015 // 1016 // Note that in a call of "addLabel" for a message in state (b), the message 1017 // has some other label, so transiently removing other occurences of the 1018 // given label is harmless. 1019 // 1020 // In "moveMsg", dest=="" moves a message with just one label to state (c) 1021 // 1022 // In "moveMsg", srce=="" is designed for moving to trash, but is not used 1023 // 1024 // TEMP: the "adding", "seq", and "udate" columns in the labels* tables are 1025 // historical, and should be abolished. The only reference to them is the 1026 // "insert" here, and create.txt 1027 1028 function addLabel($user, $label, $id, $adding = 'Y') { 1029 // Add or remove given label for message $id 1030 // 1031 global $readOnly; 1032 if ($readOnly) return; 1033 $changed = false; 1034 if ($adding == 'N') { 1035 $rc = doSqlQuery("remove $label for $id", 1036 "delete from labels$user " . 1037 "where id = $id and label = " . wrapSqlArg($label)); 1038 $changed |= (pachyAffectedRows() > 0); 1039 } 1040 if ($adding == 'Y') { 1041 $rc = doSqlQuery("insert label $label for $id", 1042 "insert ignore into labels$user values ( $id, " . 1043 wrapSqlArg($label) . ", 'Y', NULL, 0)"); 1044 $changed |= (pachyAffectedRows() > 0); 1045 } 1046 if ($label == C_unread && $changed) { 1047 $rc = doSqlQuery("modify unread flag for $id", 1048 "update msgs$user set unread = '$adding' where id = $id" ); 1049 } 1050 } 1051 1052 function moveMsg($user, $srce, $dest, $id) { 1053 // Add label $dest to $id and remove label $srce 1054 // If $srce == "", remove all labels except $dest and C_trash 1055 // If $dest == "", doesn't add, just removes - can leave an orphan 1056 // 1057 if ($id == 0) die("Moving message id 0"); 1058 if ($dest != "") addLabel($user, $dest, $id, 'Y'); 1059 if ($srce == "") { 1060 if ($dest == "" || $dest == C_trash) deleteCachedHit($user, $id); 1061 $rows = doSqlQuery("find labels to remove from $id", 1062 "select label from labels$user " . 1063 "where id = $id and label <> " . wrapSqlArg($dest) . 1064 " and label <> " . wrapSqlArg(C_trash) . 1065 " and label <> " . wrapSqlArg(C_unread)); 1066 while ($row = mysql_fetch_object($rows)) { 1067 addLabel($user, $row->label, $id, 'N'); 1068 } 1069 mysql_free_result($rows); 1070 } else if ($dest != $srce) { 1071 deleteCachedHit($user, $id); 1072 addLabel($user, $srce, $id, 'N'); 1073 } 1074 } 1075 1076 function getMsgLabels($user, $id) { 1077 // Return an array containing the labels of the given message 1078 $rows = doSqlQuery("find msg labels for $id", 1079 "select distinct label from labels$user " . 1080 "where id = $id and label != '" . C_unread . "' order by label"); 1081 $labels = array(); 1082 while ($row = mysql_fetch_object($rows)) $labels[] = $row->label; 1083 mysql_free_result($rows); 1084 return $labels; 1085 } 1086 1087 function testUnreadMsgsWithLabel($user, $dest) { 1088 // Return true iff there is at least one unread message labelled $dest 1089 // 1090 $msgs = doSqlQuery("test unread with label $dest", 1091 "select msgs$user.id " . 1092 "from msgs$user, labels$user " . 1093 "where msgs$user.id = labels$user.id " . 1094 "and (label = " . wrapSqlArg($dest) . ") " . 1095 "and unread = 'Y'" . 1096 "limit 1"); 1097 return (mysql_fetch_object($msgs) ? true : false); 1098 } 1099 1100 function countMsgsWithLabel($user, $dest, $limit = 0) { 1101 // Return the number of distinct messages having this label 1102 // Label might be for a folder, or C_unread, or C_incoming 1103 // Use $limit=1 to test for presence of messages with this label 1104 $rows = doSqlQuery("count msgs with label $dest", 1105 "select count(distinct id) as total from labels$user " . 1106 "where label = " . wrapSqlArg($dest) . " and id <> 0" . 1107 ($limit > 0 ? " limit $limit" : "")); 1108 $row = mysql_fetch_object($rows); 1109 return $row->total; 1110 } 1111 1112 function getFolders($user) { 1113 // Return the user's list of folders, kept as labels with id==0 1114 $list = doSqlQuery("get folder list", 1115 "select label from labels$user where id = 0 " . 1116 "order by label"); 1117 $folders = array(); 1118 while ($row = mysql_fetch_object($list)) $folders[] = $row->label; 1119 return $folders; 1120 } 1121 1122 function createFolder($user, $dest) { 1123 // Create a folder 1124 if (strlen($dest) > 40) $dest = substr($dest, 0, 40); 1125 addLabel($user, $dest, 0); 1126 $rc = doSqlQuery("forget query $dest", 1127 "delete from queries$user where name = " . wrapSqlArg($dest)); 1128 } 1129 1130 function moveAllMsgs($user, &$query, $dest) { 1131 // Move all messages matching $query to $dest 1132 // Silently suppresses silly cases: $dest == $query->f 1133 if ($dest != $query->f) { 1134 $ids = getMsgIds($user, $query); 1135 foreach ($ids as $id) { 1136 moveMsg($user, $query->f, $dest, $id); 1137 } 1138 } 1139 } 1140 1141 function copyAllMsgs($user, &$query, $dest, $adding = 'Y') { 1142 // Add label $dest to all messages matching $query 1143 $ids = getMsgIds($user, $query); 1144 foreach ($ids as $id) { 1145 addLabel($user, $dest, $id, $adding); 1146 } 1147 } 1148 1149 function deleteFolder($user, $srce, $dest) { 1150 // Delete folder $srce, moving all its messages into $dest 1151 if ($dest != $srce) { 1152 $query = new stdClass(); 1153 $query->f = $srce; 1154 $query->t = ""; 1155 $query->findin = "A"; 1156 $query->datefrom = "B"; 1157 $query->dateto = "E"; 1158 $query->acctid = 0; 1159 moveAllMsgs($user, $query, $dest); 1160 addLabel($user, $srce, 0, 'N'); 1161 } 1162 } 1163 1164 1165 // 1166 // The contacts database 1167 // 1168 1169 function enumContacts($user, $find, $selected = "*") { 1170 // Return contacts database filtered by "find" 1171 $wf = wrapSqlArg(trim($find)); 1172 return doSqlQuery("Get list of contacts", 1173 "select $selected from contacts$user " . 1174 ($find == "" ? "" : 1175 "where first = $wf or last = $wf or nickname = $wf " . 1176 "or concat(first, ' ', last) = $wf" . 1177 "or concat(first, '_', last) = $wf") . 1178 "order by first, last, nickname"); 1179 } 1180 1181 function countContacts($user, $find) { 1182 // Return count of contacts matching "find" 1183 $rows = enumContacts($user, $find, "count(*) as total"); 1184 $row = mysql_fetch_object($rows); 1185 return $row->total; 1186 } 1187 1188 function deleteContact($user, $id) { 1189 doSqlQuery("Delete contact", 1190 "delete from contacts$user where id = " . 1191 wrapSqlArg($id)); 1192 } 1193 1194 function getContactById($user, $id) { 1195 // Return fields of contact selected by ID, which is known to be unique 1196 $rows = doSqlQuery("get contact", 1197 "select * from contacts$user where id = " . wrapSqlArg($id)); 1198 return mysql_fetch_object($rows); 1199 } 1200 1201 function saveContact($user, $id, $contactArgs) { 1202 // Update contact database with new values, provided as globals (!) 1203 // $id = 0 for new contact creation 1204 // The arguments have been mysql-escaped 1205 doSqlQuery("Update contact", 1206 ($id != 0 ? "update" : "insert into") . 1207 " contacts$user set " . 1208 "first = " . wrapSqlArg($contactArgs["firstContact"]) . 1209 ", last = " . wrapSqlArg($contactArgs["lastContact"]) . 1210 ", nickname = " . wrapSqlArg($contactArgs["nicknameContact"]) . 1211 ", email = " . wrapSqlArg($contactArgs["emailContact"]) . 1212 ", address = " . wrapSqlArg($contactArgs["addressContact"]) . 1213 ", home = " . wrapSqlArg($contactArgs["homeContact"]) . 1214 ", work = " . wrapSqlArg($contactArgs["workContact"]) . 1215 ", mobile = " . wrapSqlArg($contactArgs["mobileContact"]) . 1216 ", udate = " . time() . 1217 ($id != 0 ? " where id = " . wrapSqlArg($id) : "") ); 1218 return ($id != 0 ? $id : pachyInsertID()); 1219 } 1220 1221 function addContactEmail($user, &$args, $prefix, &$res) { 1222 // For each contact id set in $args, add the email into $res 1223 // Tries to use the minimal abbreviation the will expand correctly 1224 while ( list($key, $value) = each($args) ) { 1225 $matches = array(); 1226 if (preg_match("#^$prefix([0-9]+)#", $key, $matches) > 0) { 1227 $row = getContactById($user, $matches[1]); 1228 $firstLast = "$row->first $row->last"; 1229 if (countContacts($user, $row->nickname) == 1) { 1230 $res[] = $row->nickname; 1231 } else if (countContacts($user, $row->first) == 1) { 1232 $res[] = $row->first; 1233 } else if (countContacts($user, $row->last) == 1) { 1234 $res[] = $row->last; 1235 } else if (countContacts($user, $firstLast) == 1) { 1236 $res[] = $firstLast; 1237 } else { 1238 $res[] = "$row->first $row->last <$row->email>"; 1239 } 1240 } 1241 } 1242 } 1243 1244 function expandContactList($user, &$args) { 1245 // Replace items in $args that aren't email addresses with entries 1246 // from contacts list, if possible. Since some contact names, e.g. 1247 // "first last" aren't syntactically valid RFC822 recipients, we 1248 // must expand them before doing an RFC822 parse. Similarly, some 1249 // RFC822 recipients have an imbedded comma, so we must ignore them 1250 // during contact expansion. 1251 $trimmed = trim($args); 1252 if ($trimmed != "") { 1253 $rawParse = explode(",", $trimmed); 1254 foreach ($rawParse as $value) { 1255 if (strpos($value, "@") === false) { 1256 // $value clearly isn't an RFC822 address 1257 $rows = enumContacts($user, $value); 1258 $count = mysql_num_rows($rows); 1259 if ($count == 0) { 1260 $expanded[] = $value; 1261 } else if ($count > 1) { 1262 return "Ambiguous contact name \"$value\""; 1263 } else { 1264 $row = mysql_fetch_object($rows); 1265 $expanded[] = "$row->first $row->last <$row->email>"; 1266 } 1267 } else { 1268 $expanded[] = $value; 1269 } 1270 } 1271 } 1272 if (isset($expanded)) { 1273 $fixed = implode(",", $expanded); 1274 $addrs = imap_rfc822_parse_adrlist($fixed, "PachyletContactList"); 1275 foreach ($addrs as $value) { 1276 if (!isset($value->host)) $value->host = ".SYNTAX-ERROR."; 1277 if ($value->host == ".SYNTAX-ERROR.") { 1278 $err = strtolower($value->mailbox); 1279 return "Syntax error in recipient list: $err"; 1280 } else if ($value->host == "PachyletContactList") { 1281 return "Unknown contact name \"$value->mailbox\""; 1282 } else { 1283 $found[] = imap_rfc822_write_address( 1284 $value->mailbox, $value->host, 1285 (isset($value->personal) ? $value->personal : "")); 1286 } 1287 } 1288 } 1289 if (false) { 1290 echo "args=" . htmlspecialchars($args) . "<br>"; 1291 echo "fixed=" . htmlspecialchars($fixed) . "<br>"; 1292 foreach ($addrs as $value) { 1293 echo "\"" . (isset($value->personal) ? $value->personal : "") . 1294 "\" &lt;$value->mailbox@$value->host&gt;<br>"; 1295 } 1296 if (isset($found)) { 1297 echo "res=" . htmlspecialchars(implode(", ", $found)) . "<br>"; 1298 } 1299 return "testing"; 1300 } 1301 $args = (isset($found) ? implode(", ", $found) : ""); 1302 return NULL; 1303 } 1304 1305 function expandContacts($user, &$to, &$cc) { 1306 // Update $to and $cc by expanding contact shortcuts. 1307 // Return NULL on success, failed shortcut on failure 1308 $res = expandContactList($user, $to); 1309 if (is_null($res)) $res = expandContactList($user, $cc); 1310 return $res; 1311 } 1312 1313 1314 // 1315 // Displaying message dates 1316 // 1317 1318 putenv("TZ=US/Eastern"); // server timezone, primarily for DST 1319 1320 function userZoneDST($displaytz) { 1321 // Returns user's timezone allowing for current DST (as used by server) 1322 // Element 8 of localtime() is the server's DST boolean 1323 $local = localtime(); 1324 return $displaytz + ($local[8] ? 60 : 0); 1325 } 1326 1327 function userTime($displaytz, $utime) { 1328 // Adjusts Unix timestamp to user's local time, allowing for server DST 1329 return $utime + userZoneDST($displaytz)*60; 1330 } 1331 1332 function formatUserTime($format, $displaytz, $utime) { 1333 // Return text form of given Unix timestamp as seen in $displaytz zone 1334 return htmlspecialchars(gmdate($format, userTime($displaytz, $utime))); 1335 } 1336 1337 1338 // 1339 // Consistency checking, deletion, and garbage collection 1340 // 1341 1342 function countOrphanMsgs($user) { 1343 // Count messages in database with no labels 1344 $rows = doSqlQuery("count orphan msgs", 1345 "select count(*) as total from msgs$user left join labels$user" . 1346 " on msgs$user.id = labels$user.id and label <> '" . C_unread . 1347 "' where labels$user.id is null"); 1348 $row = mysql_fetch_object($rows); 1349 return $row->total; 1350 } 1351 1352 function countOrphanParts($user) { 1353 // Count parts in database with no message 1354 $rows = doSqlQuery("count orphan parts", 1355 "select count(*) as total from parts$user left join msgs$user" . 1356 " using (id) where msgs$user.id is null"); 1357 $row = mysql_fetch_object($rows); 1358 return $row->total; 1359 } 1360 1361 function getOrphanPartFiles($user) { 1362 // Return array with pathname of files having no part record in database 1363 $rows = doSqlQuery("enum parts", 1364 "select part from parts$user"); 1365 $parts = array(); 1366 while ($row = mysql_fetch_object($rows)) $parts[$row->part] = 0; 1367 $dirEnum = opendir(C_partsDir . "/$user"); 1368 $partFiles = array(); 1369 if ($dirEnum) { 1370 while ($entry = readdir($dirEnum)) { 1371 if (preg_match('/^[0-9]+$/', $entry) && 1372 !isset($parts[$entry])) { 1373 $partFiles[] = getPartPathname($user, $entry); 1374 } 1375 } 1376 closedir($dirEnum); 1377 } 1378 return $partFiles; 1379 } 1380 1381 function countOrphanPartFiles($user) { 1382 // Count files having no part record in database 1383 $partFiles = getOrphanPartFiles($user); 1384 return count($partFiles); 1385 } 1386 1387 function deletePartFile($path) { 1388 // Delete part file, and its "new" variant 1389 // 1390 $pathNew = "$path-new"; 1391 if (file_exists($pathNew)) unlink($pathNew); 1392 if (file_exists($path)) unlink($path); 1393 } 1394 1395 function deletePart($user, $partID) { 1396 // Delete part, and any underlying file 1397 doSqlQuery("delete part", 1398 "delete from parts$user where part = $partID"); 1399 $path = getPartPathname($user, $partID); 1400 deletePartFile($path); 1401 } 1402 1403 function expungeOrphans($user) { 1404 // Delete orphans from the database, leaving label entries alone 1405 $startTime = microtime(); 1406 $msgs = 0; 1407 $rows = doSqlQuery("find orphan msgs", 1408 "select msgs$user.id from msgs$user left join labels$user" . 1409 " on msgs$user.id = labels$user.id and label <> '" . C_unread . 1410 "' where labels$user.id is null"); 1411 while ($row = mysql_fetch_object($rows)) { 1412 doSqlQuery("delete orphan msg", 1413 "delete from msgs$user where id = $row->id"); 1414 $msgs++; 1415 } 1416 $parts = 0; 1417 $rows = doSqlQuery("find orphan parts", 1418 "select part from parts$user left join msgs$user" . 1419 " using (id) where msgs$user.id is null"); 1420 while ($row = mysql_fetch_object($rows)) { 1421 deletePart($user, $row->part); 1422 $parts++; 1423 } 1424 $partFiles = getOrphanPartFiles($user); 1425 foreach ($partFiles as $path) deletePartFile($path); 1426 $fCount = count($partFiles); 1427 $labels = 0; 1428 $rows = doSqlQuery("find orphan labels", 1429 "select label, labels$user.id from labels$user left join msgs$user" . 1430 " on msgs$user.id = labels$user.id where msgs$user.id is null" . 1431 " and labels$user.id <> 0"); 1432 while ($row = mysql_fetch_object($rows)) { 1433 doSqlQuery("delete orphan label", 1434 "delete from labels$user where label = " . 1435 wrapSqlArg($row->label) . " and id = $row->id"); 1436 $labels++; 1437 } 1438 // writeLog("user $user expunge " . elapsed($startTime) . " secs"); 1439 if ($msgs + $parts + $fCount + $labels > 0) { 1440 writeLog("user $user expunged" . 1441 ($msgs > 0 ? " $msgs messages" : "") . 1442 ($parts > 0 ? " $parts parts" : "") . 1443 ($fCount > 0 ? " $fCount part files" : "") . 1444 ($labels > 0 ? " $labels labels" : "") 1445 ); 1446 } 1447 } 1448 1449 function countBadDates($user) { 1450 $rows = doSqlQuery("count bad dates", 1451 "select count(*) as total from msgs$user where udate < 0"); 1452 $row = mysql_fetch_object($rows); 1453 return $row->total; 1454 } 1455 1456 1457 // 1458 // Message header parsing 1459 // 1460 1461 function fixAndParseDate($date) { 1462 // Fix up some incorrect date formats that I've seen 1463 $date = preg_replace('# *\(.*\) *$#', "", $date); 1464 $date = preg_replace('#"?GMT"?#i', '+0000', $date); 1465 $date = preg_replace('#"?EST"?#i', '-0500', $date); 1466 $date = preg_replace('#"?UT"?$#i', '+0000', $date); 1467 $date = preg_replace('# ([0-9])\\.([0-9]*( |$))#i', 1468 ' -0\\1\\2', $date); 1469 $date = preg_replace('# ((0[0-9]|1[0-3])[0-5][0-9])#i', 1470 ' -\\1', $date); 1471 $date = preg_replace('#([0-9]*:[0-9]*:[0-9]*) ([+-][0-9]*) '. 1472 '([0-9]*)#i', 1473 '\\3 \\1 \\2', $date); 1474 $res = strtotime($date); 1475 if ($res == false || $res == -1) $res = time(); 1476 return $res; 1477 } 1478 1479 function cleanUpAddrs($list) { 1480 // Excise invalid addresses from a recipient list 1481 $addrs = imap_rfc822_parse_adrlist($list, C_localDomain); 1482 $res = array(); 1483 foreach ($addrs as $value) { 1484 if (isset($value->host) && isset($value->mailbox) && 1485 $value->host != ".SYNTAX-ERROR.") { 1486 $res[] = trim(imap_rfc822_write_address( 1487 $value->mailbox, $value->host, 1488 (isset($value->personal) ? $value->personal : ""))); 1489 } 1490 } 1491 return implode(", ", $res); 1492 } 1493 1494 function parseAndCleanUpHeader($rawHeader) { 1495 // Excise invalid addresses from the to/cc lines 1496 $hdr = imap_rfc822_parse_headers($rawHeader); 1497 if (isset($hdr->toaddress)) { 1498 $hdr->toaddress = cleanUpAddrs($hdr->toaddress); 1499 } 1500 if (isset($hdr->ccaddress)) { 1501 $hdr->ccaddress = cleanUpAddrs($hdr->ccaddress); 1502 } 1503 return $hdr; 1504 } 1505 1506 function parseHeaderFixingDate(&$rawHeader) { 1507 $hdr = parseAndCleanUpHeader($rawHeader); 1508 if (isset($hdr->date)) { 1509 $hdr->udate = fixAndParseDate($hdr->date); 1510 } 1511 return $hdr; 1512 } 1513 1514 function insertMsgToCC($user, $id, $rawHeader) { 1515 // Insert msgto and msgcc columns, by parsing the header 1516 $hdr = parseAndCleanUpHeader($rawHeader); 1517 doSqlQuery("insert msgto", 1518 "update msgs$user set" . 1519 " msgto = " . 1520 wrapSqlArg((isset($hdr->toaddress) ? $hdr->toaddress : "")) . 1521 ", msgcc = " . 1522 wrapSqlArg((isset($hdr->ccaddress) ? $hdr->ccaddress : "")) . 1523 " where id = $id"); 1524 } 1525 1526 function getAndParseHeader($user, &$part, &$mimeKludge) { 1527 // If given part (a row from getMsgParts) is the message header, 1528 // read the raw header from the database and parse it and return it; 1529 // otherwise return false. 1530 // $mimeKludge patches up messages with content-type and no mime-version 1531 $mimeKludge = false; 1532 if ($part->type != "message" || $part->subtype != "RFC822") { 1533 return false; 1534 } else { 1535 $rawHeader = getPartContent($user, getPart($user, $part->part)); 1536 $mimeKludge = 1537 (preg_match('#\ncontent-type *: *text/html *[;\n\r]#i', 1538 $rawHeader) > 0); 1539 $hdr = parseHeaderFixingDate($rawHeader); 1540 $myHdr = new stdClass(); 1541 $myHdr->udate = 1542 (isset($hdr->udate)? $hdr->udate : false); 1543 $myHdr->subject = 1544 (isset($hdr->subject)? $hdr->subject : "");; 1545 $myHdr->msgfrom = 1546 (isset($hdr->fromaddress)? $hdr->fromaddress : ""); 1547 $myHdr->msgto = 1548 (isset($hdr->toaddress)? $hdr->toaddress : ""); 1549 $myHdr->msgcc = 1550 (isset($hdr->ccaddress)? $hdr->ccaddress : ""); 1551 $myHdr->sender = 1552 (isset($hdr->senderaddress)? $hdr->senderaddress : ""); 1553 $myHdr->replyto = 1554 (isset($hdr->reply_toaddress)? $hdr->reply_toaddress : ""); 1555 return $myHdr; 1556 } 1557 } 1558 1559 1560 // 1561 // HTML-related subroutines common between the HTML and XML interfaces 1562 // 1563 1564 function htmlFromUtf8($str, $isHtml) { 1565 // Convert UTF-8 encoded $str to ASCII with escaped HTML entities 1566 // See RFC 3629 for definition of UTF-8 1567 // 1568 // Iff $isHtml, then ASCII entities have already been escaped as needed 1569 // 1570 // The code enforces the range limits for multi-byte encodings, as 1571 // required by the RFC. 1572 // 1573 $len = strlen($str); 1574 $res = ""; 1575 $j = 0; 1576 for ($i = 0; $i < $len; $i++) { 1577 if (($c0 = ord($str[$i]) - 128) < 0) { 1578 // single byte: accumulate into ASCII fragment starting at $j 1579 } else { 1580 // multi-byte 1581 $frag = substr($str, $j, $i-$j); 1582 $res .= ($isHtml ? $frag : htmlspecialchars($frag)); 1583 unset($c); 1584 if (($c0-=64) < 0) { 1585 // stray 10xxxxxx: skip 1586 } else if ($i+1 >= $len || ($c1 = ord($str[$i+1]) - 128) < 0 || 1587 $c1 >= 64) { 1588 // missing second byte: skip 1589 } else if ($c0 < 32) { 1590 // 110xxxxx: 2-byte 1591 $c = $c0*64 + $c1; 1592 $i++; 1593 if ($c < 0x80) unset($c); 1594 } else if ($i+2 >= $len || ($c2 = ord($str[$i+2]) - 128) < 0 || 1595 $c2 >= 64) { 1596 // missing third byte: skip 1597 } else if (($c0-=32) < 16) { 1598 // 1110xxxx: 3-byte 1599 $c = ($c0*64 + $c1)*64 + $c2; 1600 $i+=2; 1601 if ($c < 0x800) unset($c); 1602 } else if ($i+3 >= $len || ($c3 = ord($str[$i+3]) - 128) < 0 || 1603 $c3 >= 64) { 1604 // missing fourth byte: skip 1605 } else if (($c0-=16) < 8) { 1606 // 11110xxx: 4-byte 1607 $c = (($c0*64 + $c1)*64 + $c2)*64 + $c3; 1608 $i+=3; 1609 if ($c < 0x10000 || $c > 0x10FFFF) unset($c); 1610 } else { 1611 // illegal 11111xxx: skip 1612 } 1613 if (isset($c)) $res .= "&#$c;"; 1614 $j = $i+1; 1615 } 1616 } 1617 $frag = ($j == 0 ? $str : substr($str, $j, $i-$j)); 1618 $res .= ($isHtml ? $frag : htmlspecialchars($frag)); 1619 return $res; 1620 } 1621 1622 function utf8FromCharset($str, $charset) { 1623 // Convert given string from given charset to UTF-8 1624 // 1625 $charset = strtoupper($charset); 1626 if ($charset == "ISO8859-1") $charset = "ISO-8859-1"; 1627 if ($charset == "UTF-8") { 1628 return $str; 1629 } else if (preg_match('#^ISO-8859-([1-9]|(1[0345]))$#i', $charset) || 1630 preg_match('#^Windows-125[12]$#i', $charset)) { 1631 return mb_convert_encoding($str, "UTF-8", $charset); 1632 } else { // Unknown charset: treat as ISO-8859-1 1633 return utf8_encode($str); 1634 } 1635 } 1636 1637 function htmlFromCharset($str, $charset, $isHtml) { 1638 // Convert given string from given charset to ASCII HTML, with escaped 1639 // HTML entities. 1640 // 1641 // Iff $isHtml, then ASCII entities have already been escaped as needed 1642 // 1643 return htmlFromUtf8(utf8FromCharset($str, $charset), $isHtml); 1644 } 1645 1646 function stripcrlf($s) { 1647 // Remove CR and LF to prevent illegal header item content 1648 return strtr($s, "\r\n", " "); 1649 } 1650 1651 function rfc1342($str) { 1652 // Return $str formatted according to RFC 1342 1653 $str = stripcrlf($str); 1654 $uStr = strtr($str, " ", "_"); 1655 $encodedStr = imap_8bit($uStr); 1656 $qpStrTest = preg_replace("#=3D#i", "=", $encodedStr); 1657 if ($qpStrTest != $uStr) { 1658 $str = $encodedStr; 1659 $str = preg_replace("#\\?#", "=3F", $str); 1660 $str = preg_replace("#=\r\n#", 1661 "?=\r\n =?UTF-8?Q?", 1662 "=?UTF-8?Q?$encodedStr?="); 1663 } 1664 return $str; 1665 } 1666 1667 function utf8FromRfc1342($str, $max) { 1668 // Given a string containing RFC 1342 escapes, convert to Unicode. 1669 // Truncate the Unicode to $max characters. 1670 // Unescaped non-ASCII is treated as ISO-8859-1 1671 // 1672 $res = ""; 1673 $matches = array(); 1674 while (preg_match( 1675 '#^(([^=]|=[^\\?])*)=\\?([^\\?]*)\\?([^\\?])' . 1676 '\\?([^\\?]*)\\?= ?(.*)$#', 1677 $str, $matches)) { 1678 $pre = substr($matches[1], 0, $max); 1679 $res .= utf8FromCharset($pre, "ISO-8859-1", false); 1680 $max -= strlen($pre); 1681 $charset = $matches[3]; 1682 $encoding = strtoupper($matches[4]); 1683 $body = $matches[5]; 1684 if ($encoding == "Q") { 1685 $body = preg_replace('#_#', '=20', $body); 1686 $body = imap_qprint($body); 1687 } else if ($encoding == "B") { 1688 $body = imap_base64($body); 1689 } 1690 $body = substr($body, 0, $max); 1691 $res .= utf8FromCharset($body, $charset, false); 1692 $max -= strlen($body); 1693 $str = $matches[6]; 1694 $matches = array(); 1695 } 1696 return $res . 1697 utf8FromCharset(substr($str, 0, $max), "ISO-8859-1", false); 1698 } 1699 1700 function htmlFromRfc1342($str, $max) { 1701 // Given a string containing RFC 1342 escapes, convert to Unicode 1702 // then escape HTML entities to yield an ASCII-encoded piece of HTML. 1703 // 1704 return htmlFromUtf8(utf8FromRfc1342($str, $max), false); 1705 } 1706 1707 function buildTocLines($user, $msgs) { 1708 // Return an array of useful strings for TOC lines 1709 $accts = getAccts($user); 1710 $acct0 = $accts[0]; 1711 $displaytz = $acct0->displaytz; 1712 if (mysql_num_rows($msgs) > 0) mysql_data_seek($msgs, 0); 1713 $tocLines = array(); 1714 while ($msg = mysql_fetch_object($msgs)) { 1715 $thisLine = new stdClass(); 1716 $thisLine->id = $msg->id; 1717 if ($msg->udate == -1) { 1718 $dateStr = "&nbsp; &nbsp;(no date)"; 1719 } else { 1720 $dateTime = userTime($displaytz, $msg->udate); 1721 $now = userTime($displaytz, time()); 1722 $nowDay = floor($now / (24*60*60)); 1723 $msgDay = floor($dateTime / (24*60*60)); 1724 $dateStr = formatUserTime( 1725 ($msgDay >= $nowDay - 5 && $msgDay <= $nowDay ? "g:i a D" : 1726 "j M Y"), 1727 $displaytz, $msg->udate); 1728 $dateStr = str_pad($dateStr, 12, " ", STR_PAD_LEFT); 1729 if ($dateStr[0] == " ") { 1730 $dateStr = "&nbsp;" . substr($dateStr, 1, 11); 1731 } 1732 } 1733 $thisLine->date = $dateStr; 1734 $from = $msg->msgfrom; 1735 $fromAddrs = imap_rfc822_parse_adrlist($from, C_localDomain); 1736 foreach ($fromAddrs as $fromAddr) { 1737 if (!isset($fromAddr->host)) $fromAddr->host = "none"; 1738 if (!isset($fromAddr->mailbox)) $fromAddr->mailbox = "none"; 1739 $thisFrom = "$fromAddr->mailbox@$fromAddr->host"; 1740 foreach ($accts as $acct) { 1741 if ($thisFrom == $acct->msgfrom) { 1742 $from = ""; 1743 break; 1744 } 1745 } 1746 } 1747 if ($from == "") $from = "To: $msg->msgto"; 1748 $from = str_pad($from, 18, " "); 1749 $from = htmlFromRfc1342($from, 18); 1750 $from = preg_replace('# #', ' &nbsp;', $from); 1751 $thisLine->from = $from; 1752 $thisLine->subject = htmlFromRfc1342($msg->subject, 54); 1753 $thisLine->unread = $msg->unread; 1754 $tocLines[] = $thisLine; 1755 } 1756 return $tocLines; 1757 } 1758 1759 function startMsgParts(&$parts, &$partNo, &$children) { 1760 // Subroutine for showMsg 1761 mysql_data_seek($parts, 0); 1762 $partNo = -1; 1763 $children = 1; 1764 } 1765 1766 function nextMsgPart(&$parts, &$partNo, &$children) { 1767 // Subroutine for showMsg 1768 $part = mysql_fetch_object($parts); 1769 if (!$part) die("Missing part"); 1770 $partNo++; 1771 $children--; 1772 return $part; 1773 } 1774 1775 function renderableItem($part) { 1776 // Return true if we're willing to render the part inline 1777 if ($part->type != "text") return false; 1778 return ($part->subtype == "PLAIN" || $part->subtype == "HTML"); 1779 } 1780 1781 function scanMsgItem(&$classes, &$parts, &$partNo, &$children, 1782 $context) { 1783 // Scan a message item and its children recursively, classifying them. 1784 // "best" renderable item has its number assigned to $classes["best"] 1785 // Each item has $classes[$partNo] set to one of: 1786 // M = top-level message header 1787 // N = ignore 1788 // A = "best" or an alternative 1789 // Y = attachment 1790 // 1791 // Argument $context tells about parentage: 1792 // M = top-level entry 1793 // N = child of non-multi, i.e. part of an imbedded message 1794 // A = multi/alternative allowing "best" 1795 // B = multi/alternative not allowing "best" 1796 // X = multi/anythingElse allowing "best" 1797 // Y = multi/anythingElse not allowing "best" 1798 // 1799 $part = nextMsgPart($parts, $partNo, $children); 1800 $multi = ($part->type == "multipart"); 1801 $renderable = renderableItem($part); 1802 $classes[$partNo] = ( $multi || $context == "N" ? "N" : 1803 ($context == "A" || $context == "B" || $context == "X" ? 1804 ($renderable ? "A" : "Y") : 1805 "Y")); 1806 if ($context != "N") { 1807 if ($context != "Y" && $context != "B" && $renderable) { 1808 $classes["best"] = $partNo; 1809 } 1810 // Now choose a class for this item's children 1811 if ($multi && $part->subtype == "ALTERNATIVE") { 1812 $context = ($context == "Y" || $context == "B" ? "B" : "A"); 1813 } else if ($multi || $context == "M") { 1814 $context = ($context == "Y" || $context == "B" ? "Y" : "X"); 1815 } else { 1816 $context = "N"; 1817 } 1818 } 1819 1820 // Scan this item's children 1821 // For class == "X" we switch to "Y" after the first child, 1822 // because that's what we want for multipart other than alternative. 1823 // For class == "A" (multipart/alternative), we prefer later children 1824 $myChildren = $part->children; 1825 while ($myChildren > 0) { 1826 scanMsgItem($classes, $parts, $partNo, $myChildren, $context); 1827 if ($context == "X") $context = "Y"; 1828 } 1829 } 1830 1831 function putByteLength($length) { 1832 // Put item size neatly 1833 echo ($length < 1024+512 ? "$length Bytes" : 1834 ($length < 1048576+1048576/2 ? round($length/1024)." KBytes" : 1835 round($length/1048576)." MBytes")); 1836 } 1837 1838 function getContentAsAsciiHtml($user, $part, $isHtml) { 1839 // Return part's contents, as ASCII-encoded HTML, converting as 1840 // appropriate from part's charset. 1841 // 1842 $content = getPartContent($user, $part); 1843 return htmlFromCharset($content, getPartCharset($part), $isHtml); 1844 } 1845 1846 function getSafeContent($user, $part, $imbedded) { 1847 // Fetch contents of given part, which is text/html, sanitizing HTML 1848 // contents. 1849 // 1850 // $imbedded disables tags that we don't allow when imbedded in main 1851 // result page. Tags that produce active content (e.g. "script") are 1852 // always disabled, as are script-handling attributes such as "onclick". 1853 // 1854 $content = getContentAsAsciiHtml($user, $part, true); 1855 1856 // Active content tags, and iframe 1857 // 1858 $content = preg_replace( 1859 '#(<\s*/?\s*)(script|iframe|meta|object|embed|applet)#i', 1860 '\1p-\2', $content); 1861 1862 // "on*" script attributes 1863 // 1864 $content = replaceAll( 1865 '#(<[^>]*\s)(on[a-z0-9]*)#i', 1866 '\1q-\2', $content); 1867 1868 if ($imbedded) { 1869 // Container tags that don't make sense in imbedded body 1870 // 1871 $content = preg_replace( 1872 '#<\s*(title|style)[^>]*>[^<]*<\s*/\s*\1[^>]*>#i', 1873 '<r-\1>', $content); 1874 1875 // Non-container tags that don't make sense in imbedded body 1876 // 1877 $content = preg_replace( 1878 '#(<\s*/?\s*)(html|head|title|base|body|link|style|input)#i', 1879 '\1s-\2', $content); 1880 1881 // Attributes that fetch content from a foreign server 1882 // 1883 $content = replaceAll( 1884 '#(<[^>]*\s)' . 1885 '((src|lowsrc|background)\s*=\s*)(?!"?cid)#i', 1886 '\1t-\2', 1887 $content); 1888 1889 // URL references from within style attributes 1890 // 1891 $content = replaceAll( 1892 '#(<[^>]*:\s?)url#i', 1893 '\1u-url', 1894 $content); 1895 } 1896 return $content; 1897 } 1898 1899 function getTextContent($user, $partID) { 1900 // Fetch contents of given partID, which is text/plain, and fix up 1901 // for use within an HTML page 1902 // 1903 $part = getPart($user, $partID); 1904 $content = getContentAsAsciiHtml($user, $part, false); 1905 1906 // Strip initial and final blank lines 1907 // 1908 $content = preg_replace("#^[ \n\r\t]*(\n|\r)#", "", $content); 1909 $content = preg_replace("#(\n|\r)[ \n\r\t]*\$#", '\1', $content); 1910 1911 // Clean up newlines and replace tabs with spaces 1912 // 1913 $content = fixupWhitespace($content, 8); 1914 1915 // Convert obvious URL's into hyperlinks 1916 // 1917 $content = preg_replace( 1918 ',((http|https|ftp|telnet):' . 1919 '//([-\w?=;/#+$!~@:.\\,%]|&amp;)*' . 1920 '([-\w?=;/#+$!~]|&amp;)),i', 1921 '<a href="\1">\1</a>', $content); 1922 1923 // Convert to HTML that wraps nicely; 1924 // short lines are unaffected, long lines wrap at window edge 1925 // 1926 $content = preg_replace("#\n#", "<br>\n", $content); 1927 $content = preg_replace("#\n #", "\n&nbsp;", $content); 1928 $content = preg_replace("# #", "&nbsp; ", $content); 1929 1930 // The following fixes up for odd-length runs of spaces 1931 // 1932 $content = preg_replace("# #", "&nbsp; ", $content); 1933 1934 return "<div class=fixedButWrap>$content</div>"; 1935 } 1936 1937 function uudecode($encode) { 1938 // Decode the contents of a uuencoded file. 1939 // Assumes that the "begin" and "end" lines have been stripped 1940 // Implemented by converting to base-64, then decoding that 1941 $b64chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" . 1942 "0123456789+/A"; 1943 $uuchars = str_pad("", 65); 1944 for ($i = 0; $i <= 64; $i++) $uuchars[$i] = chr(32+$i); 1945 $encode = preg_replace("/^.|\r|\n.?/","",$encode); 1946 $encode = strtr($encode, $uuchars, $b64chars); 1947 $encode = str_pad($encode, floor((strlen($encode)+3) / 4) * 4, "="); 1948 return base64_decode($encode); 1949 } 1950 1951 function getRawContentWithUUDecode($user, $part) { 1952 // Get the raw content and if needed remove UUencoding. 1953 $content = getPartContent($user, $part); 1954 if ($part->encoding == "OTHER") { 1955 $matches = array(); 1956 if (preg_match('#begin [^\n]*\r?\n(.*)\r?\nend\r?\n#s', 1957 $content, $matches) > 0) { 1958 $content = uudecode($matches[1]); 1959 } 1960 } 1961 return $content; 1962 } 1963 1964 // Unused: hmac-based pachyparts authentication 1965 // 1966 function getPartHmac($user, $partID, $time) { 1967 // Return an HMAC signed by the part's secret 1968 // 1969 $rows = doSqlQuery("get part secret", 1970 "select secret from parts$user where part = $partID"); 1971 $row = mysql_fetch_object($rows); 1972 if (!$row) return "0"; 1973 $secret = $row->secret; 1974 if ($secret == null) { 1975 $secret = getRandomString(16); 1976 doSqlQuery("set part secret", 1977 "update parts$user set secret = " . wrapSqlArg($secret) . 1978 " where part = $partID"); 1979 } 1980 return base64_encode(hash_hmac("sha256", 1981 "Pachyparts\x00$user\x00$partID\x00$time", $secret, true)); 1982 } 1983 1984 // Unused: hmac-based pachyparts authentication 1985 // 1986 function getPartURLHmac($user, $partID) { 1987 // Return a URL suitable for viewing the part in a browser 1988 // 1989 // The URL is a capability, authenticated by an HMAC signed by 1990 // the part's "secret" column. 1991 // 1992 $now = time(); 1993 $hmac = getPartHmac($user, $partID, $now); 1994 return "pachyparts.php?user=$user&part=$partID&time=$now&hmac=" . 1995 rawurlencode($hmac); 1996 } 1997 1998 function getPartURL($user, $partID) { 1999 // Return a URL suitable for viewing the part in a browser. 2000 // 2001 // Access will be authenticated by the deriver key cookie. 2002 // 2003 return "pachyparts.php?user=$user&part=$partID"; 2004 } 2005 2006 function cidImg($user, $id, $pre, $cid, $post) { 2007 // Return HTML with HTTP or HTTPS URL replacing CID URL $cid 2008 $parts = doSqlQuery("get cid image part", 2009 "select part from parts$user where id = $id and contentid = " . 2010 wrapSqlArg("<$cid>") . " limit 1"); 2011 $part = mysql_fetch_object($parts); 2012 if ($part) { 2013 return "$pre\"" . getPartURL($user, $part->part) . "\"$post"; 2014 } else { 2015 return "[IMAGE]"; 2016 } 2017 } 2018 2019 function getHtmlContent($user, $partID, $imbedded) { 2020 // Fetch contents of given part (a database row from getPart), 2021 // sanitizing HTML contents. $imbedded disables tags that we don't 2022 // allow when imbedded in main result page. Tags that produce active 2023 // content (e.g. "script") are always disabled, as are script-handling 2024 // attributes such as "onclick". 2025 // 2026 // Translates "cid:" image URL's into appropriate pachylet URL's 2027 // 2028 $part = getPart($user, $partID); 2029 $content = getSafeContent($user, $part, $imbedded); 2030 $content = preg_replace( 2031 '#(<\s*(img|input|body|table|tr|td|th)[^>]*\s' . 2032 '(src|background)\s*=\s*)"?cid:([^" >]*)"?([^>]*>)#ei', 2033 "cidImg(\$user, \$part->id, '\\1', '\\4', '\\5')", 2034 $content); 2035 return $content; 2036 } 2037 2038 ?>
End of listing