Source of “pachylib.php”.
1049 lines, 34 KBytes.   Last modified 8:13 am, 19th April 2017 PDT.
1 <?php // Emacs settings: -*- mode: Fundamental; tab-width: 4; -*- 2 3 //////////////////////////////////////////////////////////////////////////// 4 // // 5 // Pachylet: Andrew's Web Mail Interface // 6 // // 7 // Copyright (c) 2002-2004 // 8 // // 9 // See http://birrell.org/pachylet/help.html // 10 // // 11 // Non-HTML code for inc, sync, getRawMsg, compose (etc.), send, modify // 12 // // 13 //////////////////////////////////////////////////////////////////////////// 14 15 16 // 17 // RC4, for keeping mail server passwords in the database 18 // From Bruce Schneier, Applied Cryptography, second edition, section 17.1 19 // 20 21 function keyRC4($key) { 22 // Set the RC4 globals to start an encrypt/decrypt stream with this key 23 global $rc4s, $rc4i, $rc4j; 24 $length = strlen($key); 25 for ($i = 0; $i < 256; $i++) $rc4s[$i] = $i; 26 $j = 0; 27 for ($i = 0; $i < 256; $i++) { 28 $j = ($j + $rc4s[$i] + ord($key[$i % $length])) % 256; 29 $t = $rc4s[$i]; $rc4s[$i] = $rc4s[$j]; $rc4s[$j] = $t; 30 } 31 $rc4i = 0; 32 $rc4j = 0; 33 } 34 35 function cryptRC4($str) { 36 // Encrypt or decrypt $str using previously set RC4 key 37 global $rc4s, $rc4i, $rc4j; 38 $length = strlen($str); 39 for ($c = 0; $c < $length; $c++) { 40 $rc4i = ($rc4i + 1) % 256; 41 $rc4j = ($rc4j + $rc4s[$rc4i]) % 256; 42 $t = $rc4s[$rc4i]; $rc4s[$rc4i] = $rc4s[$rc4j]; $rc4s[$rc4j] = $t; 43 $k = $rc4s[($rc4s[$rc4i] + $rc4s[$rc4j]) % 256]; 44 $str[$c] = chr(ord($str[$c]) ^ $k); 45 } 46 return $str; 47 } 48 49 function keyRC4withIV($iv, $pwd) { 50 // See Fluhrer, Mantin & Shamir "Weaknesses in the key scheduling 51 // algorithm of RC4" (Proc. SAC 2001), and Mironov "(Not So) Random 52 // Shuffles of RC4" (Proc. CRYPTO'02). 53 // To compensate for those weaknesses, we use MD5 to intermingle the 54 // password and IV, and we discard the first 16*256 output bytes. 55 global $rc4s, $rc4i, $rc4j; 56 keyRC4(md5("$iv.$pwd")); 57 for ($c = 0; $c < 16*256; $c++) { 58 // discard one byte of RC4 output; takes roughly 4 usec per byte 59 $rc4i = ($rc4i + 1) % 256; 60 $rc4j = ($rc4j + $rc4s[$rc4i]) % 256; 61 $t = $rc4s[$rc4i]; $rc4s[$rc4i] = $rc4s[$rc4j]; $rc4s[$rc4j] = $t; 62 } 63 } 64 65 66 // 67 // Managing the account database 68 // 69 70 function enumUsers() { 71 // Return array of user names 72 $rows = doSqlQuery("Get list of users", 73 "show tables like 'accts%'"); 74 $users = array(); 75 while ($row = mysql_fetch_row($rows)) { 76 $users[] = preg_replace("#^accts#", "", $row[0]); 77 } 78 return $users; 79 } 80 81 function newAcct($user) { 82 // Add a new account for the user 83 $acct = getAcct($user); 84 $rc = doSqlQuery("Create account", 85 "insert into accts$user set user = " . wrapSqlArg($acct->user) . 86 ", pwd = " . wrapSqlArg($acct->pwd) . 87 ", pwdiv = " . wrapSqlArg($acct->pwdiv) . 88 ", server = 'unset'" . 89 ", type = " . wrapSqlArg($acct->type) . 90 ", dodelete = " . wrapSqlArg($acct->dodelete) . 91 ", msgfrom = " . wrapSqlArg($acct->msgfrom) . 92 ", person = " . wrapSqlArg($acct->person) . 93 ", displaytz = $acct->displaytz" . 94 ", udate = " . time()); 95 return ($rc ? pachyInsertID() : 0); 96 } 97 98 function forgetAcct($user, $id) { 99 // Forget given account 100 $rc = doSqlQuery("Delete account $id", 101 "delete from accts$user where acctid = " . wrapSqlArg($id)); 102 } 103 104 function setSettings($user, $dk, $h2, $displaytz, $newPwd) { 105 // Update misc settings for $user, possibly including a new Pachylet 106 // password, with resulting re-encryption of mail server passwords and 107 // resulting new derived key. Returns the new (or unchanged) derived 108 // key, for use in a cookie. 109 // 110 $displaytz += 0; // force to integer 111 $now = time(); 112 if ($newPwd == "") { 113 // Keep Pachylet password unchanged 114 $rc = doSqlQuery("Modify settings for $user", 115 "update accts$user set " . 116 "displaytz = " . wrapSqlArg($displaytz) . ", " . 117 "udate = $now" ); 118 return null; 119 } else { 120 // Change everything 121 $newDk = setDerivedKey($user, $dk, $newPwd); 122 $newH2 = getMysqlLoginKey($newDk); 123 $rc = doSqlQuery("Change password for $user", 124 "set password for $user = password(".wrapSqlArg($newH2).")" ); 125 // Re-encrypt mail server password(s) 126 $rows = doSqlQuery("Get password for re-encryption", 127 "select acctid, pwd, pwdiv from accts$user"); 128 while ($row = mysql_fetch_object($rows)) { 129 keyRC4withIV($row->pwdiv, $h2); 130 $oldpwd = cryptRC4($row->pwd); 131 $iv = getRandomString(16); 132 keyRC4withIV($iv, $newH2); 133 $cryptpwd = cryptRC4($oldpwd); 134 $rc = doSqlQuery("Re-encrypt server pwd for $user", 135 "update accts$user " . 136 "set pwd = " . wrapSqlArg($cryptpwd) . ", " . 137 "pwdiv = " . wrapSqlArg($iv) . ", " . 138 "displaytz = " . wrapSqlArg($displaytz) . ", " . 139 "udate = $now " . 140 "where acctid = '$row->acctid'" ); 141 } 142 return $newDk; 143 } 144 } 145 146 function setAcct($user, $h2, $acct) { 147 // Update a specific account for $user 148 if ($acct->pwd != "") { 149 $iv = getRandomString(16); 150 // TODO: this should be user h2, not root h2 (EJB, 4/19/17) 151 keyRC4withIV($iv, $h2); 152 $cryptpwd = cryptRC4($acct->pwd); 153 } 154 $rc = doSqlQuery("Modify account details for $user ($acct->acctid)", 155 "update accts$user set user = " . wrapSqlArg($acct->user) . 156 (isset($cryptpwd) ? ", pwd = " . wrapSqlArg($cryptpwd) . 157 ", pwdiv=" . wrapSqlArg($iv) : "") . 158 ", server = " . wrapSqlArg($acct->server) . 159 ", dodelete = " . wrapSqlArg($acct->dodelete) . 160 ", msgfrom = " . wrapSqlArg($acct->msgfrom) . 161 ", person = " . wrapSqlArg($acct->person) . 162 ", type = " . wrapSqlArg($acct->type) . 163 ", udate = " . time() . 164 " where acctid = " . wrapSqlArg($acct->acctid) ); 165 } 166 167 168 // 169 // Inserting new message details and parts 170 // 171 172 function insertMsgDetails($user, &$hdr) { 173 // Insert a new row in msgs$user, and return the new message's ID 174 // Returns -id if the message was a duplicateof message with that id 175 // $hdr is an object with the following fields: 176 // ->id ... desired id, or unset to use auto-increment 177 // ->msgid ... RFC 822 message ID text 178 // ->udate ... RFC 822 date/time as a Unix 1-second timestamp 179 // ->subject 180 // ->msgfrom 181 // ->msgto, unset to use null 182 // ->msgcc, unset to use null 183 // ->unread ... "unread" flag as "Y" or "N" 184 if ($user == 'andrew') { 185 if (!isset($hdr->udate)) die("udate not set"); 186 if (is_null($hdr->udate)) die("udate null"); 187 if ($hdr->udate == "") die("udate empty"); 188 } 189 $md5 = md5("$hdr->msgid\n$hdr->udate\n$hdr->subject\n$hdr->msgfrom"); 190 $md5 = pack("H*", $md5); 191 while (true) { 192 $rc = doSqlQuery("insert message", 193 "insert ignore into msgs$user set " . 194 "id = " . (isset($hdr->id) ? $hdr->id : "NULL") . ", " . 195 "acctid = $hdr->acctid, " . 196 "md5 = " . wrapSqlArg($md5) . ", " . 197 "msgid = " . wrapSqlArg($hdr->msgid) . ", " . 198 "udate = " . (0+$hdr->udate) . ", " . 199 "subject = " . wrapSqlArg($hdr->subject) . ", " . 200 "msgfrom = " . wrapSqlArg($hdr->msgfrom) . ", " . 201 "msgto = " . (isset($hdr->msgto) ? wrapSqlArg($hdr->msgto) : 202 "null") . ", " . 203 "msgcc = " . (isset($hdr->msgcc) ? wrapSqlArg($hdr->msgcc) : 204 "null") . ", " . 205 "unread = " . wrapSqlArg($hdr->unread) ); 206 if (pachyAffectedRows() > 0) return pachyInsertID(); 207 208 // If the initial insertion detects a duplicate md5 column, 209 // and if the relevant id column has no labels, we delete the 210 // pre-existing message details row, on the grounds that the 211 // message creation must have failed. I.e., during "inc" 212 // adding the label is the commit point, and we never remove 213 // all labels from a message. 214 $rows = doSqlQuery("find duplicate message's id", 215 "select id from msgs$user where md5 = " . 216 wrapSqlArg($md5) . " limit 1"); 217 $row = mysql_fetch_object($rows); 218 if (!$row) die("Can't find id for partial duplicate, id=" . 219 (isset($hdr->id) ? $hdr->id : "NULL") . 220 ", md5=" . wrapSqlArg($md5)); 221 $id = $row->id; 222 $rows = doSqlQuery("check if duplicate msg has any labels", 223 "select label from labels$user where id = $id limit 1"); 224 if (mysql_num_rows($rows) > 0) return -$id; // committed duplicate 225 $rc = doSqlQuery("deleting incomplete message details", 226 "delete from msgs$user where id = $id"); 227 // Any parts for $id are left as orphans, for later clean-up. 228 unset($id); 229 } 230 } 231 232 function writePartFile($user, $partID, $content, $baseKey) { 233 // If appropriate, write the content into the part file, encrypted. 234 // Updates the part's row to have the appropriate key and MAC. 235 // If the file already exists (saveDraft), does a dance to allow 236 // recovery in case of failure at an inconvenient time. 237 // 238 // The encryption uses a key derived from $baseKey, and a fixed IV 239 // 240 if (strlen($content) > C_maxDBLength) { 241 $key = getHashedKey("pachyPartFile-$user-$partID", $baseKey); 242 $cipher = encryptContent($content, $key, C_partIV); 243 $mac = macContent($content, $key); 244 $filename = getPartPathname($user, $partID); 245 $updating = file_exists($filename); 246 $wFile = ($updating ? "$filename-new" : $filename); 247 if (file_exists($wFile)) { 248 writeLog("user $user $wFile already exists"); 249 die("Part file already exists; consult expert"); 250 } 251 $rc = file_put_contents($wFile, $cipher); 252 if ($rc === false) { 253 writeLog("user $user failed to write $wFile"); 254 die("Failed to write part file $wFile; consult expert"); 255 } 256 $rc = doSqlQuery("update key and mac for $user $partID", 257 "update parts$user set " . 258 "filekey = " . wrapSqlArg($key) . ", " . 259 "mac = " . wrapSqlArg($mac) . 260 " where part = $partID"); 261 // we get here only on success 262 if ($updating) { 263 $rc = rename($wFile, $filename); 264 if (!$rc) { 265 writeLog("user $user failed to rename $wFile"); 266 die("Failed to rename $wFile; consult expert"); 267 } 268 writeLog("user $user re-encrypted $partID"); 269 } else { 270 writeLog("user $user encrypted $partID"); 271 } 272 } 273 } 274 275 function insertPart($user, $h2, $details) { 276 // Insert given part into database; large contents are kept as files 277 // $details is an object with the following fields: 278 // ->id ... the id for the message, from insertMsgDetails 279 // ->imapnum ... the IMAP part number of this part 280 // ->type ... the MIME media type, as a string 281 // ->subtype ... the MIME subtype, as a string 282 // ->encoding ... the original content transfer encoding 283 // ->description ... content-description 284 // ->contentid ... content-id 285 // ->disposition ... content-disposition 286 // ->dparameters ... content-disposition options 287 // ->parameters ... content-type options 288 // ->children ... number of children for a multi-part item, or 0 289 // ->content ... the content, after undoing any transfer encoding 290 // Fields id and content are required. 291 // Other fields are optional, and if missing will be NULL in database. 292 // Base64 and Quoted-printable encoding hve been removed from content. 293 if (isset($details->part)) { 294 writeLog("user $user unexpected details->part"); 295 die("Unexpected details part"); 296 } 297 $imapnum = (isset($details->imapnum) ? 298 wrapSqlArg($details->imapnum) : "0"); 299 $type = (isset($details->type) ? 300 wrapSqlArg($details->type) : "NULL"); 301 $subtype = (isset($details->subtype) ? 302 wrapSqlArg($details->subtype) : "NULL"); 303 $encoding = (isset($details->encoding) ? 304 wrapSqlArg($details->encoding) : "NULL"); 305 $description = (isset($details->description) ? 306 wrapSqlArg($details->description) : "NULL"); 307 $contentid = (isset($details->contentid) ? 308 wrapSqlArg($details->contentid) : "NULL"); 309 $disposition = (isset($details->disposition) ? 310 wrapSqlArg($details->disposition) : "NULL"); 311 $dParams = (isset($details->dparameters) ? 312 wrapParams($details->dparameters) : "NULL"); 313 $params = (isset($details->parameters) ? 314 wrapParams($details->parameters) : "NULL"); 315 $length = strlen($details->content); 316 $rc = doSqlQuery("insert part into $details->id", 317 "insert into parts$user set " . 318 "id = $details->id, " . 319 "part = " . 320 (isset($details->part) ? $details->part : "NULL") . ", " . 321 "imapnum = $imapnum, " . 322 "type = $type, " . 323 "subtype = $subtype, " . 324 "encoding = $encoding, " . 325 "description = $description, " . 326 "contentid = $contentid, " . 327 "disposition = $disposition, " . 328 "dparameters = $dParams, " . 329 "parameters = $params, " . 330 "children = " . 331 (isset($details->children) ? $details->children : "0") . ", " . 332 "length = " . 333 ($length > C_maxDBLength ? $length : -$length) . ", " . 334 "content = " . 335 wrapSqlArg($length > C_maxDBLength ? 336 // Store prefix of textual big content, for indexing 337 (isset($details->type) && 338 ($details->type=="audio" || $details->type=="image") ? 339 "AV" : 340 substr($details->content, 0, C_maxDBLength)) : 341 $details->content) 342 ); 343 $partID = pachyInsertID(); 344 writePartFile($user, $partID, $details->content, $h2); 345 return $partID; 346 } 347 348 function insertUploadedPart($user, $h2, $id, $att) { 349 // Insert as a part the uploaded file given by $att 350 $details = new stdClass(); 351 $details->id = $id; 352 if (isset($att->description)) $details->description = $att->description; 353 $details->content = file_get_contents($att->localPath); 354 $tParts = explode('/', $att->type); 355 $details->type = $tParts[0]; 356 if (isset($tParts[1])) $details->subtype = $tParts[1]; 357 $name = new stdClass(); 358 $name->attribute = "NAME"; 359 $name->value = $att->name; 360 $parameters[] = $name; 361 $details->parameters = $parameters; 362 $dname = new stdClass(); 363 $dname->attribute = "FILENAME"; 364 $dname->value = $att->name; 365 $dparameters[] = $dname; 366 $details->dparameters = $dparameters; 367 return insertPart($user, $h2, $details); 368 } 369 370 371 // 372 // Incorporating new mail 373 // 374 375 function mimeType($type) { 376 switch ($type) { 377 case 0: return "text"; 378 case 1: return "multipart"; 379 case 2: return "message"; 380 case 3: return "application"; 381 case 4: return "audio"; 382 case 5: return "image"; 383 case 6: return "video"; 384 default: return "other"; 385 } 386 } 387 388 function mimeEncoding($encoding) { 389 switch ($encoding) { 390 case 0: return "7BIT"; 391 case 1: return "8BIT"; 392 case 2: return "BINARY"; 393 case 3: return "BASE64"; 394 case 4: return "QP"; 395 default: return "OTHER"; 396 } 397 } 398 399 function incParts($user, $h2, $mailbox, $msg, $id, &$structure, 400 $part, $prefix) { 401 // insert all parts of the message into database, by recursive treewalk 402 // $part is part number for this part's content 403 // $prefix is prefix for children's content, if multi 404 405 if ($prefix == "") { 406 // Record top-level message header 407 $hDetails = new stdClass(); 408 $hDetails->id = $id; 409 $hDetails->type = "message"; 410 $hDetails->subtype = "RFC822"; 411 $hDetails->encoding = "7BIT"; 412 $hDetails->children = 1; 413 $hDetails->imapnum = "0"; 414 $hDetails->content = 415 imap_fetchbody($mailbox, $msg, $hDetails->imapnum); 416 insertPart($user, $h2, $hDetails); 417 insertMsgToCC($user, $id, $hDetails->content); 418 } 419 420 $details = new stdClass(); 421 $details->id = $id; 422 423 if (!$structure) { 424 echo "No body ($user)\n"; 425 $details->type = "text"; 426 $details->subtype = "PLAIN"; 427 $details->imapnum = $part; 428 $details->content = "No message body\n"; 429 $details->children = 0; 430 } else { 431 if (isset($structure->type)) { 432 $details->type = mimeType($structure->type); 433 } else { 434 $details->type = "text"; 435 $details->subtype = "PLAIN"; 436 } 437 if ($structure->ifsubtype) { 438 $details->subtype = $structure->subtype; 439 } 440 if (isset($structure->encoding)) { 441 $details->encoding = mimeEncoding($structure->encoding); 442 } 443 if ($structure->ifdescription) { 444 $details->description = $structure->description; 445 } 446 if ($structure->ifid) { 447 $details->contentid = $structure->id; 448 } 449 if ($structure->ifdisposition) { 450 $details->disposition = $structure->disposition; 451 } 452 if ($structure->ifdparameters) { 453 $details->dparameters = &$structure->dparameters; 454 } 455 if ($structure->ifparameters) { 456 $details->parameters = &$structure->parameters; 457 } 458 $details->children = (isset($structure->parts) ? 459 count($structure->parts) : 0); 460 $isMsg = ($details->type == "message" && 461 isset($details->subtype) && $details->subtype == "RFC822"); 462 $isMulti = ($details->type == "multipart"); 463 $details->imapnum = ($isMsg ? "${prefix}0" : $part); 464 $details->content = ($isMulti ? "" : 465 imap_fetchbody($mailbox, $msg, $details->imapnum)); 466 } 467 468 if (isset($details->encoding)) { 469 if ($details->encoding == "QP") { 470 $details->content = imap_qprint($details->content); 471 } else if ($details->encoding == "BASE64") { 472 $details->content = imap_base64($details->content); 473 } 474 } 475 insertPart($user, $h2, $details); 476 477 for ( $i = 0; $i < $details->children; $i++) { 478 $child = $structure->parts[$i]; 479 // Children of a multi have their body numbered "$prefix$i", 480 // or are multi and their children have bodies "$prefix$i.$j" 481 // The header of a message (top-level or imbedded) is ${prefix}0 482 // The body of a non-multi message is numbered ${prefix}1. 483 // However, a top-level message has no structure level for 484 // it's children, whereas an imbedded message has; hence the special 485 // case below. 486 $newPart = $prefix . ($i+1); 487 $newPrefix = $newPart . "."; 488 if ($isMsg) $newPrefix = $prefix; 489 incParts($user, $h2, $mailbox, $msg, $id, $child, $newPart, 490 $newPrefix); 491 } 492 } 493 494 function connectMailServer($acct) { 495 // Open connection to POP/IMAP server 496 // 497 if ($acct->type == "SEND") return false; 498 $port = ($acct->type == "POP3" ? "110/pop3" : 499 ($acct->type == "SPOP" ? "995/pop3/ssl" : 500 ($acct->type == "SIMAP" ? "993/imap/ssl" : "143/imap"))); 501 $matches = array(); 502 if (preg_match('#^([^/ ]*)/(.*)$#', $acct->server, $matches)) { 503 $server = $matches[1]; 504 $folder = $matches[2]; 505 } else { 506 $server = $acct->server; 507 $folder = "INBOX"; 508 } 509 //echo "server:{$server}, port:{$port}, folder: {$folder},user: {$acct-> user}, password: {$acct->pwd}"; 510 $mailbox = imap_open("{"."$server:$port/novalidate-cert}$folder", 511 $acct->user, $acct->pwd); 512 return $mailbox; 513 } 514 515 function inc($user, $h2) { 516 // Incorporate new mail from the mail server, applying user's filters. 517 // 518 // The commit point for incorporating a message is when it has the 519 // C_incoming label added. Before then, on failure it will be discarded 520 // as an orphan by a later call of "expungeOrphans". After adding the 521 // label, a subsequent re-incorporationg will be discarded as a 522 // duplicate. 523 // 524 global $readOnly; 525 if ($readOnly) return; 526 $accts = getAccts($user, $h2); 527 setCachedQuery($user, NULL); 528 set_time_limit(0); 529 lockDB($user); 530 expungeOrphans($user); 531 unlockDB($user); 532 533 // work-around for PHP bug #33039 ("mailbox is empty" notice) 534 error_reporting(E_ALL ^ E_NOTICE); 535 536 $foundNewMail = false; 537 538 foreach ($accts as $acct) { 539 $oldErr = set_error_handler("ignoreError"); 540 $mailbox = connectMailServer($acct); 541 set_error_handler($oldErr); 542 if (!$mailbox) { 543 if ($acct->type != "SEND") { 544 writeLog("user $user inc failed to $acct->server " . 545 "via $acct->type"); 546 } 547 } else { 548 $mCount = imap_num_msg($mailbox); 549 for ( $i = 0; $i < $mCount; $i++ ) { 550 $overview = imap_fetch_overview($mailbox, $i+1); 551 $hdr = $overview[0]; 552 $msgno = $hdr->msgno; 553 $myHdr = new stdClass(); 554 $myHdr->acctid = $acct->acctid; 555 $myHdr->msgid = // Message-id from the RFC 822 header 556 (isset($hdr->message_id) ? $hdr->message_id : ""); 557 $myHdr->udate = 558 (isset($hdr->date) ? strtotime($hdr->date) : 0); 559 if ($myHdr->udate === false || $myHdr->udate == -1) { 560 $myHdr->udate = fixAndParseDate($hdr->date); 561 } 562 $myHdr->subject = 563 (isset($hdr->subject) ? $hdr->subject : ""); 564 $myHdr->msgfrom = 565 (isset($hdr->from) ? $hdr->from : ""); 566 $myHdr->unread = "Y"; // ignored by later addLabel C_unread 567 lockDB($user); 568 $id = insertMsgDetails($user, $myHdr); 569 if ($id > 0) { 570 // the message isn't a duplicate 571 $body = imap_fetchstructure($mailbox, $msgno); 572 incParts($user, $h2, $mailbox, $msgno, $id, $body, 1, 573 ""); 574 addLabel($user, C_unread, $id); // also sets msgs.unread 575 addLabel($user, C_incoming, $id); 576 $foundNewMail = true; 577 } else { 578 // Alleged duplicate. For the paranoid, verify this! 579 // We could compare message details, or the header. 580 // I choose just comparing the details. 581 $oldDetails = getMsgDetails($user, -$id); 582 if ($oldDetails->msgid != $myHdr->msgid || 583 $oldDetails->udate != $myHdr->udate || 584 $oldDetails->subject != $myHdr->subject || 585 $oldDetails->msgfrom != $myHdr->msgfrom) { 586 die("False duplicate, id = " . (-$id) . 587 ", msgno = $msgno"); 588 } 589 } 590 unlockDB($user); 591 // If $acct->dodelete, delete, even if it's a duplicate 592 if ($acct->dodelete=='Y' && 593 !imap_delete($mailbox, $msgno)) { 594 die("imap_delete failed"); 595 } 596 } 597 598 if (!imap_expunge($mailbox)) die("imap_expunge failed"); 599 imap_close($mailbox); 600 if ($mCount > 0) { 601 writeLog("user $user inc $mCount from $acct->server"); 602 } 603 } 604 } 605 if ($foundNewMail) { 606 exec(C_indexer . " --quiet --rotate ${user}Delta"); 607 sleep(3); 608 applyInboxFilter($user); 609 } 610 } 611 612 613 // 614 // Message re-assembly 615 // 616 617 function getRawParams($params) { 618 // Given parameters from getMsgParts or getPart, reassembled into 619 // MIME format 620 $res = ""; 621 if (!is_null($params)) { 622 $lines = explode("\n", $params); 623 $i = 0; 624 while ($i < count($lines)) { 625 $res = $res . ";\r\n " . 626 $lines[$i] . "=\"" . $lines[$i+1] . "\""; 627 $i += 2; 628 } 629 } 630 return $res . "\r\n"; 631 } 632 633 function getRawMsgItem($user, &$parts, &$partNo, &$children, $type) { 634 // Return text of this message item, including its children if multi. 635 // Encodes the item appropriately, and includes separators for multi. 636 $part = nextMsgPart($parts, $partNo, $children); 637 $part = getPart($user, $part->part); 638 $boundary = getParam($part->parameters, "BOUNDARY"); 639 $multi = ($part->type == "multipart"); 640 $msg = ($part->type == "message" && $part->subtype == "RFC822"); 641 $content = getPartContent($user, $part); 642 if ($multi && $content == "") { 643 $content = "This is a multi-part message in MIME format,\r\n" . 644 "reconstructed by Pachylet\r\n"; 645 } 646 if (!is_null($part->encoding)) { 647 if ($part->encoding == "BASE64") { 648 $content = imap_binary($content); 649 $encoding = "base64"; 650 } else if ($part->encoding == "QP") { 651 $content = imap_8bit($content); 652 $encoding = "quoted-printable"; 653 } else { 654 $encoding = $part->encoding; 655 } 656 } 657 $res = ""; 658 if ($type) { 659 $res = $res . "Content-type: $part->type/$part->subtype"; 660 $res = $res . getRawParams($part->parameters); 661 if (isset($encoding)) { 662 $res = $res . "Content-transfer-encoding: $encoding\r\n"; 663 } 664 if (!is_null($part->description)) { 665 $res = $res . "Content-description: $part->description\r\n"; 666 } 667 if (!is_null($part->contentid)) { 668 $res = $res . "Content-ID: $part->contentid\r\n"; 669 } 670 if (!is_null($part->disposition)) { 671 $res = $res . "Content-disposition: $part->disposition"; 672 $res = $res . getRawParams($part->dparameters); 673 } 674 $res = $res . "\r\n"; 675 } 676 $res = $res . $content; 677 678 // Scan this item's children 679 $myChildren = $part->children; 680 while ($myChildren > 0) { 681 if ($multi) $res = $res . "\r\n--$boundary\r\n"; 682 $res = $res . 683 getRawMsgItem($user, $parts, $partNo, $myChildren, !$msg); 684 } 685 if ($multi) $res = $res . "\r\n--$boundary--\r\n"; 686 return $res; 687 } 688 689 function getRawMsg($user, $id, $startAt = 0) { 690 // Return the text of this message, reassembled and suitably encoded 691 $parts = getMsgParts($user, $id, $startAt); 692 startMsgParts($parts, $partNo, $children); 693 return getRawMsgItem($user, $parts, $partNo, $children, false); 694 } 695 696 697 // 698 // Message composition and sending 699 // 700 701 function mergeRecipients($fromList, $toList, $ccList, $alreadyTo) { 702 // Return an RFC 822 recipient list formed from the union of 703 // the given lists, excluding duplicates and names in $alreadyTo 704 if ($fromList != "") $everyone[] = $fromList; 705 if ($toList != "") $everyone[] = $toList; 706 if ($ccList != "") $everyone[] = $ccList; 707 if (isset($everyone[0])) { 708 // Parse the combined recipient list 709 $parsed = imap_rfc822_parse_adrlist( 710 implode(",", $everyone), C_localDomain); 711 while (list($key, $val) = each($parsed)) { 712 $addrs[$val->mailbox . "@" . $val->host] = $val; 713 } 714 // Subtract "alreadyTo" (which might = $fromList, BTW) 715 if ($alreadyTo != "") { 716 $parsed = imap_rfc822_parse_adrlist( 717 $alreadyTo, C_localDomain); 718 while (list($key,$val)=each($parsed)) { 719 $thisAddr = $val->mailbox . "@" . $val->host; 720 if (isset($addrs[$thisAddr])) unset($addrs[$thisAddr]); 721 } 722 } 723 // Construct the cc list 724 while (list($key, $val) = each($addrs)) { 725 $ccArray[] = imap_rfc822_write_address($val->mailbox, 726 $val->host, 727 (isset($val->personal) ? $val->personal : "")); 728 } 729 } 730 return (isset($ccArray[0]) ? implode(", ", $ccArray) : ""); 731 } 732 733 function buildDraft($user, $h2, $basedOn, $op) { 734 // Construct the fields for a new draft message and return its id. 735 // $op == "compose", "forward", "reply", or "replyAll" 736 // $basedOn is currently selected message, or 0 737 // 738 // The draft's body part is text/plain, and later code assumes it is 739 // either US-ASCII or UTF-8, though it has no explicit charset param. 740 // 741 $draft = new stdClass(); 742 $draft->acctid = 0; 743 $draft->msgto = ""; 744 $draft->msgcc = ""; 745 $draft->subject = ""; 746 $draft->body = ""; 747 $draft->attachMsg = "none"; 748 $draft->attachParts = array(); 749 if ($basedOn > 0) { 750 $details = getMsgDetails($user, $basedOn); 751 $acct = getAcct($user, NULL, $details->acctid); 752 } else { 753 $acct = getAcct($user); 754 } 755 $draft->msgfrom = "$acct->person <$acct->msgfrom>"; 756 if ($op != "compose") { 757 $bases = explode(",", $basedOn); 758 $parts = getMsgParts($user, $bases[0]); 759 $hdrPart = mysql_fetch_object($parts); 760 $mimeKludge = false; 761 $hdr = getAndParseHeader($user, $hdrPart, $mimeKludge); 762 if ($hdr) { 763 $draft->subject = 764 ($op=="forward" ? "Fwd: " : "Re: ") . 765 replaceAll('#^ *(fwd|fw|re): ?#i', "", $hdr->subject); 766 if (count($bases) > 1) $draft->subject .= " (etc)"; 767 if ($op != "forward") { 768 $draft->msgto = 769 ($hdr->replyto != "" ? $hdr->replyto : $hdr->msgfrom); 770 } 771 if ($op == "replyAll") { 772 $draft->msgcc = mergeRecipients($hdr->msgfrom, 773 $hdr->msgto, $hdr->msgcc, $draft->msgto); 774 } 775 } // if no header, plough ahead as for op=="send" 776 777 if ($op == "forward") $draft->attachMsg = $basedOn; 778 779 foreach ($bases as $base) { 780 if (!isset($parts)) $parts = getMsgParts($user, $base); 781 while ($part = mysql_fetch_object($parts)) { 782 if ($part->type=="text" && $part->subtype == "PLAIN") { 783 $part = getPart($user, $part->part); 784 $content = ""; 785 if ($hdr) { 786 $content .= "From: $hdr->msgfrom\n" . 787 "Subject: " . 788 utf8FromRfc1342($hdr->subject, 54) . 789 "\n" . 790 "Date: " . 791 date("r", $hdr->udate) . "\n" . 792 "To: $hdr->msgto\n" . 793 "Cc: $hdr->msgcc\n\n"; 794 } 795 $content .= utf8FromCharset( 796 getPartContent($user, $part), 797 getPartCharset($part)); 798 $content = preg_replace("#\r\n#", "\n", $content); 799 $draft->body .= "\n"; 800 if ($op == "forward") { 801 $draft->body .= "\n----- " . 802 "Text from the forwarded message " . 803 "(the entire message is attached) -----\n\n"; 804 } else { 805 $content = preg_replace("#^|\n#", "\n> ",$content); 806 } 807 $draft->body .= $content; 808 break; 809 } 810 } 811 unset($parts); 812 } 813 } 814 $draft->msgid = microtime(); 815 $draft->udate = time(); 816 $draft->unread = "N"; 817 $draftId = insertMsgDetails($user, $draft); 818 if ($draftId < 0) die("Failed to save draft ($draftId)"); 819 $details = new stdClass(); 820 $details->id = $draftId; 821 $details->content = $draft->body; 822 $details->type = "text"; 823 $details->subtype = "PLAIN"; 824 insertPart($user, $h2, $details); 825 $details = new stdClass(); 826 $details->id = $draftId; 827 $details->content = $draft->attachMsg; 828 $details->type = "pachydraft"; 829 $details->subtype = "attachmsg"; 830 insertPart($user, $h2, $details); 831 addLabel($user, C_unsent, $draftId, "Y"); 832 setCachedQuery($user, null); 833 writeLog("user $user created draft #$draftId"); 834 return $draftId; 835 } 836 837 function saveDraft($user, $id, $from, $to, $cc, $subject, $body) { 838 // Update an existing draft 839 // 840 // Arguments are in UTF-8 841 // 842 $subject = rfc1342($subject); 843 $subject = preg_replace("#\r\n#", "", $subject); 844 doSqlQuery("modify draft", 845 "update msgs$user set" . 846 " udate = " . time() . 847 ", subject = " . wrapSqlArg($subject) . 848 ", msgfrom = " . wrapSqlArg($from) . 849 ", msgto = " . wrapSqlArg($to) . 850 ", msgcc = " . wrapSqlArg($cc) . 851 " where id = $id"); 852 $rows = doSqlQuery("find draft body part", 853 "select part, filekey from parts$user where id = $id" . 854 " order by part limit 1"); 855 if (!($row = mysql_fetch_object($rows))) die("No message body"); 856 $length = strlen($body); 857 doSqlQuery("update draft body part", 858 "update parts$user set content = " . wrapSqlArg($body) . 859 ", length = " . ($length > C_maxDBLength ? $length : -$length) . 860 " where part = $row->part"); 861 writePartFile($user, $row->part, $body, $row->filekey); 862 if (count(getMsgLabels($user, $id)) == 0) { 863 // Re-saving a deleted draft 864 addLabel($user, C_unsent, $id, "Y"); 865 } 866 setCachedQuery($user, null); 867 } 868 869 function getDraft($user, $id) { 870 // Extract existing draft from the database, including attachments 871 // 872 // Fields in result are in UTF-8, with RFC 1342 undone. 873 // 874 if (count(getMsgLabels($user, $id)) == 0) return null; // deleted 875 $rows = doSqlQuery("get draft", 876 "select id, udate, subject, msgfrom, msgto, msgcc from msgs$user" . 877 " where id = $id limit 1"); 878 if (!($draft = mysql_fetch_object($rows))) return null; // expunged 879 $draft->subject = utf8FromRfc1342($draft->subject, 999999); 880 $rows = doSqlQuery("get draft parts", 881 "select part, filekey, mac, dparameters, parameters, length, " . 882 "content " . 883 "from parts$user where id = $id order by part"); 884 if (!($row = mysql_fetch_object($rows))) die("No message body"); 885 $draft->body = getPartContent($user, $row); 886 if (!($row = mysql_fetch_object($rows))) die("No attachmsg"); 887 $draft->attachMsg = ($row->content == "0" ? "none" : $row->content); 888 $draft->attachParts = array(); 889 while ($row = mysql_fetch_object($rows)) { 890 $att = new stdClass(); 891 $att->part = $row->part; 892 $att->length = ($row->length < 0 ? -$row->length : $row->length); 893 $att->name = getPartName($row); 894 $draft->attachParts[] = $att; 895 } 896 return $draft; 897 } 898 899 function getDraftPartNumbers($user, $id) { 900 // Return an array with the draft's part numbers, for "forget" 901 $parts = array(); 902 $rows = doSqlQuery("get draft part numbers", 903 "select part from parts$user where id = $id"); 904 while ($row = mysql_fetch_object($rows)) $parts[] = $row->part; 905 return $parts; 906 } 907 908 function deleteDraftInner($user, $draftId) { 909 moveMsg($user, C_unsent, "", $draftId); 910 setCachedQuery($user, null); 911 } 912 913 function deleteDraft($user, $draftId) { 914 deleteDraftInner($user, $draftId); 915 writeLog("user $user discarded draft #$draftId"); 916 } 917 918 function sendmail($user, $msgfrom, $headers, $body) { 919 // Transmit given message to sendmail 920 // Include resent-from if needed 921 $fromEmail = stripcrlf( 922 preg_replace('#^.*<(.*)>$#', '\1', $msgfrom)); 923 $primary = getAcct($user); 924 $fp = popen(C_sendmail . " -i -t '-f$primary->msgfrom'", "w"); 925 if ($fromEmail != $primary->msgfrom) { 926 fputs($fp, "Sender: $primary->person <$primary->msgfrom>\r\n"); 927 } 928 fputs($fp, $headers); 929 fputs($fp, "\r\n"); 930 fputs($fp, $body); 931 fputs($fp, "\r\n"); 932 pclose($fp); 933 } 934 935 function resend($user, $to, $content) { 936 // Send given RFC822 content to given recipient list 937 // Returns count of recipients 938 $parsed = imap_rfc822_parse_adrlist($to, C_localDomain); 939 $count = 0; 940 while (list($key, $val) = each($parsed)) { 941 if (isset($val->mailbox) && isset($val->host)) { 942 $addrs[] = $val->mailbox . "@" . $val->host; 943 $count++; 944 } 945 } 946 if ($count > 0) { 947 $primary = getAcct($user); 948 $fp = popen(C_sendmail . " -i '-f$primary->msgfrom' " . 949 implode(" ", $addrs), "w"); 950 fputs($fp, $content); 951 pclose($fp); 952 } 953 return $count; 954 } 955 956 function send($user, $draftId) { 957 // Send the message that we've been composing 958 // Returns null on success, non-null on expand failure 959 $draft = getDraft($user, $draftId); 960 $msgto = $draft->msgto; $msgcc = $draft->msgcc; 961 $sendError = expandContacts($user, $msgto, $msgcc); 962 if (!is_null($sendError)) return $sendError; 963 $subject = rfc1342($draft->subject); 964 $headers = "From: " . stripcrlf($draft->msgfrom) . 965 "\r\nTo: " . stripcrlf($msgto) . 966 "\r\nCc: " . stripcrlf($msgcc) . 967 "\r\nBcc: " . stripcrlf($draft->msgfrom) . 968 "\r\nSubject: $subject" . 969 "\r\nX-mailer: " . stripcrlf(C_program . ", version " . C_version) . 970 "\r\nMIME-version: 1.0\r\n"; 971 // Consider encoding the body 972 $encodedBody = imap_8bit($draft->body); 973 $qpTest = preg_replace("#=3D#i", "=", $encodedBody); 974 if ($qpTest != $draft->body) { 975 // QP replaced more than just "=", so we need it 976 $body = $encodedBody; 977 $bodyEncoding = "quoted-printable"; 978 $bodyCharset = "UTF-8"; 979 } else { 980 $body = $draft->body; 981 $bodyEncoding = "7bit"; 982 $bodyCharset = "US-ASCII"; 983 } 984 985 $boundary = "The-MIME=Boundary"; // illegal base-64 and QP 986 $attachments = array(); 987 if ($draft->attachMsg != "none") { 988 $attachMsgs = explode(",", $draft->attachMsg); 989 foreach ($attachMsgs as $attachMsg) { 990 $att = new stdClass(); 991 $att->name = ""; 992 $att->data = getRawMsg($user, $draft->attachMsg); 993 $att->encoding = "7bit"; 994 $att->type = "message/RFC822"; 995 $attachments[] = $att; 996 while (strpos($att->data, $boundary) !== false) { 997 $boundary = $boundary . "-X"; 998 } 999 } 1000 } 1001 foreach ($draft->attachParts as $attachPart) { 1002 $att = new stdClass(); 1003 $att->name = $attachPart->name; 1004 $part = getPart($user, $attachPart->part); 1005 $att->data = getPartContent($user, $part); 1006 $att->data = imap_binary($att->data); 1007 $att->encoding = "BASE64"; 1008 $att->type = "$part->type/$part->subtype"; 1009 $attachments[] = $att; 1010 } 1011 if (count($attachments) == 0) { 1012 // use single-part MIME 1013 $headers = $headers . 1014 "Content-type: text/plain; charset=$bodyCharset\r\n" . 1015 "Content-transfer-encoding: $bodyEncoding\r\n"; 1016 } else { 1017 while (strpos($body, $boundary) !== false) { 1018 $boundary = $boundary . "-Y"; 1019 } 1020 $headers = $headers . 1021 "Content-type: multipart/mixed; boundary=\"$boundary\"\r\n" . 1022 "Content-transfer-encoding: 7bit\r\n"; 1023 $body = "This is a MIME multipart message\r\n" . 1024 "\r\n--$boundary\r\n" . 1025 "Content-type: text/plain; charset=$bodyCharset\r\n" . 1026 "Content-transfer-encoding: $bodyEncoding\r\n" . 1027 "Content-disposition: inline\r\n\r\n" . 1028 $body; 1029 foreach ($attachments as $att) { 1030 $body = $body . "\r\n--$boundary\r\n" . 1031 "Content-type: " . stripcrlf($att->type) . 1032 ($att->name == "" ? "" : 1033 "; name=\"" . stripcrlf($att->name) . "\"") . 1034 "\r\nContent-transfer-encoding: $att->encoding\r\n" . 1035 "Content-disposition: attachment" . 1036 ($att->name=="" ? "" : 1037 "; filename=\"" . stripcrlf($att->name) . "\"") . 1038 "\r\n\r\n" . 1039 $att->data; 1040 } 1041 $body = $body . "\r\n--$boundary--\r\n"; 1042 } 1043 sendmail($user, $draft->msgfrom, $headers, $body); 1044 writeLog("user $user sent draft #$draftId"); 1045 deleteDraftInner($user, $draftId); 1046 return null; 1047 } 1048 1049 ?>
End of listing