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 21KB

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