You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

authres_status.php 22KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485
  1. <?php
  2. /**
  3. * This plugin displays an icon showing the status
  4. * of an authentication results header of the message
  5. *
  6. * @version 0.3.1
  7. * @author pimlie
  8. * @mail pimlie@hotmail.com
  9. *
  10. * based on dkimstatus plugin by Julien vehent - julien@linuxwall.info
  11. * original plugin from Vladimir Mach - wladik@gmail.com
  12. *
  13. * icons by brankic1970: http://brankic1979.com/icons/
  14. *
  15. */
  16. class authres_status extends rcube_plugin
  17. {
  18. public $task = 'mail|settings';
  19. const STATUS_NOSIG = 1;
  20. const STATUS_NORES = 2;
  21. const STATUS_PASS = 4;
  22. const STATUS_PARS = 8;
  23. const STATUS_THIRD = 16;
  24. const STATUS_WARN = 32;
  25. const STATUS_FAIL = 64;
  26. const STATUS_ALL = 127;
  27. private static $RFC5451_authentication_methods = array(
  28. "auth",
  29. "dkim",
  30. "domainkeys",
  31. "sender-id",
  32. "spf"
  33. );
  34. private static $RFC5451_authentication_results = array(
  35. "none" => self::STATUS_NOSIG,
  36. "pass" => self::STATUS_PASS,
  37. "fail" => self::STATUS_FAIL,
  38. "policy" => self::STATUS_FAIL,
  39. "neutral" => self::STATUS_WARN,
  40. "temperror" => self::STATUS_WARN,
  41. "permerror" => self::STATUS_FAIL,
  42. "hardfail" => self::STATUS_FAIL,
  43. "softfail" => self::STATUS_WARN
  44. );
  45. private static $RFC5451_ptypes = array("smtp", "header", "body", "policy");
  46. private static $RFC5451_properties = array("auth", "d", "i", "from", "sender", "iprev", "mailfrom", "helo");
  47. private $override;
  48. private $img_status;
  49. private $message_headers_done = false;
  50. private $trusted_mtas;
  51. public function init()
  52. {
  53. $this->add_texts('localization', true);
  54. $rcmail = rcmail::get_instance();
  55. $this->load_config();
  56. if ($rcmail->action == 'show' || $rcmail->action == 'preview') {
  57. $this->add_hook('storage_init', array($this, 'storage_init'));
  58. $this->add_hook('message_headers_output', array($this, 'message_headers'));
  59. } elseif ($rcmail->action == 'list' || $rcmail->action == 'refresh' || $rcmail->action == 'check-recent' || $rcmail->action == 'move' || $rcmail->action == 'expunge' || $rcmail->action == 'search') {
  60. $this->add_hook('storage_init', array($this, 'storage_init'));
  61. $this->add_hook('messages_list', array($this, 'messages_list'));
  62. } elseif ($rcmail->action == '') {
  63. // with enabled_caching we're fetching additional headers before show/preview
  64. $this->add_hook('storage_init', array($this, 'storage_init'));
  65. }
  66. $dont_override = $rcmail->config->get('dont_override', array());
  67. $this->override = array(
  68. 'list_cols' => !in_array('list_cols', $dont_override),
  69. 'column' => !in_array('enable_authres_status_column', $dont_override),
  70. 'fallback' => !in_array('use_fallback_verifier', $dont_override),
  71. 'statuses' => !in_array('show_statuses', $dont_override),
  72. 'trusted_mtas' => !in_array('trusted_mtas', $dont_override),
  73. );
  74. if ($this->override['list_cols']) {
  75. $this->include_stylesheet($this->local_skin_path() . '/authres_status.css');
  76. if ($rcmail->config->get('enable_authres_status_column')) {
  77. $this->include_script('authres_status.js');
  78. }
  79. if ($this->override['column'] || $this->override['fallback'] || $this->override['statuses']) {
  80. $this->add_hook('preferences_list', array($this, 'preferences_list'));
  81. $this->add_hook('preferences_sections_list', array($this, 'preferences_section'));
  82. $this->add_hook('preferences_save', array($this, 'preferences_save'));
  83. }
  84. }
  85. $this->trusted_mtas = $rcmail->config->get('trusted_mtas', array());
  86. }
  87. public function storage_init($p)
  88. {
  89. $p['fetch_headers'] = trim($p['fetch_headers'] . ' ' . strtoupper('Authentication-Results') . ' ' . strtoupper('X-DKIM-Authentication-Results') . ' ' . strtoupper('X-Spam-Status') . ' ' . strtoupper('DKIM-Signature') . ' ' . strtoupper('DomainKey-Signature'));
  90. return $p;
  91. }
  92. public function preferences_list($args)
  93. {
  94. if ($args['section'] == 'authres_status') {
  95. $rcmail = rcmail::get_instance();
  96. if ($this->override['column'] || $this->override['fallback']) {
  97. $args['blocks']['authrescolumn']['name'] = $this->gettext('title_enable_column');
  98. if ($this->override['column']) {
  99. $args['blocks']['authrescolumn']['options']['enable']['title'] = $this->gettext('label_enable_column');
  100. $input = new html_checkbox(array('name' => '_enable_authres_status_column', 'id' => 'enable_authres_status_column', 'value' => 1));
  101. $args['blocks']['authrescolumn']['options']['enable']['content'] = $input->show($rcmail->config->get('enable_authres_status_column'));
  102. }
  103. if ($this->override['fallback']) {
  104. $args['blocks']['authrescolumn']['options']['fallback']['title'] = $this->gettext('label_fallback_verifier');
  105. $input = new html_checkbox(array('name' => '_use_fallback_verifier', 'id' => 'use_fallback_verifier', 'value' => 1));
  106. $args['blocks']['authrescolumn']['options']['fallback']['content'] = $input->show($rcmail->config->get('use_fallback_verifier'));
  107. }
  108. }
  109. if ($this->override['trusted_mtas']) {
  110. $args['blocks']['authrestrusted']['name'] = $this->gettext('title_trusted_mtas');
  111. $args['blocks']['authrestrusted']['options']['trusted_mtas']['title'] = $this->gettext('label_trusted_mtas');
  112. $input = new html_inputfield(array('name' => '_trusted_mtas', 'id' => 'trusted_mtas'));
  113. $args['blocks']['authrestrusted']['options']['trusted_mtas']['content'] = $input->show(implode(",", $rcmail->config->get('trusted_mtas')));
  114. }
  115. if ($this->override['statuses']) {
  116. $statuses = array(1, 2, 4, 8, 16, 32, 64);
  117. $show_statuses = $rcmail->config->get('show_statuses');
  118. if ($show_statuses === null) {
  119. $show_statuses = array_sum($statuses) - self::STATUS_NOSIG;
  120. }
  121. foreach ($statuses as $status) {
  122. $args['blocks']['authresstatus']['name'] = $this->gettext('title_include_status');
  123. $args['blocks']['authresstatus']['options']['enable' . $status]['title'] = $this->gettext('label_include_status' . $status);
  124. $input = new html_checkbox(array('name' => '_show_statuses[]', 'id' => 'enable_authres_status_column', 'value' => $status));
  125. $args['blocks']['authresstatus']['options']['enable' . $status]['content'] = $input->show(($show_statuses & $status));
  126. }
  127. }
  128. }
  129. return $args;
  130. }
  131. public function preferences_section($args)
  132. {
  133. $args['list']['authres_status'] = array(
  134. 'id' => 'authres_status',
  135. 'section' => rcube::Q($this->gettext('section_title'))
  136. );
  137. return $args;
  138. }
  139. public function preferences_save($args)
  140. {
  141. if ($args['section'] == 'authres_status') {
  142. $args['prefs']['enable_authres_status_column'] = isset($_POST["_enable_authres_status_column"]) && $_POST["_enable_authres_status_column"] == 1;
  143. $list_cols = rcmail::get_instance()->config->get('list_cols');
  144. $args['prefs']['use_fallback_verifier'] = isset($_POST["_use_fallback_verifier"]) && $_POST["_use_fallback_verifier"] == 1;
  145. if (isset($_POST['_trusted_mtas'])) {
  146. $trusted_mtas = array_map(function($value) {
  147. return trim($value);
  148. }, explode(",", $_POST["_trusted_mtas"]));
  149. $args['prefs']['trusted_mtas'] = array_diff($trusted_mtas, array(""));
  150. } else {
  151. $args['prefs']['trusted_mtas'] = array();
  152. }
  153. if (!is_array($list_cols)) {
  154. $list_cols = array();
  155. }
  156. if ($args['prefs']['enable_authres_status_column']) {
  157. if (!in_array('authres_status', $list_cols)) {
  158. $list_cols[] = 'authres_status';
  159. }
  160. } else {
  161. $list_cols = array_diff($list_cols, array('authres_status'));
  162. }
  163. $args['prefs']['list_cols'] = $list_cols;
  164. if (is_array($_POST["_show_statuses"])) {
  165. $args['prefs']['show_statuses'] = (int)array_sum($_POST["_show_statuses"]);
  166. }
  167. }
  168. return $args;
  169. }
  170. public function messages_list($p)
  171. {
  172. if (!empty($p['messages'])) {
  173. $rcmail = rcmail::get_instance();
  174. if ($rcmail->config->get('enable_authres_status_column')) {
  175. $show_statuses = (int)$rcmail->config->get('show_statuses');
  176. foreach ($p['messages'] as $index => $message) {
  177. $img_status = $this->get_authentication_status($message, $show_statuses, $message->uid);
  178. $p['messages'][$index]->list_cols['authres_status'] = $img_status;
  179. }
  180. }
  181. }
  182. return $p;
  183. }
  184. public function message_headers($p)
  185. {
  186. /* We only have to check the headers once and this method is executed more than once,
  187. /* so let's cache the result
  188. */
  189. if (!$this->message_headers_done) {
  190. $this->message_headers_done = true;
  191. $show_statuses = (int)rcmail::get_instance()->config->get('show_statuses');
  192. $this->img_status = $this->get_authentication_status($p['headers'], $show_statuses, (int)$_GET["_uid"]);
  193. }
  194. $p['output']['from']['value'] = $this->img_status . $p['output']['from']['value'];
  195. $p['output']['from']['html'] = true;
  196. return $p;
  197. }
  198. /* See https://tools.ietf.org/html/rfc5451
  199. */
  200. public function rfc5451_extract_authresheader($headers)
  201. {
  202. if (!is_array($headers)) {
  203. $headers = array($headers);
  204. }
  205. //rfc2822 token setup
  206. $crlf = "(?:\r\n)";
  207. $wsp = "[\t ]";
  208. $text = "[\\x01-\\x09\\x0B\\x0C\\x0E-\\x7F]";
  209. $quoted_pair = "(?:\\\\$text)";
  210. $fws = "(?:(?:$wsp*$crlf)?$wsp+)";
  211. $ctext = "[\\x01-\\x08\\x0B\\x0C\\x0E-\\x1F" . "!-'*-[\\]-\\x7F]";
  212. $comment = "(\\((?:$fws?(?:$ctext|$quoted_pair|(?1)))*" . "$fws?\\))";
  213. $cfws = "(?:(?:$fws?$comment)*(?:(?:$fws?$comment)|$fws))" . "?";
  214. $atom = "[a-z0-9!#$%&\'*+-\/=?^_`{|}~]+";
  215. $results = array();
  216. foreach ($headers as $header) {
  217. if (preg_match('/^' . $cfws . '((?=.{1,254}$)((?=[a-z0-9-]{1,63}\.)(xn--)?[a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,63}(\/[^\s]*)?)' . $cfws . '(\(.*?\))?' . $cfws . ';/i', trim($header), $m)) {
  218. $authservid = $m[3];
  219. if (!count($this->trusted_mtas) || in_array($authservid, $this->trusted_mtas)) {
  220. $header = substr($header, strlen($m[0]));
  221. $resinfos = array();
  222. $header_parts = explode(";", $header);
  223. while (count($header_parts)) {
  224. $header_part = array_shift($header_parts);
  225. // check whether part is not from within comment, eg 'dkim=pass (1024-bit key; insecure key)' should be matched as one
  226. if (preg_match('/\([^)]*$/', $header_part)) {
  227. $resinfos[] = trim($header_part . ';' . array_shift($header_parts));
  228. } else {
  229. $resinfos[] = trim($header_part);
  230. }
  231. }
  232. foreach ($resinfos as $resinfo) {
  233. if (preg_match('/(' . implode("|", self::$RFC5451_authentication_methods) . ')' . $cfws . '=' . $cfws . '(' . implode("|", array_keys(self::$RFC5451_authentication_results)) . ')' . $cfws . '(\(.*?\))?/i', $resinfo, $m, PREG_OFFSET_CAPTURE)) {
  234. $parsed_resinfo = array(
  235. 'title' => trim($m[0][0]),
  236. 'method' => $m[1][0],
  237. 'result' => $m[6][0],
  238. 'reason' => isset($m[7]) ? $m[7][0] : '',
  239. 'props' => array()
  240. );
  241. $propspec = trim(($m[0][1] > 0 ? substr($resinfo, 0, $m[0][1]) : '') . substr($resinfo, strlen($m[0][0])));
  242. if ($propspec) {
  243. if (preg_match_all('/(' . implode("|", self::$RFC5451_ptypes) . ')' . $cfws . '\.' . $cfws . '(' . implode("|", self::$RFC5451_properties) . ')' . $cfws . '=' . $cfws . '([^\s]*)/i', $propspec, $m)) {
  244. foreach ($m[0] as $k => $v) {
  245. if (!isset($parsed_resinfo['props'][$m[1][$k]])) {
  246. $parsed_resinfo['props'][$m[1][$k]] = array();
  247. }
  248. $parsed_resinfo['props'][$m[1][$k]] [$m[6][$k]] = $m[11][$k];
  249. }
  250. }
  251. }
  252. $results[] = $parsed_resinfo;
  253. }
  254. }
  255. }
  256. }
  257. }
  258. return $results;
  259. }
  260. public function get_authentication_status($headers, $show_statuses = 0, $uid = 0)
  261. {
  262. /* If dkimproxy did not find a signature, stop here
  263. */
  264. if (($results = $headers->others['x-dkim-authentication-results']) && strpos($results, 'none') !== false) {
  265. $status = self::STATUS_NOSIG;
  266. } else {
  267. if ($headers->others['authentication-results']) {
  268. $results = $this->rfc5451_extract_authresheader($headers->others['authentication-results']);
  269. $status = 0;
  270. $title = '';
  271. foreach ($results as $result) {
  272. $status = $status | (isset(self::$RFC5451_authentication_results[$result['result']]) ? self::$RFC5451_authentication_results[$result['result']] : self::STATUS_FAIL);
  273. $title .= ($title ? '; ' : '') . $result['title'];
  274. }
  275. if ($status == self::STATUS_PASS) {
  276. /* Verify if its an author's domain signature or a third party
  277. */
  278. if (preg_match("/[@]([a-zA-Z0-9]+([.][a-zA-Z0-9]+)?\.[a-zA-Z]{2,4})/", $headers->from, $m)) {
  279. $title = '';
  280. $authorDomain = $m[1];
  281. $authorDomainFound = false;
  282. foreach ($results as $result) {
  283. if ($result['method'] == 'dkim' || $result['method'] == 'domainkeys') {
  284. if (is_array($result['props']) && isset($result['props']['header'])) {
  285. $pvalue = '';
  286. // d is required, but still not always present
  287. if (isset($result['props']['header']['d'])) {
  288. $pvalue = $result['props']['header']['d'];
  289. } elseif (isset($result['props']['header']['i'])) {
  290. $pvalue = substr($result['props']['header']['i'], strpos($result['props']['header']['i'], '@') + 1);
  291. }
  292. if ($pvalue == $authorDomain || substr($authorDomain, -1 * strlen($pvalue)) == $pvalue) {
  293. $authorDomainFound = true;
  294. if ($status != self::STATUS_PASS) {
  295. $status = self::STATUS_PASS;
  296. $title = $result['title'];
  297. } else {
  298. $title.= ($title ? '; ' : '') . $result['title'];
  299. }
  300. } else {
  301. if ($status == self::STATUS_THIRD) {
  302. $title .= '; ' . $this->gettext('for') . ' ' . $pvalue . ' ' . $this->gettext('by') . ' ' . $result['title'];
  303. } elseif (!$authorDomainFound) {
  304. $status = self::STATUS_THIRD;
  305. $title = $pvalue . ' ' . $this->gettext('by') . ' ' . $result['title'];
  306. }
  307. }
  308. }
  309. }
  310. }
  311. }
  312. }
  313. if (!$status) {
  314. $status = self::STATUS_NOSIG;
  315. }
  316. /* Check for spamassassin's X-Spam-Status
  317. */
  318. } elseif ($headers->others['x-spam-status']) {
  319. $status = self::STATUS_NOSIG;
  320. /* DKIM_* are defined at: http://search.cpan.org/~kmcgrail/Mail-SpamAssassin-3.3.2/lib/Mail/SpamAssassin/Plugin/DKIM.pm */
  321. $results = $headers->others['x-spam-status'];
  322. if (is_array($results)) {
  323. $results = end($results); // Should we take first or last header found? Last has probably been added by our own MTA
  324. }
  325. if (preg_match_all('/DKIM_[^,=]+/', $results, $m)) {
  326. if (array_search('DKIM_SIGNED', $m[0]) !== false) {
  327. if (array_search('DKIM_VALID', $m[0]) !== false) {
  328. if (array_search('DKIM_VALID_AU', $m[0])) {
  329. $status = self::STATUS_PASS;
  330. $title = 'DKIM_SIGNED, DKIM_VALID, DKIM_VALID_AU';
  331. } else {
  332. $status = self::STATUS_THIRD;
  333. $title = 'DKIM_SIGNED, DKIM_VALID';
  334. }
  335. } else {
  336. $status = self::STATUS_FAIL;
  337. $title = 'DKIM_SIGNED';
  338. }
  339. }
  340. }
  341. } elseif ($headers->others['dkim-signature'] || $headers->others['domainkey-signature']) {
  342. $status = 0;
  343. if ($uid) {
  344. $rcmail = rcmail::get_instance();
  345. if ($headers->others['dkim-signature'] && $rcmail->config->get('use_fallback_verifier')) {
  346. if (!class_exists('Crypt_RSA')) {
  347. $autoload = require __DIR__ . "/../../vendor/autoload.php";
  348. $autoload->loadClass('Crypt_RSA'); // Preload for use in DKIM_Verify
  349. }
  350. $dkimVerify = new DKIM_Verify($rcmail->imap->get_raw_body($uid));
  351. $results = $dkimVerify->validate();
  352. if (count($results)) {
  353. $status = 0;
  354. $title = '';
  355. foreach ($results as $result) {
  356. foreach ($result as $res) {
  357. if (count($res)) {
  358. $status = $status | (isset(self::$RFC5451_authentication_results[$res['status']]) ? self::$RFC5451_authentication_results[$res['status']] : self::STATUS_FAIL);
  359. if ($res['status'] == 'pass') {
  360. $title .= ($title ? '; ' : '') . "dkim=pass (internal verifier)";
  361. }
  362. }
  363. }
  364. }
  365. if (!$title) {
  366. $title = $res['reason'];
  367. }
  368. }
  369. }
  370. }
  371. if (!$status) {
  372. $status = self::STATUS_NORES;
  373. }
  374. } else {
  375. $status = self::STATUS_NOSIG;
  376. }
  377. }
  378. if ($status == self::STATUS_NOSIG) {
  379. $image = 'status_nosig.png';
  380. $alt = 'nosignature';
  381. } elseif ($status == self::STATUS_NORES) {
  382. $image = 'status_nores.png';
  383. $alt = 'noauthresults';
  384. } elseif ($status == self::STATUS_PASS) {
  385. $image = 'status_pass.png';
  386. $alt = 'signaturepass';
  387. } else {
  388. // at least one auth method was passed, show partial pass
  389. if (($status & self::STATUS_PASS)) {
  390. $status = self::STATUS_PARS;
  391. $image = 'status_partial_pass.png';
  392. $alt = 'partialpass';
  393. } elseif ($status >= self::STATUS_FAIL) {
  394. $image = 'status_fail.png';
  395. $alt = 'invalidsignature';
  396. } elseif ($status >= self::STATUS_WARN) {
  397. $image = 'status_warn.png';
  398. $alt = 'temporaryinvalid';
  399. } elseif ($status >= self::STATUS_THIRD) {
  400. $image = 'status_third.png';
  401. $alt = 'thirdparty';
  402. }
  403. }
  404. if (!$show_statuses || ($show_statuses & $status)) {
  405. $alt = $this->gettext($alt);
  406. return '<img src="plugins/authres_status/images/' . $image . '" alt="' . $alt . '" title="' . $alt . htmlentities($title) . '" width="14" height="14" class="authres-status-img" /> ';
  407. }
  408. return '';
  409. }
  410. }