32_mailcheck.pm 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585
  1. # $Id: 32_mailcheck.pm 16299 2018-03-01 08:06:55Z justme1968 $
  2. # basic idea from https://github.com/justinribeiro/idlemailcheck
  3. package main;
  4. use strict;
  5. use warnings;
  6. use Mail::IMAPClient;
  7. use IO::Socket::SSL;
  8. use IO::Socket::INET;
  9. use IO::File;
  10. use IO::Handle;
  11. use Data::Dumper;
  12. my $mailcheck_hasGPG = 1;
  13. my $mailcheck_hasMIME = 1;
  14. sub
  15. mailcheck_Initialize($)
  16. {
  17. my ($hash) = @_;
  18. eval "use MIME::Parser";
  19. $mailcheck_hasMIME = 0 if($@);
  20. eval "use Mail::GnuPG";
  21. $mailcheck_hasGPG = 0 if($@);
  22. $hash->{ReadFn} = "mailcheck_Read";
  23. $hash->{DefFn} = "mailcheck_Define";
  24. $hash->{NotifyFn} = "mailcheck_Notify";
  25. $hash->{UndefFn} = "mailcheck_Undefine";
  26. $hash->{SetFn} = "mailcheck_Set";
  27. $hash->{GetFn} = "mailcheck_Get";
  28. $hash->{AttrFn} = "mailcheck_Attr";
  29. $hash->{AttrList} = "debug:1 ".
  30. "delete_message:1 ".
  31. "disable:1 ".
  32. "interval ".
  33. "logfile ".
  34. "nossl:1 ";
  35. $hash->{AttrList} .= "accept_from " if( $mailcheck_hasMIME && $mailcheck_hasGPG );
  36. $hash->{AttrList} .= $readingFnAttributes;
  37. }
  38. #####################################
  39. sub
  40. mailcheck_Define($$)
  41. {
  42. my ($hash, $def) = @_;
  43. my @a = split("[ \t][ \t]*", $def);
  44. return "Usage: define <name> mailcheck host user password [folder]" if(@a < 5);
  45. my $name = $a[0];
  46. my $host = $a[2];
  47. my $user = $a[3];
  48. my $password = $a[4];
  49. my $folder = $a[5];
  50. if( $user =~ m/^{.*}$/ ) {
  51. my $NAME = $name;
  52. my $HOST = $host;
  53. my $u = eval $user;
  54. if( $@ ) {
  55. Log3 $name, 2, $name .": ". $user .": ". $@;
  56. }
  57. $user = $u if( $u );
  58. }
  59. if( $password =~ m/^{.*}$/ ) {
  60. my $NAME = $name;
  61. my $HOST = $host;
  62. my $USER = $user;
  63. my $p = eval $password;
  64. if( $@ ) {
  65. Log3 $name, 2, $name .": ". $password .": ". $@;
  66. }
  67. $password = $p if( $p );
  68. }
  69. $hash->{tag} = undef;
  70. $hash->{NAME} = $name;
  71. $hash->{Host} = $host;
  72. $hash->{helper}{user} = $user;
  73. $hash->{helper}{password} = $password;
  74. $hash->{Folder} = "INBOX";
  75. $hash->{Folder} = $folder if( $folder );
  76. $hash->{HAS_GPG} = $mailcheck_hasGPG;
  77. $hash->{HAS_MIME} = $mailcheck_hasMIME;
  78. $hash->{NOTIFYDEV} = "global";
  79. if( $init_done ) {
  80. mailcheck_Disconnect($hash);
  81. mailcheck_Connect($hash);
  82. } elsif( $hash->{STATE} ne "???" ) {
  83. $hash->{STATE} = "Initialized";
  84. }
  85. return undef;
  86. }
  87. sub
  88. mailcheck_Notify($$)
  89. {
  90. my ($hash,$dev) = @_;
  91. return if($dev->{NAME} ne "global");
  92. return if(!grep(m/^INITIALIZED|REREADCFG$/, @{$dev->{CHANGED}}));
  93. mailcheck_Connect($hash);
  94. return undef;
  95. }
  96. sub
  97. mailcheck_Connect($)
  98. {
  99. my ($hash) = @_;
  100. my $name = $hash->{NAME};
  101. return undef if( IsDisabled($name) > 0 );
  102. my $socket;
  103. if( AttrVal($name, "nossl", 0) ) {
  104. $socket = IO::Socket::INET->new( PeerAddr => $hash->{Host},
  105. PeerPort => 143, #AttrVal($name, "port", 143)
  106. );
  107. } else {
  108. $socket = IO::Socket::SSL->new( PeerAddr => $hash->{Host},
  109. PeerPort => 993, #AttrVal($name, "port", 993)
  110. );
  111. }
  112. if($socket) {
  113. $hash->{STATE} = "Connected";
  114. $hash->{LAST_CONNECT} = FmtDateTime( gettimeofday() );
  115. $hash->{FD} = $socket->fileno();
  116. $hash->{CD} = $socket; # sysread / close won't work on fileno
  117. $hash->{CONNECTS}++;
  118. $selectlist{$name} = $hash;
  119. Log3 $name, 3, "$name: connected to $hash->{Host}";
  120. my $client = Mail::IMAPClient->new(
  121. Socket => $socket,
  122. KeepAlive => 'true',
  123. User => $hash->{helper}{user},
  124. Password => $hash->{helper}{password},
  125. );
  126. $client->Debug(AttrVal($name, "debug", 0)) if( $client );
  127. $client->Debug_fh($hash->{FH}) if( $client && defined($hash->{FH}) );
  128. if( $client && $client->IsConnected && $client->IsAuthenticated ) {
  129. $hash->{STATE} = "Logged in";
  130. $hash->{LAST_LOGIN} = FmtDateTime( gettimeofday() );
  131. $hash->{CLIENT} = $client;
  132. Log3 $name, 3, "$name: logged in to $hash->{helper}{user}";
  133. $hash->{HAS_IDLE} = $client->has_capability("IDLE");
  134. my $interval = AttrVal($name, "interval", 0);
  135. $interval = $hash->{HAS_IDLE}?60*10:60*1 if( !$interval );
  136. $hash->{INTERVAL} = $interval;
  137. RemoveInternalTimer($hash);
  138. InternalTimer(gettimeofday()+$hash->{INTERVAL}, "mailcheck_poll", $hash, 0);
  139. #if( !$client->has_capability("IDLE") ) {
  140. #mailcheck_Disconnect($hash);
  141. #$hash->{STATE} = "IDLE not supported";
  142. #return undef;
  143. #}
  144. $client->Uid(0);
  145. $client->Timeout(8);
  146. $client->select($hash->{Folder});
  147. $hash->{tag} = $client->idle;
  148. } else {
  149. mailcheck_Disconnect($hash);
  150. }
  151. } else {
  152. #$hash->{STATE} = "Connected";
  153. Log3 $name, 3, "$name: failed to connect to $hash->{Host}";
  154. }
  155. }
  156. sub
  157. mailcheck_Disconnect($)
  158. {
  159. my ($hash) = @_;
  160. my $name = $hash->{NAME};
  161. RemoveInternalTimer($hash);
  162. return if( !$hash->{CD} );
  163. my $client = $hash->{CLIENT};
  164. $client->done if($client && $client->IsAuthenticated );
  165. $client->logout if($client && $client->IsConnected);
  166. delete $hash->{CLIENT};
  167. $hash->{tag} = undef;
  168. Log3 $name, 3, "$name: logged out";
  169. close($hash->{CD}) if($hash->{CD});
  170. delete($hash->{FD});
  171. delete($hash->{CD});
  172. delete($selectlist{$name});
  173. $hash->{STATE} = "Disconnected";
  174. Log3 $name, 3, "$name: Disconnected";
  175. $hash->{LAST_DISCONNECT} = FmtDateTime( gettimeofday() );
  176. }
  177. sub
  178. mailcheck_Undefine($$)
  179. {
  180. my ($hash, $arg) = @_;
  181. mailcheck_Disconnect($hash);
  182. return undef;
  183. }
  184. sub
  185. mailcheck_Set($$@)
  186. {
  187. my ($hash, $name, $cmd) = @_;
  188. my $list = "active:noArg inactive:noArg";
  189. if( $cmd eq 'active' ) {
  190. mailcheck_Disconnect($hash);
  191. $hash->{STATE} = "Initialized";
  192. mailcheck_Connect($hash);
  193. return undef;
  194. } elsif( $cmd eq 'inactive' ) {
  195. mailcheck_Disconnect($hash);
  196. $hash->{STATE} = 'inactive';
  197. return undef;
  198. }
  199. return "Unknown argument $cmd, choose one of $list";
  200. }
  201. sub
  202. mailcheck_poll($)
  203. {
  204. my ($hash) = @_;
  205. RemoveInternalTimer($hash);
  206. InternalTimer(gettimeofday()+$hash->{INTERVAL}, "mailcheck_poll", $hash, 0);
  207. my $client = $hash->{CLIENT};
  208. if( $client && $client->IsConnected && $client->IsAuthenticated ) {
  209. $client->done;
  210. $client->select($hash->{Folder});
  211. $hash->{tag} = $client->idle;
  212. $hash->{LAST_POLL} = FmtDateTime( gettimeofday() );
  213. }
  214. }
  215. sub
  216. mailcheck_Get($$@)
  217. {
  218. my ($hash, $name, $cmd) = @_;
  219. my $list = "folders:noArg update:noArg";
  220. my $client = $hash->{CLIENT};
  221. if( $cmd eq "folders" ) {
  222. if( $client && $client->IsConnected && $client->IsAuthenticated ) {
  223. $client->done;
  224. my @folders = $client->folders;
  225. $hash->{tag} = $client->idle;
  226. return join( "\n", @folders );
  227. }
  228. return "not connected";
  229. } elsif( $cmd eq "update" ) {
  230. mailcheck_poll($hash);
  231. return undef;
  232. }
  233. return "Unknown argument $cmd, choose one of $list";
  234. }
  235. sub
  236. mailcheck_Attr($$$)
  237. {
  238. my ($cmd, $name, $attrName, $attrVal) = @_;
  239. my $orig = $attrVal;
  240. $attrVal = int($attrVal) if($attrName eq "interval");
  241. $attrVal = 60 if($attrName eq "interval" && $attrVal < 60 && $attrVal != 0);
  242. if( $attrName eq "debug" ) {
  243. $attrVal = 1 if($attrVal);
  244. my $hash = $defs{$name};
  245. my $client = $hash->{CLIENT};
  246. $client->Debug($attrVal) if( $client );
  247. } elsif( $attrName eq "logfile" ) {
  248. my $hash = $defs{$name};
  249. close( $hash->{FH} ) if( defined($hash->{FH}) );
  250. delete $hash->{FH};
  251. delete $hash->{currentlogfile};
  252. if( $cmd eq "set" ) {
  253. my @t = localtime;
  254. my $f = ResolveDateWildcards($attrVal, @t);
  255. my $fh = new IO::File ">>$f";
  256. if( defined($fh) ) {
  257. Log3 $name, 3, "$name: logging to $f";
  258. $fh->autoflush(1);
  259. $hash->{FH} = $fh;
  260. $hash->{currentlogfile} = $f;
  261. my $client = $hash->{CLIENT};
  262. $client->Debug_fh($fh) if( $client );
  263. } else {
  264. Log3 $name, 3, "$name: can't open log file $f";
  265. my $client = $hash->{CLIENT};
  266. $client->Debug_fh(*STDERR) if( $client );
  267. }
  268. }
  269. } elsif( $attrName eq "disable" ) {
  270. my $hash = $defs{$name};
  271. if( $cmd eq "set" && $attrVal ne "0" ) {
  272. mailcheck_Disconnect($hash);
  273. } else {
  274. $attr{$name}{$attrName} = 0;
  275. mailcheck_Disconnect($hash);
  276. mailcheck_Connect($hash);
  277. }
  278. }
  279. if( $cmd eq "set" ) {
  280. if( $orig ne $attrVal ) {
  281. $attr{$name}{$attrName} = $attrVal;
  282. return $attrName ." set to ". $attrVal;
  283. }
  284. }
  285. return;
  286. }
  287. sub
  288. mailcheck_Read($)
  289. {
  290. my ($hash) = @_;
  291. my $name = $hash->{NAME};
  292. my $client = $hash->{CLIENT};
  293. my $ret = $client->idle_data();
  294. if( !defined($ret) || !$ret ) {
  295. $hash->{tag} = undef;
  296. $ret = $client->done;
  297. }
  298. foreach my $resp (@$ret) {
  299. $resp =~ s/\015?\012$//;
  300. if ( $resp =~ /^\*\s+(\d+)\s+(EXISTS)\b/ ) {
  301. $resp =~ s/\D//g;
  302. $client->done;
  303. my $msg_count = $client->unseen_count||0;
  304. if ($msg_count > 0) {
  305. my $from = $client->get_header($resp, "From");
  306. Log3 $name, 4, "from: $from";
  307. if( $from =~ m/<([^>]*)>/ ) {
  308. $from = $1;
  309. }
  310. my $subject = $client->get_header($resp, "Subject");
  311. Log3 $name, 4, "subject: $subject";
  312. my $do_notify = 1;
  313. if( $hash->{HAS_MIME} ) {
  314. my $message = $client->message_string($resp);
  315. Log3 $name, 5, "message: $message";
  316. my $parser = new MIME::Parser;
  317. $parser->tmp_to_core(1);
  318. $parser->output_to_core(1);
  319. my $entity = $parser->parse_data($message);
  320. #Log3 $name, 5, "mime: $entity";
  321. if( my $accept_from = AttrVal($name, "accept_from", "" ) ) {
  322. $do_notify = 0;
  323. if( $hash->{HAS_GPG} ) {
  324. my $gpg = new Mail::GnuPG();
  325. if( $gpg->is_signed($entity) ) {
  326. my ($result,$keyid,$email) = $gpg->verify( $entity );
  327. if( $result == 0 ) {
  328. if( !$keyid && !$email) {
  329. Log3 $name, 4, "signature valid";
  330. my $result = join "", @{$gpg->{last_message}};
  331. ($keyid) = $result =~ /mittels \S+ ID (.+)$/m;
  332. ($email) = $result =~ /Korrekte Signatur von "(.+)"$/m;
  333. #($email) = $result =~ /(Korrekte|FALSCHE) Signatur von "(.+)"$/m;
  334. }
  335. if( !$keyid || !$email ) {
  336. Log3 $name, 3, "can't parse gpg result. please fix regex in module.";
  337. Log3 $name, 3, Dumper $gpg->{last_message};
  338. }
  339. $do_notify = 1 if( ",$accept_from," =~/,$keyid,/i );
  340. Log3 $name, 3, "sender $keyid not allowed" if( !$do_notify );
  341. } else {
  342. Log3 $name, 3, "invalid signature";
  343. Log3 $name, 4, Dumper $gpg->{last_message};
  344. }
  345. } else {
  346. Log3 $name, 3, "message not signed";
  347. }
  348. } elsif( $hash->{HAS_SMIME} ) {
  349. } else {
  350. Log3 $name, 2, "accept_from is set but Mail::GnuPG and/or S/MIME is not available";
  351. }
  352. }
  353. $entity->head->decode();
  354. if( $subject =~ /iso-8859-1/ ) {
  355. $subject = $entity->head->get('Subject');
  356. $subject = latin1ToUtf8( $subject );
  357. } else {
  358. $subject = $entity->head->get('Subject');
  359. }
  360. chomp( $subject );
  361. Log3 $name, 4, "subject decoded: $subject";
  362. } elsif( my $accept_from = AttrVal($name, "accept_from", "" ) ) {
  363. Log3 $name, 2, "accept_from is set but MIME::Parser is not available";
  364. }
  365. if( $do_notify ) {
  366. readingsBeginUpdate($hash);
  367. readingsBulkUpdate($hash, "From", $from);
  368. readingsBulkUpdate($hash, "Subject", $subject);
  369. readingsEndUpdate($hash, 1);
  370. }
  371. $client->delete_message( $resp ) if( AttrVal($name, "delete_message", 0) == 1 );
  372. }
  373. $client->idle;
  374. } elsif ( $resp =~ /^\*\s+(BYE)/ ) {
  375. mailcheck_Disconnect($hash);
  376. mailcheck_Connect($hash);
  377. return undef;
  378. }
  379. }
  380. $hash->{tag} ||= $client->idle;
  381. unless ( $client->IsConnected ) {
  382. mailcheck_Disconnect($hash);
  383. mailcheck_Connect($hash);
  384. }
  385. }
  386. 1;
  387. =pod
  388. =item device
  389. =item summary watches an mailbox
  390. =item summary_DE &uuml;berwacht eine Mailbox
  391. =begin html
  392. <a name="mailcheck"></a>
  393. <h3>mailcheck</h3>
  394. <ul>
  395. Watches a mailbox with imap idle and for each new mail triggers an event with the subject of this mail.<br><br>
  396. This can be used to send mails *to* FHEM and react to them from a notify. Application scenarios are for example
  397. a geofencing apps on mobile phones, networked devices that inform about warning or failure conditions by e-mail or
  398. (with a little logic in FHEM) the absence of regular status messages from such devices and so on.<br><br>
  399. Notes:
  400. <ul>
  401. <li>Mail::IMAPClient and IO::Socket::SSL and IO::Socket::INET hast to be installed on the FHEM host.</li>
  402. <li>Probably only works reliably if no other mail programm is marking messages as read at the same time.</li>
  403. <li>If you experience a hanging system caused by regular forced disconnects of your internet provider you
  404. can disable and enable the mailcheck instance with an <a href="#at">at</a>.</li>
  405. <li>If MIME::Parser is installed non ascii subjects will be docoded to utf-8</li>
  406. <li>If MIME::Parser and Mail::GnuPG are installed gpg signatures can be checked and mails from unknown senders can be ignored.</li>
  407. </ul><br>
  408. <a name="mailcheck_Define"></a>
  409. <b>Define</b>
  410. <ul>
  411. <code>define &lt;name&gt; mailcheck &lt;host&gt; &lt;user&gt; &lt;password&gt; [&lt;folder&gt;]</code><br>
  412. <br>
  413. &lt;user&gt; and &lt;password&gt; can be of the form {perl-code}. no spaces are allowed. for both evals $NAME and $HOST is set to the name and host of the mailcheck device and $USER is set to the user in the password eval.
  414. <br>
  415. Defines a mailcheck device.<br><br>
  416. Examples:
  417. <ul>
  418. <code>define mailcheck mailcheck imap.mail.me.com x.y@me.com myPassword</code><br>
  419. <code>define mailcheck mailcheck imap.mail.me.com {"x.y@me.com"} {myPasswordOfAccount($USER)}</code><br>
  420. </ul>
  421. </ul><br>
  422. <a name="mailcheck_Readings"></a>
  423. <b>Readings</b>
  424. <ul>
  425. <li>Subject<br>
  426. the subject of the last mail received</li>
  427. <li>From<br>
  428. the mail address of the last sender</li>
  429. </ul><br>
  430. <a name="mailcheck_Set"></a>
  431. <b>Set</b>
  432. <ul>
  433. <li>inactive<br>
  434. temporarily deactivates the device</li>
  435. <li>active<br>
  436. reenables the device</li>
  437. </ul><br>
  438. <a name="mailcheck_Get"></a>
  439. <b>Get</b>
  440. <ul>
  441. <li>update<br>
  442. trigger an update</li>
  443. <li>folders<br>
  444. list available folders</li>
  445. </ul><br>
  446. <a name="mailcheck_Attr"></a>
  447. <b>Attributes</b>
  448. <ul>
  449. <li>delete_message<br>
  450. 1 -> delete message after Subject reading is created</li>
  451. <li>interval<br>
  452. the interval in seconds used to trigger an update on the connection.
  453. if idle is supported the defailt is 600, without idle support the default is 60. the minimum is 60.</li>
  454. <li>nossl<br>
  455. 1 -> don't use ssl.</li><br>
  456. <li>disable<br>
  457. 1 -> disconnect and stop polling</li>
  458. <li>debug<br>
  459. 1 -> enables debug output. default target is stdout.</li>
  460. <li>logfile<br>
  461. set the target for debug messages if debug is enabled.</li>
  462. <li>accept_from<br>
  463. comma separated list of gpg keys that will be accepted for signed messages. Mail::GnuPG and MIME::Parser have to be installed</li>
  464. </ul>
  465. </ul>
  466. =end html
  467. =cut