Message.class.php 42 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198
  1. <?php
  2. /**
  3. * Message.class.php
  4. *
  5. * This file contains functions needed to handle mime messages.
  6. *
  7. * @copyright 2003-2025 The SquirrelMail Project Team
  8. * @license http://opensource.org/licenses/gpl-license.php GNU Public License
  9. * @version $Id$
  10. * @package squirrelmail
  11. * @subpackage mime
  12. * @since 1.3.2
  13. */
  14. /**
  15. * The object that contains a message.
  16. *
  17. * message is the object that contains messages. It is a recursive object in
  18. * that through the $entities variable, it can contain more objects of type
  19. * message. See documentation in mime.txt for a better description of how this
  20. * works.
  21. * @package squirrelmail
  22. * @subpackage mime
  23. * @since 1.3.0
  24. */
  25. class Message {
  26. var $header;
  27. /**
  28. * rfc822header object
  29. * @var object
  30. */
  31. var $rfc822_header = '';
  32. /**
  33. * rfc822header object (reply)
  34. * @var object
  35. */
  36. var $reply_rfc822_header = '';
  37. /**
  38. * MessageHeader object
  39. * @var object
  40. */
  41. var $mime_header = '';
  42. /**
  43. * @var mixed
  44. */
  45. var $flags = '';
  46. /**
  47. * Media type
  48. * @var string
  49. */
  50. var $type0='';
  51. /**
  52. * Media subtype
  53. * @var string
  54. */
  55. var $type1='';
  56. /**
  57. * Nested mime parts
  58. * @var array
  59. */
  60. var $entities = array();
  61. /**
  62. * Message part id
  63. * @var string
  64. */
  65. var $entity_id = '';
  66. /**
  67. * Parent message part id
  68. * @var string
  69. */
  70. var $parent_ent;
  71. /**
  72. * @var mixed
  73. */
  74. var $entity;
  75. /**
  76. * @var mixed
  77. */
  78. var $parent = '';
  79. /**
  80. * @var string
  81. */
  82. var $decoded_body='';
  83. /**
  84. * Message \seen status
  85. * @var boolean
  86. */
  87. var $is_seen = 0;
  88. /**
  89. * Message \answered status
  90. * @var boolean
  91. */
  92. var $is_answered = 0;
  93. /**
  94. * Message forward status
  95. * @var boolean
  96. */
  97. var $is_forwarded = 0;
  98. /**
  99. * Message \deleted status
  100. * @var boolean
  101. */
  102. var $is_deleted = 0;
  103. /**
  104. * Message \flagged status
  105. * @var boolean
  106. */
  107. var $is_flagged = 0;
  108. /**
  109. * Message mdn status
  110. * @var boolean
  111. */
  112. var $is_mdnsent = 0;
  113. /**
  114. * Message text body
  115. * @var string
  116. */
  117. var $body_part = '';
  118. /**
  119. * Message part offset
  120. * for fetching body parts out of raw messages
  121. * @var integer
  122. */
  123. var $offset = 0;
  124. /**
  125. * Message part length
  126. * for fetching body parts out of raw messages
  127. * @var integer
  128. */
  129. var $length = 0;
  130. /**
  131. * Local attachment filename location where the tempory attachment is
  132. * stored. For use in delivery class.
  133. * @var string
  134. */
  135. var $att_local_name = '';
  136. /**
  137. * @param string $ent entity id
  138. */
  139. function setEnt($ent) {
  140. $this->entity_id= $ent;
  141. }
  142. /**
  143. * Add nested message part
  144. * @param object $msg
  145. */
  146. function addEntity ($msg) {
  147. $this->entities[] = $msg;
  148. }
  149. /**
  150. * Get file name used for mime part
  151. * @return string file name
  152. * @since 1.3.2
  153. */
  154. function getFilename() {
  155. $filename = '';
  156. $header = $this->header;
  157. if (is_object($header->disposition)) {
  158. $filename = $header->disposition->getProperty('filename');
  159. if (trim($filename) == '') {
  160. $name = decodeHeader($header->disposition->getProperty('name'));
  161. if (!trim($name)) {
  162. $name = $header->getParameter('name');
  163. if(!trim($name)) {
  164. if (!trim( $header->id )) {
  165. $filename = 'untitled-[' . $this->entity_id . ']' . '.' . strtolower($header->type1);
  166. } else {
  167. $filename = 'cid: ' . $header->id . '.' . strtolower($header->type1);
  168. }
  169. } else {
  170. $filename = $name;
  171. }
  172. } else {
  173. $filename = $name;
  174. }
  175. }
  176. } else {
  177. $filename = $header->getParameter('filename');
  178. if (!trim($filename)) {
  179. $filename = $header->getParameter('name');
  180. if (!trim($filename)) {
  181. if (!trim( $header->id )) {
  182. $filename = 'untitled-[' . $this->entity_id . ']' . '.' . strtolower($header->type1);
  183. } else {
  184. $filename = 'cid: ' . $header->id . '.' . strtolower($header->type1);
  185. }
  186. }
  187. }
  188. }
  189. return $filename;
  190. }
  191. /**
  192. * Add header object to message object.
  193. * WARNING: Unfinished code. Don't expect it to work in older sm versions.
  194. * @param mixed $read array or string with message headers
  195. * @todo FIXME: rfc822header->parseHeader() does not return rfc822header object
  196. */
  197. function addRFC822Header($read) {
  198. $header = new Rfc822Header();
  199. $this->rfc822_header = $header->parseHeader($read);
  200. }
  201. /**
  202. * @param string $ent
  203. * @return mixed (object or string?)
  204. */
  205. function getEntity($ent) {
  206. $cur_ent = $this->entity_id;
  207. $msg = $this;
  208. if (($cur_ent == '') || ($cur_ent == '0')) {
  209. $cur_ent_a = array();
  210. } else {
  211. $cur_ent_a = explode('.', $this->entity_id);
  212. }
  213. $ent_a = explode('.', $ent);
  214. for ($i = 0,$entCount = count($ent_a) - 1; $i < $entCount; ++$i) {
  215. if (isset($cur_ent_a[$i]) && ($cur_ent_a[$i] != $ent_a[$i])) {
  216. $msg = $msg->parent;
  217. $cur_ent_a = explode('.', $msg->entity_id);
  218. --$i;
  219. } else if (!isset($cur_ent_a[$i])) {
  220. if (isset($msg->entities[($ent_a[$i]-1)])) {
  221. $msg = $msg->entities[($ent_a[$i]-1)];
  222. } else {
  223. $msg = $msg->entities[0];
  224. }
  225. }
  226. if (($msg->type0 == 'message') && ($msg->type1 == 'rfc822')) {
  227. /*this is a header for a message/rfc822 entity */
  228. $msg = $msg->entities[0];
  229. }
  230. }
  231. if (($msg->type0 == 'message') && ($msg->type1 == 'rfc822')) {
  232. /*this is a header for a message/rfc822 entity */
  233. $msg = $msg->entities[0];
  234. }
  235. if (isset($msg->entities[($ent_a[$entCount])-1])) {
  236. if (is_object($msg->entities[($ent_a[$entCount])-1])) {
  237. $msg = $msg->entities[($ent_a[$entCount]-1)];
  238. }
  239. }
  240. return $msg;
  241. }
  242. /**
  243. * Set message body
  244. * @param string $s message body
  245. */
  246. function setBody($s) {
  247. $this->body_part = $s;
  248. }
  249. /**
  250. * Clean message object
  251. */
  252. function clean_up() {
  253. $msg = $this;
  254. $msg->body_part = '';
  255. foreach ($msg->entities as $m) {
  256. $m->clean_up();
  257. }
  258. }
  259. /**
  260. * @return string
  261. */
  262. function getMailbox() {
  263. $msg = $this;
  264. while (is_object($msg->parent)) {
  265. $msg = $msg->parent;
  266. }
  267. return $msg->mailbox;
  268. }
  269. /*
  270. * Bodystructure parser, a recursive function for generating the
  271. * entity-tree with all the mime-parts.
  272. *
  273. * It follows RFC2060 and stores all the described fields in the
  274. * message object.
  275. *
  276. * Question/Bugs:
  277. *
  278. * Ask for me (Marc Groot Koerkamp, stekkel@users.sourceforge.net)
  279. * @param string $read
  280. * @param integer $i
  281. * @param mixed $sub_msg
  282. * @return object Message object
  283. * @todo define argument and return types
  284. */
  285. static function parseStructure($read, &$i, $sub_msg = '') {
  286. $msg = Message::parseBodyStructure($read, $i, $sub_msg);
  287. if($msg) $msg->setEntIds($msg,false,0);
  288. return $msg;
  289. }
  290. /**
  291. * @param object $msg
  292. * @param mixed $init
  293. * @param integer $i
  294. * @todo document me
  295. * @since 1.4.0
  296. */
  297. function setEntIds(&$msg,$init=false,$i=0) {
  298. $iCnt = count($msg->entities);
  299. if ($init !==false) {
  300. $iEntSub = $i+1;
  301. if ($msg->parent->type0 == 'message' &&
  302. $msg->parent->type1 == 'rfc822' &&
  303. $msg->type0 == 'multipart') {
  304. $iEntSub = '0';
  305. }
  306. if ($init) {
  307. $msg->entity_id = "$init.$iEntSub";
  308. } else {
  309. $msg->entity_id = $iEntSub;
  310. }
  311. } else if ($iCnt) {
  312. $msg->entity_id='0';
  313. } else {
  314. $msg->entity_id='1';
  315. }
  316. for ($i=0;$i<$iCnt;++$i) {
  317. $msg->entities[$i]->parent =& $msg;
  318. if (strrchr($msg->entity_id, '.') != '.0') {
  319. $msg->entities[$i]->setEntIds($msg->entities[$i],$msg->entity_id,$i);
  320. } else {
  321. $msg->entities[$i]->setEntIds($msg->entities[$i],$msg->parent->entity_id,$i);
  322. }
  323. }
  324. }
  325. /**
  326. * @param string $read
  327. * @param integer $i
  328. * @param mixed $sub_msg
  329. * @return object Message object
  330. * @todo document me
  331. * @since 1.4.0 (code was part of parseStructure() in 1.3.x)
  332. */
  333. static function parseBodyStructure($read, &$i, $sub_msg = '') {
  334. $arg_no = 0;
  335. $arg_a = array();
  336. if ($sub_msg) {
  337. $message = $sub_msg;
  338. } else {
  339. $message = new Message();
  340. }
  341. for ($cnt = strlen($read); $i < $cnt; ++$i) {
  342. $char = strtoupper($read[$i]);
  343. switch ($char) {
  344. case '(':
  345. switch($arg_no) {
  346. case 0:
  347. if (!isset($msg)) {
  348. $msg = new Message();
  349. $hdr = new MessageHeader();
  350. $hdr->type0 = 'text';
  351. $hdr->type1 = 'plain';
  352. $hdr->encoding = '7bit';
  353. $msg->header = $hdr;
  354. } else {
  355. $msg->header->type0 = 'multipart';
  356. $msg->type0 = 'multipart';
  357. while ($read[$i] == '(') {
  358. $msg->addEntity($msg->parseBodyStructure($read, $i, $msg));
  359. }
  360. }
  361. break;
  362. case 1:
  363. /* multipart properties */
  364. ++$i;
  365. $arg_a[] = $msg->parseProperties($read, $i);
  366. ++$arg_no;
  367. break;
  368. case 2:
  369. if (isset($msg->type0) && ($msg->type0 == 'multipart')) {
  370. ++$i;
  371. $arg_a[] = $msg->parseDisposition($read, $i);
  372. } else { /* properties */
  373. $arg_a[] = $msg->parseProperties($read, $i);
  374. }
  375. ++$arg_no;
  376. break;
  377. case 3:
  378. if (isset($msg->type0) && ($msg->type0 == 'multipart')) {
  379. ++$i;
  380. $arg_a[]= $msg->parseLanguage($read, $i);
  381. }
  382. case 7:
  383. if (($arg_a[0] == 'message') && ($arg_a[1] == 'rfc822')) {
  384. $msg->header->type0 = $arg_a[0];
  385. $msg->header->type1 = $arg_a[1];
  386. $msg->type0 = $arg_a[0];
  387. $msg->type1 = $arg_a[1];
  388. $rfc822_hdr = new Rfc822Header();
  389. $msg->rfc822_header = $msg->parseEnvelope($read, $i, $rfc822_hdr);
  390. while (($i < $cnt) && ($read[$i] != '(')) {
  391. ++$i;
  392. }
  393. $msg->addEntity($msg->parseBodyStructure($read, $i,$msg));
  394. }
  395. break;
  396. case 8:
  397. ++$i;
  398. $arg_a[] = $msg->parseDisposition($read, $i);
  399. ++$arg_no;
  400. break;
  401. case 9:
  402. ++$i;
  403. if (($arg_a[0] == 'text') || (($arg_a[0] == 'message') && ($arg_a[1] == 'rfc822'))) {
  404. $arg_a[] = $msg->parseDisposition($read, $i);
  405. } else {
  406. $arg_a[] = $msg->parseLanguage($read, $i);
  407. }
  408. ++$arg_no;
  409. break;
  410. case 10:
  411. if (($arg_a[0] == 'text') || (($arg_a[0] == 'message') && ($arg_a[1] == 'rfc822'))) {
  412. ++$i;
  413. $arg_a[] = $msg->parseLanguage($read, $i);
  414. } else {
  415. $i = $msg->parseParenthesis($read, $i);
  416. $arg_a[] = ''; /* not yet described in rfc2060 */
  417. }
  418. ++$arg_no;
  419. break;
  420. default:
  421. /* unknown argument, skip this part */
  422. $i = $msg->parseParenthesis($read, $i);
  423. $arg_a[] = '';
  424. ++$arg_no;
  425. break;
  426. } /* switch */
  427. break;
  428. case '"':
  429. /* inside an entity -> start processing */
  430. $arg_s = $msg->parseQuote($read, $i);
  431. ++$arg_no;
  432. if ($arg_no < 3) {
  433. $arg_s = strtolower($arg_s); /* type0 and type1 */
  434. }
  435. $arg_a[] = $arg_s;
  436. break;
  437. case 'n':
  438. case 'N':
  439. /* probably NIL argument */
  440. $tmpnil = strtoupper(substr($read, $i, 4));
  441. if ($tmpnil == 'NIL ' || $tmpnil == 'NIL)') {
  442. $arg_a[] = '';
  443. ++$arg_no;
  444. $i += 2;
  445. }
  446. break;
  447. case '{':
  448. /* process the literal value */
  449. $arg_a[] = $msg->parseLiteral($read, $i);
  450. ++$arg_no;
  451. break;
  452. case '0':
  453. case is_numeric($read[$i]):
  454. /* process integers */
  455. if ($read[$i] == ' ') { break; }
  456. ++$arg_no;
  457. if (preg_match('/^([0-9]+).*/',substr($read,$i), $regs)) {
  458. $i += strlen($regs[1])-1;
  459. $arg_a[] = $regs[1];
  460. } else {
  461. $arg_a[] = 0;
  462. }
  463. break;
  464. case ')':
  465. $multipart = (isset($msg->type0) && ($msg->type0 == 'multipart'));
  466. if (!$multipart) {
  467. $shifted_args = (($arg_a[0] == 'text') || (($arg_a[0] == 'message') && ($arg_a[1] == 'rfc822')));
  468. $hdr->type0 = $arg_a[0];
  469. $hdr->type1 = $arg_a[1];
  470. $msg->type0 = $arg_a[0];
  471. $msg->type1 = $arg_a[1];
  472. $arr = $arg_a[2];
  473. if (is_array($arr)) {
  474. $hdr->parameters = $arg_a[2];
  475. }
  476. $hdr->id = str_replace('<', '', str_replace('>', '', $arg_a[3]));
  477. $hdr->description = $arg_a[4];
  478. $hdr->encoding = strtolower($arg_a[5]);
  479. $hdr->entity_id = $msg->entity_id;
  480. $hdr->size = $arg_a[6];
  481. if ($shifted_args) {
  482. $hdr->lines = $arg_a[7];
  483. $s = 1;
  484. } else {
  485. $s = 0;
  486. }
  487. $hdr->md5 = (isset($arg_a[7+$s]) ? $arg_a[7+$s] : $hdr->md5);
  488. $hdr->disposition = (isset($arg_a[8+$s]) ? $arg_a[8+$s] : $hdr->disposition);
  489. $hdr->language = (isset($arg_a[9+$s]) ? $arg_a[9+$s] : $hdr->language);
  490. $msg->header = $hdr;
  491. } else {
  492. $hdr->type0 = 'multipart';
  493. $hdr->type1 = $arg_a[0];
  494. $msg->type0 = 'multipart';
  495. $msg->type1 = $arg_a[0];
  496. $hdr->parameters = (isset($arg_a[1]) ? $arg_a[1] : $hdr->parameters);
  497. $hdr->disposition = (isset($arg_a[2]) ? $arg_a[2] : $hdr->disposition);
  498. $hdr->language = (isset($arg_a[3]) ? $arg_a[3] : $hdr->language);
  499. $msg->header = $hdr;
  500. }
  501. return $msg;
  502. default: break;
  503. } /* switch */
  504. } /* for */
  505. } /* parsestructure */
  506. /**
  507. * @param string $read
  508. * @param integer $i
  509. * @return array
  510. */
  511. function parseProperties($read, &$i) {
  512. $properties = array();
  513. $prop_name = '';
  514. for (; $read[$i] != ')'; ++$i) {
  515. $arg_s = '';
  516. if ($read[$i] == '"') {
  517. $arg_s = $this->parseQuote($read, $i);
  518. } else if ($read[$i] == '{') {
  519. $arg_s = $this->parseLiteral($read, $i);
  520. }
  521. if ($arg_s != '') {
  522. if ($prop_name == '') {
  523. $prop_name = strtolower($arg_s);
  524. $properties[$prop_name] = '';
  525. } else if ($prop_name != '') {
  526. $properties[$prop_name] = $arg_s;
  527. $prop_name = '';
  528. }
  529. }
  530. }
  531. return $this->handleRfc2231($properties);
  532. }
  533. /**
  534. * Joins RFC-2231 continuations, converts encoding to RFC-2047 style
  535. * @param array $properties
  536. * @return array
  537. */
  538. function handleRfc2231($properties) {
  539. /* STAGE 1: look for multi-line parameters, convert to the single line
  540. form, and normalize values */
  541. $cont = array();
  542. foreach($properties as $key=>$value) {
  543. /* Look for parameters followed by "*", a number, and an optional "*"
  544. at the end. */
  545. if (preg_match('/^(.*\*)(\d+)(|\*)$/', $key, $matches)) {
  546. unset($properties[$key]);
  547. $prop_name = $matches[1];
  548. if (!isset($cont[$prop_name])) $cont[$prop_name] = array();
  549. /* An asterisk at the end of parameter name indicates that there
  550. may be an encoding information present, and the parameter
  551. value is percent-hex encoded. If parameter is not encoded, we
  552. encode it to simplify further processing.
  553. */
  554. if ($matches[3] == '') $value = rawurlencode($value);
  555. /* Use the number from parameter name as segment index */
  556. $cont[$prop_name][$matches[2]] = $value;
  557. }
  558. }
  559. foreach($cont as $key=>$values) {
  560. /* Sort segments of multi-line parameters by index number. */
  561. ksort($values);
  562. /* Join segments. We can do it safely, because:
  563. - All segments are encoded.
  564. - Per RFC-2231, chapter 4.1 notes.
  565. */
  566. $value = implode('', $values);
  567. /* Add language and character set field delimiters if not present,
  568. as required per RFC-2231, chapter 4.1, note #5. */
  569. if (strpos($value, "'") === false) $value = "''".$value;
  570. $properties[$key] = $value;
  571. }
  572. /* STAGE 2: Convert single line RFC-2231 encoded parameters, and
  573. previously converted multi-line parameters, to RFC-2047 encoding */
  574. foreach($properties as $key=>$value) {
  575. if ($idx = strpos($key, '*')) {
  576. unset($properties[$key]);
  577. /* Extract the charset & language. */
  578. $charset = substr($value,0,strpos($value,"'"));
  579. $value = substr($value,strlen($charset)+1);
  580. $language = substr($value,0,strpos($value,"'"));
  581. $value = substr($value,strlen($language)+1);
  582. /* No character set defaults to US-ASCII */
  583. if (!$charset) $charset = 'US-ASCII';
  584. if ($language) $language = '*'.$language;
  585. /* Convert to RFC-2047 base64 encoded string. */
  586. $properties[substr($key, 0, $idx)] = '=?'.$charset.$language.'?B?'.base64_encode(rawurldecode($value)).'?=';
  587. }
  588. }
  589. return $properties;
  590. }
  591. /**
  592. * @param string $read
  593. * @param integer $i
  594. * @param object $hdr MessageHeader object
  595. * @return object MessageHeader object
  596. */
  597. function parseEnvelope($read, &$i, $hdr) {
  598. $arg_no = 0;
  599. $arg_a = array();
  600. ++$i;
  601. for ($cnt = strlen($read); ($i < $cnt) && ($read[$i] != ')'); ++$i) {
  602. $char = strtoupper($read[$i]);
  603. switch ($char) {
  604. case '"':
  605. $arg_a[] = $this->parseQuote($read, $i);
  606. ++$arg_no;
  607. break;
  608. case '{':
  609. $arg_a[] = $this->parseLiteral($read, $i);
  610. /* temp bugfix (SM 1.5 will have a working clean version)
  611. too much work to implement that version right now */
  612. // --$i;
  613. ++$arg_no;
  614. break;
  615. case 'N':
  616. /* probably NIL argument */
  617. if (strtoupper(substr($read, $i, 3)) == 'NIL') {
  618. $arg_a[] = '';
  619. ++$arg_no;
  620. $i += 2;
  621. }
  622. break;
  623. case '(':
  624. /* Address structure (with group support)
  625. * Note: Group support is useless on SMTP connections
  626. * because the protocol doesn't support it
  627. */
  628. $addr_a = array();
  629. $group = '';
  630. $a=0;
  631. for (; $i < $cnt && $read[$i] != ')'; ++$i) {
  632. if ($read[$i] == '(') {
  633. $addr = $this->parseAddress($read, $i);
  634. if (($addr->host == '') && ($addr->mailbox != '')) {
  635. /* start of group */
  636. $group = $addr->mailbox;
  637. $group_addr = $addr;
  638. $j = $a;
  639. } else if ($group && ($addr->host == '') && ($addr->mailbox == '')) {
  640. /* end group */
  641. if ($a == ($j+1)) { /* no group members */
  642. $group_addr->group = $group;
  643. $group_addr->mailbox = '';
  644. $group_addr->personal = "$group: Undisclosed recipients;";
  645. $addr_a[] = $group_addr;
  646. $group ='';
  647. }
  648. } else {
  649. $addr->group = $group;
  650. $addr_a[] = $addr;
  651. }
  652. ++$a;
  653. }
  654. }
  655. $arg_a[] = $addr_a;
  656. break;
  657. default: break;
  658. }
  659. }
  660. if (count($arg_a) > 9) {
  661. $d = strtr($arg_a[0], array(' ' => ' '));
  662. $d_parts = explode(' ', $d);
  663. if (!$arg_a[1]) $arg_a[1] = _("(no subject)");
  664. $hdr->date = getTimeStamp($d_parts); /* argument 1: date */
  665. $hdr->date_unparsed = strtr($d,'<>',' '); /* original date */
  666. $hdr->subject = $arg_a[1]; /* argument 2: subject */
  667. $hdr->from = is_array($arg_a[2]) ? $arg_a[2][0] : ''; /* argument 3: from */
  668. $hdr->sender = is_array($arg_a[3]) ? $arg_a[3][0] : ''; /* argument 4: sender */
  669. $hdr->reply_to = is_array($arg_a[4]) ? $arg_a[4][0] : ''; /* argument 5: reply-to */
  670. $hdr->to = $arg_a[5]; /* argument 6: to */
  671. $hdr->cc = $arg_a[6]; /* argument 7: cc */
  672. $hdr->bcc = $arg_a[7]; /* argument 8: bcc */
  673. $hdr->in_reply_to = $arg_a[8]; /* argument 9: in-reply-to */
  674. $hdr->message_id = $arg_a[9]; /* argument 10: message-id */
  675. }
  676. return $hdr;
  677. }
  678. /**
  679. * @param string $read
  680. * @param integer $i
  681. * @return string
  682. * @todo document me
  683. */
  684. function parseLiteral($read, &$i) {
  685. $lit_cnt = '';
  686. ++$i;
  687. $iPos = strpos($read,'}',$i);
  688. if ($iPos) {
  689. $lit_cnt = substr($read, $i, $iPos - $i);
  690. $i += strlen($lit_cnt) + 3; /* skip } + \r + \n */
  691. /* Now read the literal */
  692. $s = ($lit_cnt ? substr($read,$i,$lit_cnt): '');
  693. $i += $lit_cnt;
  694. /* temp bugfix (SM 1.5 will have a working clean version)
  695. too much work to implement that version right now */
  696. --$i;
  697. } else { /* should never happen */
  698. $i += 3; /* } + \r + \n */
  699. $s = '';
  700. }
  701. return $s;
  702. }
  703. /**
  704. * function parseQuote
  705. *
  706. * This extract the string value from a quoted string. After the end-quote
  707. * character is found it returns the string. The offset $i when calling
  708. * this function points to the first double quote. At the end it points to
  709. * The ending quote. This function takes care of escaped double quotes.
  710. * "some \"string\""
  711. * ^ ^
  712. * initial $i end position $i
  713. *
  714. * @param string $read
  715. * @param integer $i offset in $read
  716. * @return string string inbetween the double quotes
  717. * @author Marc Groot Koerkamp
  718. */
  719. function parseQuote($read, &$i) {
  720. $s = '';
  721. $iPos = ++$i;
  722. $iPosStart = $iPos;
  723. while (true) {
  724. $iPos = strpos($read,'"',$iPos);
  725. if (!$iPos) break;
  726. if ($iPos && $read[$iPos -1] != '\\') {
  727. $s = substr($read,$i,($iPos-$i));
  728. $i = $iPos;
  729. break;
  730. } else if ($iPos > 1 && $read[$iPos -1] == '\\' && $read[$iPos-2] == '\\') {
  731. // This is an unique situation where the fast detection of the string
  732. // fails. If the quote string ends with \\ then we need to iterate
  733. // through the entire string to make sure we detect the unexcaped
  734. // double quotes correctly.
  735. $s = '';
  736. $bEscaped = false;
  737. $k = 0;
  738. for ($j=$iPosStart,$iCnt=strlen($read);$j<$iCnt;++$j) {
  739. $cChar = $read[$j];
  740. switch ($cChar) {
  741. case '\\':
  742. $bEscaped = !$bEscaped;
  743. $s .= $cChar;
  744. break;
  745. case '"':
  746. if ($bEscaped) {
  747. $s .= $cChar;
  748. $bEscaped = false;
  749. } else {
  750. $i = $j;
  751. break 3;
  752. }
  753. break;
  754. default:
  755. if ($bEscaped) {
  756. $bEscaped = false;
  757. }
  758. $s .= $cChar;
  759. break;
  760. }
  761. }
  762. }
  763. ++$iPos;
  764. if ($iPos > strlen($read)) {
  765. break;
  766. }
  767. }
  768. return $s;
  769. }
  770. /**
  771. * @param string $read
  772. * @param integer $i
  773. * @return object AddressStructure object
  774. */
  775. function parseAddress($read, &$i) {
  776. $arg_a = array();
  777. for (; $read[$i] != ')'; ++$i) {
  778. $char = strtoupper($read[$i]);
  779. switch ($char) {
  780. case '"': $arg_a[] = $this->parseQuote($read, $i); break;
  781. case '{': $arg_a[] = $this->parseLiteral($read, $i); break;
  782. case 'n':
  783. case 'N':
  784. if (strtoupper(substr($read, $i, 3)) == 'NIL') {
  785. $arg_a[] = '';
  786. $i += 2;
  787. }
  788. break;
  789. default: break;
  790. }
  791. }
  792. if (count($arg_a) == 4) {
  793. $adr = new AddressStructure();
  794. $adr->personal = $arg_a[0];
  795. $adr->adl = $arg_a[1];
  796. $adr->mailbox = $arg_a[2];
  797. $adr->host = $arg_a[3];
  798. } else {
  799. $adr = '';
  800. }
  801. return $adr;
  802. }
  803. /**
  804. * @param string $read
  805. * @param integer $i
  806. * @param object Disposition object or empty string
  807. */
  808. function parseDisposition($read, &$i) {
  809. $arg_a = array();
  810. for (; $read[$i] != ')'; ++$i) {
  811. switch ($read[$i]) {
  812. case '"': $arg_a[] = $this->parseQuote($read, $i); break;
  813. case '{': $arg_a[] = $this->parseLiteral($read, $i); break;
  814. case '(': $arg_a[] = $this->parseProperties($read, $i); break;
  815. default: break;
  816. }
  817. }
  818. if (isset($arg_a[0])) {
  819. $disp = new Disposition($arg_a[0]);
  820. if (isset($arg_a[1])) {
  821. $disp->properties = $arg_a[1];
  822. }
  823. }
  824. return (is_object($disp) ? $disp : '');
  825. }
  826. /**
  827. * @param string $read
  828. * @param integer $i
  829. * @return object Language object or empty string
  830. */
  831. function parseLanguage($read, &$i) {
  832. /* no idea how to process this one without examples */
  833. $arg_a = array();
  834. for (; $read[$i] != ')'; ++$i) {
  835. switch ($read[$i]) {
  836. case '"': $arg_a[] = $this->parseQuote($read, $i); break;
  837. case '{': $arg_a[] = $this->parseLiteral($read, $i); break;
  838. case '(': $arg_a[] = $this->parseProperties($read, $i); break;
  839. default: break;
  840. }
  841. }
  842. if (isset($arg_a[0])) {
  843. $lang = new Language($arg_a[0]);
  844. if (isset($arg_a[1])) {
  845. $lang->properties = $arg_a[1];
  846. }
  847. }
  848. return (is_object($lang) ? $lang : '');
  849. }
  850. /**
  851. * Parse message text enclosed in parenthesis
  852. * @param string $read
  853. * @param integer $i
  854. * @return integer
  855. */
  856. function parseParenthesis($read, $i) {
  857. for ($i++; $read[$i] != ')'; ++$i) {
  858. switch ($read[$i]) {
  859. case '"': $this->parseQuote($read, $i); break;
  860. case '{': $this->parseLiteral($read, $i); break;
  861. case '(': $this->parseProperties($read, $i); break;
  862. default: break;
  863. }
  864. }
  865. return $i;
  866. }
  867. /**
  868. * Function to fill the message structure in case the
  869. * bodystructure is not available
  870. * NOT FINISHED YET
  871. * @param string $read
  872. * @param string $type0 message part type
  873. * @param string $type1 message part subtype
  874. * @return string (only when type0 is not message or multipart)
  875. */
  876. function parseMessage($read, $type0, $type1) {
  877. switch ($type0) {
  878. case 'message':
  879. $rfc822_header = true;
  880. $mime_header = false;
  881. break;
  882. case 'multipart':
  883. $rfc822_header = false;
  884. $mime_header = true;
  885. break;
  886. default: return $read;
  887. }
  888. for ($i = 1; $i < $count; ++$i) {
  889. $line = trim($body[$i]);
  890. if (($mime_header || $rfc822_header) &&
  891. (preg_match("/^.*boundary=\"?(.+(?=\")|.+).*/i", $line, $reg))) {
  892. $bnd = $reg[1];
  893. $bndreg = $bnd;
  894. $bndreg = str_replace("\\", "\\\\", $bndreg);
  895. $bndreg = str_replace("?", "\\?", $bndreg);
  896. $bndreg = str_replace("+", "\\+", $bndreg);
  897. $bndreg = str_replace(".", "\\.", $bndreg);
  898. $bndreg = str_replace("/", "\\/", $bndreg);
  899. $bndreg = str_replace("-", "\\-", $bndreg);
  900. $bndreg = str_replace("(", "\\(", $bndreg);
  901. $bndreg = str_replace(")", "\\)", $bndreg);
  902. } else if ($rfc822_header && $line == '') {
  903. $rfc822_header = false;
  904. if ($msg->type0 == 'multipart') {
  905. $mime_header = true;
  906. }
  907. }
  908. if ((($line[0] == '-') || $rfc822_header) && isset($boundaries[0])) {
  909. $cnt = count($boundaries)-1;
  910. $bnd = $boundaries[$cnt]['bnd'];
  911. $bndreg = $boundaries[$cnt]['bndreg'];
  912. $regstr = '/^--'."($bndreg)".".*".'/';
  913. if (preg_match($regstr, $line, $reg)) {
  914. $bndlen = strlen($reg[1]);
  915. $bndend = false;
  916. if (strlen($line) > ($bndlen + 3)) {
  917. if (($line[$bndlen+2] == '-') && ($line[$bndlen+3] == '-')) {
  918. $bndend = true;
  919. }
  920. }
  921. if ($bndend) {
  922. /* calc offset and return $msg */
  923. //$entStr = CalcEntity("$entStr", -1);
  924. array_pop($boundaries);
  925. $mime_header = true;
  926. $bnd_end = true;
  927. } else {
  928. $mime_header = true;
  929. $bnd_end = false;
  930. //$entStr = CalcEntity("$entStr", 0);
  931. ++$content_indx;
  932. }
  933. } else {
  934. if ($header) { }
  935. }
  936. }
  937. }
  938. }
  939. /**
  940. * @param array $entity
  941. * @param array $alt_order
  942. * @param boolean $strict
  943. * @return array
  944. */
  945. function findDisplayEntity($entity = array(), $alt_order = array('text/plain', 'text/html'), $strict=false) {
  946. $found = false;
  947. if ($this->type0 == 'multipart') {
  948. if($this->type1 == 'alternative') {
  949. $msg = $this->findAlternativeEntity($alt_order);
  950. if ( ! is_null($msg) ) {
  951. if (count($msg->entities) == 0) {
  952. $entity[] = $msg->entity_id;
  953. } else {
  954. $entity = $msg->findDisplayEntity($entity, $alt_order, $strict);
  955. }
  956. $found = true;
  957. }
  958. } else if ($this->type1 == 'related') { /* RFC 2387 */
  959. $msgs = $this->findRelatedEntity();
  960. foreach ($msgs as $msg) {
  961. if (count($msg->entities) == 0) {
  962. $entity[] = $msg->entity_id;
  963. } else {
  964. $entity = $msg->findDisplayEntity($entity, $alt_order, $strict);
  965. }
  966. }
  967. if (count($msgs) > 0) {
  968. $found = true;
  969. }
  970. } else { /* Treat as multipart/mixed */
  971. foreach ($this->entities as $ent) {
  972. if(!(is_object($ent->header->disposition) && strtolower($ent->header->disposition->name) == 'attachment') &&
  973. (!isset($ent->header->parameters['filename'])) &&
  974. (!isset($ent->header->parameters['name'])) &&
  975. (($ent->type0 != 'message') && ($ent->type1 != 'rfc822'))) {
  976. $entity = $ent->findDisplayEntity($entity, $alt_order, $strict);
  977. $found = true;
  978. }
  979. }
  980. }
  981. } else { /* If not multipart, then just compare with each entry from $alt_order */
  982. $type = $this->type0.'/'.$this->type1;
  983. // $alt_order[] = "message/rfc822";
  984. foreach ($alt_order as $alt) {
  985. if( ($alt == $type) && isset($this->entity_id) ) {
  986. if ((count($this->entities) == 0) &&
  987. (!isset($this->header->parameters['filename'])) &&
  988. (!isset($this->header->parameters['name'])) &&
  989. isset($this->header->disposition) && is_object($this->header->disposition) &&
  990. !(is_object($this->header->disposition) && strtolower($this->header->disposition->name) == 'attachment')) {
  991. $entity[] = $this->entity_id;
  992. $found = true;
  993. }
  994. }
  995. }
  996. }
  997. if(!$found) {
  998. foreach ($this->entities as $ent) {
  999. if(!(is_object($ent->header->disposition) && strtolower($ent->header->disposition->name) == 'attachment') &&
  1000. (($ent->type0 != 'message') && ($ent->type1 != 'rfc822'))) {
  1001. $entity = $ent->findDisplayEntity($entity, $alt_order, $strict);
  1002. $found = true;
  1003. }
  1004. }
  1005. }
  1006. if(!$strict && !$found) {
  1007. if (($this->type0 == 'text') &&
  1008. in_array($this->type1, array('plain', 'html', 'message')) &&
  1009. isset($this->entity_id)) {
  1010. if (count($this->entities) == 0) {
  1011. if (!is_object($this->header->disposition) || strtolower($this->header->disposition->name) != 'attachment') {
  1012. $entity[] = $this->entity_id;
  1013. }
  1014. }
  1015. }
  1016. }
  1017. return $entity;
  1018. }
  1019. /**
  1020. * @param array $alt_order
  1021. * @return entity
  1022. */
  1023. function findAlternativeEntity($alt_order) {
  1024. /* If we are dealing with alternative parts then we */
  1025. /* choose the best viewable message supported by SM. */
  1026. $best_view = 0;
  1027. $entity = null;
  1028. foreach($this->entities as $ent) {
  1029. $type = $ent->header->type0 . '/' . $ent->header->type1;
  1030. if ($type == 'multipart/related') {
  1031. $type = $ent->header->getParameter('type');
  1032. // Mozilla bug. Mozilla does not provide the parameter type.
  1033. if (!$type) $type = 'text/html';
  1034. }
  1035. $altCount = count($alt_order);
  1036. for ($j = $best_view; $j < $altCount; ++$j) {
  1037. if (($alt_order[$j] == $type) && ($j >= $best_view)) {
  1038. $best_view = $j;
  1039. $entity = $ent;
  1040. }
  1041. }
  1042. }
  1043. return $entity;
  1044. }
  1045. /**
  1046. * @return array
  1047. */
  1048. function findRelatedEntity() {
  1049. $msgs = array();
  1050. $related_type = $this->header->getParameter('type');
  1051. // Mozilla bug. Mozilla does not provide the parameter type.
  1052. if (!$related_type) $related_type = 'text/html';
  1053. $entCount = count($this->entities);
  1054. for ($i = 0; $i < $entCount; ++$i) {
  1055. $type = $this->entities[$i]->header->type0.'/'.$this->entities[$i]->header->type1;
  1056. if ($related_type == $type) {
  1057. $msgs[] = $this->entities[$i];
  1058. }
  1059. }
  1060. return $msgs;
  1061. }
  1062. /**
  1063. * @param array $exclude_id
  1064. * @param array $result
  1065. * @return array
  1066. */
  1067. function getAttachments($exclude_id=array(), $result = array()) {
  1068. /*
  1069. if (($this->type0 == 'message') &&
  1070. ($this->type1 == 'rfc822') &&
  1071. ($this->entity_id) ) {
  1072. $this = $this->entities[0];
  1073. }
  1074. */
  1075. if (count($this->entities)) {
  1076. foreach ($this->entities as $entity) {
  1077. $exclude = false;
  1078. foreach ($exclude_id as $excl) {
  1079. if ($entity->entity_id === $excl) {
  1080. $exclude = true;
  1081. }
  1082. }
  1083. if (!$exclude) {
  1084. if ($entity->type0 == 'multipart') {
  1085. $result = $entity->getAttachments($exclude_id, $result);
  1086. } else if ($entity->type0 != 'multipart') {
  1087. $result[] = $entity;
  1088. }
  1089. }
  1090. }
  1091. } else {
  1092. $exclude = false;
  1093. foreach ($exclude_id as $excl) {
  1094. $exclude = $exclude || ($this->entity_id == $excl);
  1095. }
  1096. if (!$exclude) {
  1097. $result[] = $this;
  1098. }
  1099. }
  1100. return $result;
  1101. }
  1102. /**
  1103. * Add attachment to message object
  1104. * @param string $type attachment type
  1105. * @param string $name attachment name
  1106. * @param string $location path to attachment
  1107. */
  1108. function initAttachment($type, $name, $location) {
  1109. $attachment = new Message();
  1110. $mime_header = new MessageHeader();
  1111. $mime_header->setParameter('name', $name);
  1112. // FIXME: duplicate code. see ContentType class
  1113. $pos = strpos($type, '/');
  1114. if ($pos > 0) {
  1115. $mime_header->type0 = substr($type, 0, $pos);
  1116. $mime_header->type1 = substr($type, $pos+1);
  1117. } else {
  1118. $mime_header->type0 = $type;
  1119. }
  1120. $attachment->att_local_name = $location;
  1121. $disposition = new Disposition('attachment');
  1122. $disposition->properties['filename'] = $name;
  1123. $mime_header->disposition = $disposition;
  1124. $attachment->mime_header = $mime_header;
  1125. $this->entities[]=$attachment;
  1126. }
  1127. /**
  1128. * Delete all attachments from this object from disk.
  1129. * @since 1.5.1
  1130. */
  1131. function purgeAttachments() {
  1132. if ($this->att_local_name) {
  1133. global $username, $attachment_dir;
  1134. $hashed_attachment_dir = getHashedDir($username, $attachment_dir);
  1135. if ( file_exists($hashed_attachment_dir . '/' . $this->att_local_name) ) {
  1136. unlink($hashed_attachment_dir . '/' . $this->att_local_name);
  1137. }
  1138. }
  1139. // recursively delete attachments from entities contained in this object
  1140. for ($i=0, $entCount=count($this->entities);$i< $entCount; ++$i) {
  1141. $this->entities[$i]->purgeAttachments();
  1142. }
  1143. }
  1144. }