PageRenderTime 65ms CodeModel.GetById 26ms RepoModel.GetById 0ms app.codeStats 0ms

/perl/lib/Mail/Rspamd/Client.pm

https://bitbucket.org/vstakhov/rspamd/
Perl | 1390 lines | 1083 code | 296 blank | 11 comment | 172 complexity | 49c006eb85506f061b5134bcb7a1926c MD5 | raw file
Possible License(s): BSD-3-Clause
  1. =head1 NAME
  2. Mail::Rspamd::Client - Client for rspamd Protocol
  3. =head1 SYNOPSIS
  4. my $client = new Mail::Rspamd::Client($config);
  5. if ($client->ping()) {
  6. $self->{error} = "Ping is ok\n";
  7. }
  8. my $result = $client->check($testmsg);
  9. if ($result->{'default'}->{isspam} eq 'True') {
  10. do something with spam message here
  11. }
  12. =head1 DESCRIPTION
  13. Mail::Rspamd::Client is a module that provides a perl implementation for
  14. the spamd protocol.
  15. =cut
  16. package Mail::Rspamd::Client;
  17. use IO::Socket;
  18. use Carp;
  19. use vars qw($VERSION);
  20. $VERSION = "1.02";
  21. my $EOL = "\015\012";
  22. my $BLANK = $EOL x 2;
  23. my $PROTOVERSION = 'RSPAMC/1.2';
  24. =head1 PUBLIC METHODS
  25. =head2 new
  26. public class (Mail::Rspamd::Client) new (\% $args)
  27. Description:
  28. This method creates a new Mail::Rspamd::Client object.
  29. =cut
  30. sub new {
  31. my ($class, $args) = @_;
  32. $class = ref($class) || $class;
  33. my $self = {};
  34. # with a sockets_path set then it makes no sense to set host and port
  35. if ($args->{hosts}) {
  36. $self->{hosts} = $args->{hosts};
  37. $self->{alive_hosts} = $self->{hosts};
  38. }
  39. if ($args->{username}) {
  40. $self->{username} = $args->{username};
  41. }
  42. if ($args->{ip}) {
  43. $self->{ip} = $args->{ip};
  44. }
  45. if ($args->{from}) {
  46. $self->{from} = $args->{from};
  47. }
  48. if ($args->{subject}) {
  49. $self->{subject} = $args->{subject};
  50. }
  51. if ($args->{rcpt}) {
  52. $self->{rcpt} = $args->{rcpt};
  53. }
  54. if ($args->{deliver_to}) {
  55. $self->{deliver_to} = $args->{deliver_to};
  56. }
  57. if ($args->{timeout}) {
  58. $self->{timeout} = $args->{timeout};
  59. }
  60. else {
  61. $self->{timeout} = 5;
  62. }
  63. if ($args->{password}) {
  64. $self->{password} = $args->{password};
  65. }
  66. if ($args->{statfile}) {
  67. $self->{statfile} = $args->{statfile};
  68. }
  69. if ($args->{weight}) {
  70. $self->{weight} = $args->{weight};
  71. }
  72. else {
  73. $self->{weight} = 1;
  74. }
  75. if ($args->{pass_all}) {
  76. $self->{pass_all} = 1;
  77. }
  78. if ($args->{imap_search}) {
  79. $self->{imap_search} = $args->{imap_search};
  80. }
  81. else {
  82. $self->{imap_search} = 'ALL';
  83. }
  84. if ($args->{command}) {
  85. if ($args->{command} =~ /(SYMBOLS|PROCESS|CHECK|URLS|EMAILS)/i) {
  86. $self->{'command'} = $1;
  87. $self->{'control'} = 0;
  88. }
  89. elsif ($args->{command} =~ /(STAT|LEARN|SHUTDOWN|RELOAD|UPTIME|COUNTERS|FUZZY_ADD|FUZZY_DEL|WEIGHTS)/i) {
  90. $self->{'command'} = $1;
  91. $self->{'control'} = 1;
  92. }
  93. }
  94. $self->{error} = "";
  95. bless($self, $class);
  96. $self;
  97. }
  98. sub make_ssl_socket {
  99. my ($host, $port) = @_;
  100. eval {
  101. require IO::Socket::SSL;
  102. IO::Socket::SSL->import(LIST);
  103. } or croak "IO::Socket::SSL required for imaps";
  104. return IO::Socket::SSL->new("$host:$port");
  105. }
  106. =head2 process_item
  107. public instance (\%) process_item (String $item)
  108. Description:
  109. Do specified command for a single file, path or IMAP folder
  110. The return value is a hash reference containing results of each command for each server from cluster
  111. =cut
  112. sub process_item {
  113. my $self = shift;
  114. my $item = shift;
  115. my $cb = shift;
  116. if (defined ($item)) {
  117. if ($item =~ qr|^imap(s?):user:([^:]+):password:([^:]*):host:([^:]+):mbox:(.+)$|) {
  118. return $self->_process_imap ($1, $2, $3, $4, $5, $cb);
  119. }
  120. elsif (-f $item) {
  121. return $self->_process_file ($item, $cb);
  122. }
  123. elsif (-d $item) {
  124. return $self->_process_directory ($item, $cb);
  125. }
  126. else {
  127. warn "urecognized argument: $item";
  128. }
  129. }
  130. undef;
  131. }
  132. =head2 process_path
  133. public instance (\%) process_path ()
  134. Description:
  135. Do specified command for each file in path or message in IMAP folder
  136. The return value is a hash reference containing results of each command for each server from cluster
  137. =cut
  138. sub process_path {
  139. my $self = shift;
  140. my $cb = shift;
  141. my %res;
  142. foreach (@_) {
  143. $res{$_} = $self->process_item($_, $cb);
  144. }
  145. return \%res;
  146. }
  147. =head2 do_all_cmd
  148. public instance (\%) do_all_cmd (String $msg)
  149. Description:
  150. This method makes a call to the the whole rspamd cluster and call specified command
  151. (in $self->{command}).
  152. The return value is a hash reference containing results of each command for each server from cluster
  153. =cut
  154. sub do_all_cmd {
  155. my ($self, $input) = @_;
  156. my %res;
  157. if (!$self->{'hosts'} || scalar (@{ $self->{'hosts'} }) == 0) {
  158. $res{'error'} = 'Hosts list is empty';
  159. $res{'error_code'} = 404;
  160. }
  161. else {
  162. foreach my $hostdef (@{ $self->{'hosts'} }) {
  163. $self->_clear_errors();
  164. my $remote = $self->_create_connection($hostdef);
  165. if (! $remote) {
  166. $res{$hostdef}->{error_code} = 404;
  167. $res{$hostdef}->{error} = "Cannot connect to $hostdef";
  168. }
  169. else {
  170. if ($self->{'control'}) {
  171. $res{$hostdef} = $self->_do_control_command ($remote, $input);
  172. }
  173. else {
  174. $res{$hostdef} = $self->_do_rspamc_command ($remote, $input);
  175. }
  176. }
  177. }
  178. }
  179. return \%res;
  180. }
  181. =head2 do_cmd
  182. public instance (\%) do_cmd (String $msg)
  183. Description:
  184. This method makes a call to a single rspamd server from a cluster
  185. (in $self->{command}).
  186. The return value is a hash reference containing results of each command for each server from cluster
  187. =cut
  188. sub do_cmd {
  189. my ($self, $input) = @_;
  190. my $res;
  191. if (!$self->{'hosts'} || scalar (@{ $self->{'hosts'} }) == 0) {
  192. $res->{'error'} = 'Hosts list is empty';
  193. $res->{'error_code'} = 404;
  194. }
  195. else {
  196. $self->_clear_errors();
  197. my $remote = $self->_create_connection();
  198. if (! $remote) {
  199. $res->{error_code} = 404;
  200. $res->{error} = "Cannot connect to " . $remote;
  201. }
  202. else {
  203. if ($self->{'control'}) {
  204. $res = $self->_do_control_command ($remote, $input);
  205. }
  206. else {
  207. $res = $self->_do_rspamc_command ($remote, $input);
  208. }
  209. }
  210. }
  211. return $res;
  212. }
  213. =head2 check
  214. public instance (\%) check (String $msg)
  215. Description:
  216. This method makes a call to the spamd server and depending on the value of
  217. C<$is_check_p> either calls PROCESS or CHECK.
  218. The return value is a hash reference containing metrics indexed by name. Each metric
  219. is hash that contains data:
  220. =over
  221. =item *
  222. isspam
  223. =item *
  224. score
  225. =item *
  226. threshold
  227. =item *
  228. symbols - array of symbols
  229. =back
  230. =cut
  231. sub check {
  232. my ($self, $msg) = @_;
  233. $self->{command} = 'CHECK';
  234. $self->{control} = 0;
  235. return $self->do_cmd ($msg);
  236. }
  237. =head2 symbols
  238. public instance (\%) symbols (String $msg)
  239. Description:
  240. This method makes a call to the spamd server
  241. The return value is a hash reference containing metrics indexed by name. Each metric
  242. is hash that contains data:
  243. =over
  244. =item *
  245. isspam
  246. =item *
  247. score
  248. =item *
  249. threshold
  250. =item *
  251. symbols - array of symbols
  252. =back
  253. =cut
  254. sub symbols {
  255. my ($self, $msg) = @_;
  256. $self->{command} = 'SYMBOLS';
  257. $self->{control} = 0;
  258. return $self->do_cmd ($msg);
  259. }
  260. =head2 process
  261. public instance (\%) process (String $msg)
  262. Description:
  263. This method makes a call to the spamd server
  264. The return value is a hash reference containing metrics indexed by name. Each metric
  265. is hash that contains data:
  266. =over
  267. =item *
  268. isspam
  269. =item *
  270. score
  271. =item *
  272. threshold
  273. =item *
  274. symbols - array of symbols
  275. =back
  276. =cut
  277. sub process {
  278. my ($self, $msg) = @_;
  279. $self->{command} = 'PROCESS';
  280. $self->{control} = 0;
  281. return $self->do_cmd ($msg);
  282. }
  283. =head2 urls
  284. public instance (\%) urls (String $msg)
  285. Description:
  286. This method makes a call to the spamd server
  287. The return value is a hash reference containing metrics indexed by name. Each metric
  288. is hash that contains data:
  289. urls - list of all urls in message
  290. =cut
  291. sub urls {
  292. my ($self, $msg) = @_;
  293. $self->{command} = 'URLS';
  294. $self->{control} = 0;
  295. return $self->do_cmd ($msg);
  296. }
  297. =head2 learn
  298. public instance (\%) learn (String $msg)
  299. Description:
  300. This method makes a call to the spamd learning a statfile with message.
  301. =cut
  302. sub learn {
  303. my ($self, $msg) = @_;
  304. $self->{command} = 'learn';
  305. $self->{control} = 1;
  306. return $self->do_cmd ($msg);
  307. }
  308. =head2 weights
  309. public instance (\%) weights (String $msg)
  310. Description:
  311. This method makes a call to the spamd showing weights of message by each statfile.
  312. =cut
  313. sub weights {
  314. my ($self, $msg) = @_;
  315. $self->{command} = 'weights';
  316. $self->{control} = 1;
  317. return $self->do_cmd ($msg);
  318. }
  319. =head2 fuzzy_add
  320. public instance (\%) fuzzy_add (String $msg)
  321. Description:
  322. This method makes a call to the spamd adding specified message to fuzzy storage.
  323. =cut
  324. sub fuzzy_add {
  325. my ($self, $msg) = @_;
  326. $self->{command} = 'fuzzy_add';
  327. $self->{control} = 1;
  328. return $self->do_cmd ($msg);
  329. }
  330. =head2 fuzzy_del
  331. public instance (\%) fuzzy_add (String $msg)
  332. Description:
  333. This method makes a call to the spamd removing specified message from fuzzy storage.
  334. =cut
  335. sub fuzzy_del {
  336. my ($self, $msg) = @_;
  337. $self->{command} = 'fuzzy_del';
  338. $self->{control} = 1;
  339. return $self->do_cmd ($msg);
  340. }
  341. =head2 stat
  342. public instance (\%) stat ()
  343. Description:
  344. This method makes a call to the spamd and get statistics.
  345. =cut
  346. sub stat {
  347. my ($self) = @_;
  348. $self->{command} = 'stat';
  349. $self->{control} = 1;
  350. return $self->do_cmd (undef);
  351. }
  352. =head2 uptime
  353. public instance (\%) uptime ()
  354. Description:
  355. This method makes a call to the spamd and get uptime.
  356. =cut
  357. sub uptime {
  358. my ($self) = @_;
  359. $self->{command} = 'uptime';
  360. $self->{control} = 1;
  361. return $self->do_cmd (undef);
  362. }
  363. =head2 counters
  364. public instance (\%) counters ()
  365. Description:
  366. This method makes a call to the spamd and get counters.
  367. =cut
  368. sub counters {
  369. my ($self) = @_;
  370. $self->{command} = 'counters';
  371. $self->{control} = 1;
  372. return $self->do_cmd (undef);
  373. }
  374. =head2 ping
  375. public instance (Boolean) ping ()
  376. Description:
  377. This method performs a server ping and returns 0 or 1 depending on
  378. if the server responded correctly.
  379. =cut
  380. sub ping {
  381. my $self = shift;
  382. my $host = shift;
  383. my $remote;
  384. $self->{control} = 0;
  385. if (defined($host)) {
  386. $remote = $self->_create_connection($host);
  387. }
  388. else {
  389. # Create connection to random host from cluster
  390. $remote = $self->_create_connection();
  391. }
  392. return undef unless $remote;
  393. local $SIG{PIPE} = 'IGNORE';
  394. if (!(syswrite($remote, "PING $PROTOVERSION$EOL"))) {
  395. $self->_mark_dead($remote);
  396. close($remote);
  397. return 0;
  398. }
  399. syswrite($remote, $EOL);
  400. return undef unless $self->_get_io_readiness($remote, 0);
  401. my $line;
  402. sysread ($remote, $line, 255);
  403. close $remote;
  404. return undef unless $line;
  405. my ($version, $resp_code, $resp_msg) = $self->_parse_response_line($line);
  406. return 0 unless (defined($resp_msg) && $resp_msg eq 'PONG');
  407. return 1;
  408. }
  409. =head1 PRIVATE METHODS
  410. =head2 _connect_host
  411. private instance (IO::Socket) _create_host ($def)
  412. Description:
  413. This method sets up a proper IO::Socket connection based on the arguments
  414. used when greating the client object.
  415. On failure, it sets an internal error code and returns undef.
  416. =cut
  417. sub _connect_host {
  418. my ($self, $hostdef) = @_;
  419. my $remote;
  420. if ($hostdef =~ /^\//) {
  421. if (! socket ($remote, PF_UNIX, SOCK_STREAM, 0)) {
  422. carp "Cannot create unix socket\n";
  423. return undef;
  424. }
  425. my $sun = sockaddr_un($hostdef);
  426. if (!connect ($remote, $sun)) {
  427. carp "Cannot connect to socket $hostdef\n";
  428. close $remote;
  429. return undef;
  430. }
  431. }
  432. elsif ($hostdef =~ /^\s*(([^:]+):(\d+))\s*$/) {
  433. my $peer_addr = $2;
  434. if ($2 eq '*') {
  435. $peer_addr = '127.0.0.1';
  436. }
  437. $remote = IO::Socket::INET->new( Proto => "tcp",
  438. PeerAddr => $peer_addr,
  439. PeerPort => $3,
  440. Blocking => 0,
  441. );
  442. # Get write readiness
  443. if (defined ($remote)) {
  444. if ($self->_get_io_readiness($remote, 1) != 0) {
  445. return $remote;
  446. }
  447. else {
  448. close ($remote);
  449. return undef;
  450. }
  451. }
  452. }
  453. elsif ($hostdef =~ /^\s*([^:]+)\s*$/) {
  454. my $peer_addr = $1;
  455. if ($1 eq '*') {
  456. $peer_addr = '127.0.0.1';
  457. }
  458. $remote = IO::Socket::INET->new( Proto => "tcp",
  459. PeerAddr => $peer_addr,
  460. PeerPort => $self->{control} ? 11334 : 11333,
  461. Blocking => 0,
  462. );
  463. # Get write readiness
  464. if (defined ($remote)) {
  465. if ($self->_get_io_readiness($remote, 1) != 0) {
  466. return $remote;
  467. }
  468. else {
  469. close ($remote);
  470. return undef;
  471. }
  472. }
  473. }
  474. unless ($remote) {
  475. $self->{error} = "Failed to create connection to spamd daemon: $!\n";
  476. return undef;
  477. }
  478. $remote;
  479. }
  480. =head2 _create_connection
  481. private instance (IO::Socket) _create_connection ()
  482. Description:
  483. This method sets up a proper IO::Socket connection based on the arguments
  484. used when greating the client object.
  485. On failure, it sets an internal error code and returns undef.
  486. =cut
  487. sub _create_connection {
  488. my ($self, $hostdef) = @_;
  489. my $tries = 0;
  490. if (!defined ($hostdef)) {
  491. my $server;
  492. do {
  493. $server = $self->_select_server();
  494. $tries ++;
  495. my $remote = $self->_connect_host ($server);
  496. return $remote if $remote;
  497. } while ($tries < 5);
  498. return undef;
  499. }
  500. return $self->_connect_host ($hostdef);
  501. }
  502. =head2 _auth
  503. private instance (IO::Socket) _auth (Socket sock)
  504. Description:
  505. This method do control auth.
  506. On failure this method returns 0
  507. =cut
  508. sub _auth {
  509. my ($self, $sock) = @_;
  510. local $SIG{PIPE} = 'IGNORE';
  511. if (!(syswrite($sock, "password $self->{password}$EOL"))) {
  512. $self->_mark_dead($remote);
  513. return 0;
  514. }
  515. return 0 unless $self->_get_io_readiness($sock, 0);
  516. if (sysread($sock, $reply, 255)) {
  517. if ($reply =~ /^password accepted/) {
  518. return 0 unless $self->_get_io_readiness($sock, 0);
  519. # read "END"
  520. sysread($sock, $reply, 255);
  521. return 1;
  522. }
  523. }
  524. return 0;
  525. }
  526. =head2 _revive_dead
  527. private instance (IO::Socket) _revive_dead ()
  528. Description:
  529. This method marks dead upstreams as alive
  530. =cut
  531. sub _revive_dead {
  532. my ($self) = @_;
  533. my $now = time();
  534. foreach my $s ($self->{dead_hosts}) {
  535. # revive after minute of downtime
  536. if (defined($s->{dead}) && $s->{dead} == 1 && $now - $s->{t} > 60) {
  537. $s->{dead} = 0;
  538. push(@{$self->{alive_hosts}}, $s->{host});
  539. }
  540. }
  541. 1;
  542. }
  543. =head2 _select_server
  544. private instance (IO::Socket) _select_server ()
  545. Description:
  546. This method returns one server from rspamd cluster or undef if there are no suitable ones
  547. =cut
  548. sub _select_server {
  549. my($self) = @_;
  550. return undef unless $self->{alive_hosts};
  551. $self->_revive_dead();
  552. my $alive_num = scalar(@{$self->{alive_hosts}});
  553. if (!$alive_num) {
  554. $self->{alive_hosts} = $self->{hosts};
  555. $self->{dead_hosts} = ();
  556. $alive_num = scalar($self->{alive_hosts});
  557. }
  558. my $selected = $self->{alive_hosts}[int(rand($alive_num))];
  559. $selected;
  560. }
  561. =head2 _select_server
  562. private instance (IO::Socket) _mark_dead (String server)
  563. Description:
  564. This method marks upstream as dead for some time. It can be revived by _revive_dead method
  565. =cut
  566. sub _mark_dead {
  567. my ($self, $server) = @_;
  568. return undef unless $self->{hosts};
  569. my $now = time();
  570. $self->{dead_hosts}->{$server} = {
  571. host => $server,
  572. dead => 1,
  573. t => $now,
  574. };
  575. for (my $i = 0; $i < scalar (@{$self->{alive_hosts}}); $i ++) {
  576. if ($self->{alive_hosts} == $server) {
  577. splice(@{$self->{alive_hosts}}, $i, 1);
  578. last;
  579. }
  580. }
  581. }
  582. =head2 _get_io_readiness
  583. private instance (IO::Socket) _mark_dead (String server)
  584. Description:
  585. This method marks upstream as dead for some time. It can be revived by _revive_dead method
  586. =cut
  587. sub _get_io_readiness {
  588. my ($self, $sock, $is_write) = @_;
  589. my $w = '';
  590. vec($w, fileno($sock), 1) = 1;
  591. if ($is_write) {
  592. return select(undef, $w, undef, $self->{timeout});
  593. }
  594. else {
  595. return select($w, undef,undef, $self->{timeout});
  596. }
  597. undef;
  598. }
  599. =head2 _parse_response_line
  600. private instance (@) _parse_response_line (String $line)
  601. Description:
  602. This method parses the initial response line/header from the server
  603. and returns its parts.
  604. We have this as a seperate method in case we ever decide to get fancy
  605. with the response line.
  606. =cut
  607. sub _parse_response_line {
  608. my ($self, $line) = @_;
  609. $line =~ s/\r?\n$//;
  610. return split(/\s+/, $line, 3);
  611. }
  612. sub _write_message {
  613. my $self = shift;
  614. my $remote = shift;
  615. my $message = shift;
  616. my $len = shift;
  617. my $written = 0;
  618. while ($written < $len) {
  619. last unless ($self->_get_io_readiness($remote, 1));
  620. my $cur = syswrite $remote, $message, $len, $written;
  621. last if ($cur <= 0);
  622. $written += $cur;
  623. }
  624. return $written == $len;
  625. }
  626. =head2 _clear_errors
  627. private instance () _clear_errors ()
  628. Description:
  629. This method clears out any current errors.
  630. =cut
  631. sub _clear_errors {
  632. my ($self) = @_;
  633. $self->{resp_code} = undef;
  634. $self->{resp_msg} = undef;
  635. $self->{error} = undef;
  636. }
  637. # Currently just read stdin for user's message and pass it to rspamd
  638. sub _do_rspamc_command {
  639. my ($self, $remote, $msg) = @_;
  640. my %metrics;
  641. my ($in, $res);
  642. my $msgsize = length($msg);
  643. local $SIG{PIPE} = 'IGNORE';
  644. if (!(syswrite($remote, "$self->{command} $PROTOVERSION$EOL"))) {
  645. $self->_mark_dead($remote);
  646. my %r = (
  647. error => 'cannot connect to rspamd',
  648. error_code => 502,
  649. );
  650. close($remote);
  651. return \%r;
  652. }
  653. syswrite $remote, "Content-length: $msgsize$EOL";
  654. syswrite $remote, "User: $self->{username}$EOL" if (exists($self->{username}));
  655. syswrite $remote, "From: $self->{from}$EOL" if (exists($self->{from}));
  656. syswrite $remote, "IP: $self->{ip}$EOL" if (exists($self->{ip}));
  657. syswrite $remote, "Deliver-To: $self->{deliver_to}$EOL" if (exists($self->{deliver_to}));
  658. syswrite $remote, "Subject: $self->{subject}$EOL" if (exists($self->{subject}));
  659. syswrite $remote, "Pass: all$EOL" if (exists($self->{pass_all}) && $self->{pass_all});
  660. if (ref $self->{rcpt} eq "ARRAY") {
  661. foreach ($self->{rcpt}) {
  662. syswrite $remote, "Rcpt: $_ $EOL";
  663. }
  664. }
  665. syswrite $remote, $EOL;
  666. if (! $self->_write_message($remote, $msg, $msgsize)) {
  667. my %r = (
  668. error => 'error writing message to rspamd',
  669. error_code => 502,
  670. );
  671. close $remote;
  672. return \%r;
  673. }
  674. #syswrite $remote, $EOL;
  675. unless ($self->_get_io_readiness($remote, 0)) {
  676. close $remote;
  677. my %r = (
  678. error => 'timed out while waiting for reply',
  679. error_code => 502,
  680. );
  681. return \%r;
  682. }
  683. my $offset = 0;
  684. do {
  685. $res = sysread($remote, $in, 512, $offset);
  686. if (!defined ($res)) {
  687. close $remote;
  688. my %r = (
  689. error => 'IO error while reading data from socket: ' . $!,
  690. error_code => 503,
  691. );
  692. return \%r;
  693. }
  694. if ($res > 0 && $res < 512) {
  695. $self->_get_io_readiness($remote, 0);
  696. }
  697. $offset += $res;
  698. } while ($res > 0);
  699. my ($version, $resp_code, $resp_msg) = $self->_parse_response_line($in);
  700. $self->{resp_code} = $resp_code;
  701. $self->{resp_msg} = $resp_msg;
  702. unless (defined($resp_code) && $resp_code == 0) {
  703. close $remote;
  704. my %r = (
  705. error => 'invalid reply',
  706. error_code => 500,
  707. );
  708. return \%r
  709. }
  710. my $cur_metric;
  711. my @lines = split (/^/, $in);
  712. if (lc $self->{'command'} eq 'urls') {
  713. $metrics{'default'} = {
  714. isspam => 'false',
  715. score => 0,
  716. threshold => 0,
  717. symbols => [],
  718. urls => [],
  719. messages => [],
  720. action => 'reject',
  721. };
  722. foreach my $line (@lines) {
  723. if ($line =~ /^Urls: (.+)$/) {
  724. @{ $metrics{'default'}->{'urls'} } = split /,\s+/, $1;
  725. }
  726. }
  727. }
  728. else {
  729. foreach my $line (@lines) {
  730. if ($line =~ m!Metric: (\S+); (\S+); (\S+) / (\S+) (/ (\S+))?!) {
  731. $metrics{$1} = {
  732. isspam => $2,
  733. score => $3 + 0,
  734. threshold => $4 + 0,
  735. reject_score => $6,
  736. symbols => [],
  737. urls => [],
  738. messages => [],
  739. action => 'no action',
  740. };
  741. $cur_metric = $1;
  742. }
  743. elsif ($line =~ /^Symbol: (\S+);\s*(.+?)\s*$/ && $cur_metric) {
  744. # Line with parameters
  745. my $symref = $metrics{$cur_metric}->{'symbols'};
  746. push(@$symref, "$1($2)");
  747. }
  748. elsif ($line =~ /^Symbol: (\S+?)\s*$/ && $cur_metric) {
  749. my $symref = $metrics{$cur_metric}->{'symbols'};
  750. push(@$symref, $1);
  751. }
  752. elsif ($line =~ /^Urls: (.+?)\s*$/ && $cur_metric) {
  753. @{ $metrics{$cur_metric}->{'urls'} } = split /,\s+/, $1;
  754. }
  755. elsif ($line =~ /^Message: (.+?)\s*$/ && $cur_metric) {
  756. my $symref = $metrics{$cur_metric}->{'messages'};
  757. push(@$symref, $1);
  758. }
  759. elsif ($line =~ /^Action: (.+?)\s*$/ && $cur_metric) {
  760. $metrics{$cur_metric}->{'action'} = $1;
  761. }
  762. elsif ($line =~ /^${EOL}$/) {
  763. last;
  764. }
  765. }
  766. }
  767. close $remote;
  768. return \%metrics;
  769. }
  770. sub _do_control_command {
  771. my ($self, $remote, $msg) = @_;
  772. local $SIG{PIPE} = 'IGNORE';
  773. my %res = (
  774. error_code => 0,
  775. error => '',
  776. );
  777. unless ($self->_get_io_readiness($remote, 0)) {
  778. $res{error} = "Timeout while reading data from socket";
  779. $res{error_code} = 501;
  780. close($remote);
  781. return \%res;
  782. }
  783. # Read greeting first
  784. if (defined (my $greeting = <$remote>)) {
  785. if ($greeting !~ /^Rspamd version/) {
  786. $res{error} = "Not rspamd greeting line $greeting";
  787. $res{error_code} = 500;
  788. close($remote);
  789. return \%res;
  790. }
  791. }
  792. if ($self->{'command'} =~ /^learn$/i) {
  793. if (!$self->{'statfile'}) {
  794. $res{error} = "Statfile is not specified to learn command";
  795. $res{error_code} = 500;
  796. close($remote);
  797. return \%res;
  798. }
  799. if ($self->_auth ($remote)) {
  800. my $len = length ($msg);
  801. syswrite $remote, "learn $self->{statfile} $len -m $self->{weight}" . $EOL;
  802. if (! $self->_write_message($remote, $msg, length($msg))) {
  803. $res{error} = 'error writing message to rspamd';
  804. $res{error_code} = 502;
  805. close $remote;
  806. return \%res;
  807. }
  808. unless ($self->_get_io_readiness($remote, 0)) {
  809. $res{error} = "Timeout while reading data from socket";
  810. $res{error_code} = 501;
  811. close($remote);
  812. return \%res;
  813. }
  814. if (defined (my $reply = <$remote>)) {
  815. if ($reply =~ /^learn ok, sum weight: ([0-9.]+)/) {
  816. $res{error} = "Learn succeed. Sum weight: $1\n";
  817. close($remote);
  818. return \%res;
  819. }
  820. else {
  821. $res{error_code} = 500;
  822. $res{error} = "Learn failed: $reply\n";
  823. close($remote);
  824. return \%res;
  825. }
  826. }
  827. }
  828. else {
  829. $res{error_code} = 403;
  830. $res{error} = "Authentication failed\n";
  831. close($remote);
  832. return \%res;
  833. }
  834. }
  835. elsif ($self->{'command'} =~ /^weights$/i) {
  836. if (!$self->{'statfile'}) {
  837. $res{error_code} = 500;
  838. $res{error} = "Statfile is not specified to weights command";
  839. close($remote);
  840. return \%res;
  841. }
  842. my $len = length ($msg);
  843. $res{error} = "Sending $len bytes...\n";
  844. syswrite $remote, "weights $self->{'statfile'} $len" . $EOL;
  845. if (! $self->_write_message($remote, $msg, length($msg))) {
  846. $res{error} = 'error writing message to rspamd';
  847. $res{error_code} = 502;
  848. close $remote;
  849. return \%res;
  850. }
  851. unless ($self->_get_io_readiness($remote, 0)) {
  852. $res{error} = "Timeout while reading data from socket";
  853. $res{error_code} = 501;
  854. close($remote);
  855. return \%res;
  856. }
  857. while (defined (my $reply = <$remote>)) {
  858. last if $reply =~ /^END/;
  859. $res{error} .= $reply;
  860. }
  861. }
  862. elsif ($self->{'command'} =~ /(reload|shutdown)/i) {
  863. if ($self->_auth ($remote)) {
  864. syswrite $remote, $self->{'command'} . $EOL;
  865. unless ($self->_get_io_readiness($remote, 0)) {
  866. $res{error} = "Timeout while reading data from socket";
  867. $res{error_code} = 501;
  868. close($remote);
  869. return \%res;
  870. }
  871. while (defined (my $line = <$remote>)) {
  872. last if $line =~ /^END/;
  873. $res{error} .= $line;
  874. }
  875. }
  876. else {
  877. $res{error_code} = 403;
  878. $res{error} = "Authentication failed\n";
  879. close($remote);
  880. return \%res;
  881. }
  882. }
  883. elsif ($self->{'command'} =~ /(fuzzy_add|fuzzy_del)/i) {
  884. if ($self->_auth ($remote)) {
  885. my $len = length ($msg);
  886. syswrite $remote, $self->{'command'} . " $len $self->{'weight'}" . $EOL;
  887. if (! $self->_write_message($remote, $msg, length($msg))) {
  888. $res{error} = 'error writing message to rspamd';
  889. $res{error_code} = 502;
  890. close $remote;
  891. return \%res;
  892. }
  893. unless ($self->_get_io_readiness($remote, 0)) {
  894. $res{error} = "Timeout while reading data from socket";
  895. $res{error_code} = 501;
  896. close($remote);
  897. return \%res;
  898. }
  899. if (defined (my $reply = <$remote>)) {
  900. if ($reply =~ /^OK/) {
  901. $res{error} = $self->{'command'} . " succeed\n";
  902. close($remote);
  903. return \%res;
  904. }
  905. else {
  906. $res{error_code} = 500;
  907. $res{error} = $self->{'command'} . " failed\n";
  908. close($remote);
  909. return \%res;
  910. }
  911. }
  912. }
  913. else {
  914. $res{error_code} = 403;
  915. $res{error} = "Authentication failed\n";
  916. close($remote);
  917. return \%res;
  918. }
  919. }
  920. else {
  921. syswrite $remote, $self->{'command'} . $EOL;
  922. unless ($self->_get_io_readiness($remote, 0)) {
  923. $res{error} = "Timeout while reading data from socket";
  924. $res{error_code} = 501;
  925. close($remote);
  926. return \%res;
  927. }
  928. while (defined (my $line = <$remote>)) {
  929. last if $line =~ /^END/;
  930. $res{error} .= $line;
  931. }
  932. }
  933. close($remote);
  934. return \%res;
  935. }
  936. sub _process_file {
  937. my $self = shift;
  938. my $file = shift;
  939. my $cb = shift;
  940. my $res;
  941. open(FILE, "< $file") or return;
  942. my $input;
  943. while (defined (my $line = <FILE>)) {
  944. $input .= $line;
  945. }
  946. close FILE;
  947. $res = $self->do_all_cmd ($input);
  948. if (defined ($cb) && $res) {
  949. $cb->($file, $res);
  950. }
  951. }
  952. sub _process_directory {
  953. my $self = shift;
  954. my $dir = shift;
  955. my $cb = shift;
  956. opendir (DIR, $dir) or return;
  957. while (defined (my $file = readdir (DIR))) {
  958. $file = "$dir/$file";
  959. if (-f $file) {
  960. $self->_process_file ($file, $cb);
  961. }
  962. }
  963. closedir (DIR);
  964. }
  965. sub _check_imap_reply {
  966. my $self = shift;
  967. my $sock = shift;
  968. my $seq = shift;
  969. my $input;
  970. while (defined ($input = <$sock>)) {
  971. chomp $input;
  972. if ($input =~ /BAD|NO (.+)$/) {
  973. $_[0] = $1;
  974. return 0;
  975. }
  976. next if ($input =~ /^\*/);
  977. if ($input =~ /^$seq OK/) {
  978. return 1;
  979. }
  980. $_[0] = $input;
  981. return 0;
  982. }
  983. $_[0] = "timeout";
  984. return 0;
  985. }
  986. sub _parse_imap_body {
  987. my $self = shift;
  988. my $sock = shift;
  989. my $seq = shift;
  990. my $input;
  991. my $got_body = 0;
  992. while (defined (my $line = <$sock>)) {
  993. if (!$got_body && $line =~ /^\*/) {
  994. $got_body = 1;
  995. next;
  996. }
  997. if ($line =~ /^$seq OK/) {
  998. return $input;
  999. }
  1000. elsif ($got_body) {
  1001. $input .= $line;
  1002. next;
  1003. }
  1004. return undef;
  1005. }
  1006. return undef;
  1007. }
  1008. sub _parse_imap_sequences {
  1009. my $self = shift;
  1010. my $sock = shift;
  1011. my $seq = shift;
  1012. my $input;
  1013. while (defined ($input = <$sock>)) {
  1014. chomp $input;
  1015. if ($input =~ /^\* SEARCH (.+)$/) {
  1016. @res = split (/\s/, $1);
  1017. next;
  1018. }
  1019. elsif ($input =~ /^$seq OK/) {
  1020. return \@res;
  1021. }
  1022. return undef;
  1023. }
  1024. }
  1025. sub _process_imap {
  1026. my ($self, $ssl, $user, $password, $host, $mbox, $cb) = @_;
  1027. my $seq = 1;
  1028. my $sock;
  1029. my $res;
  1030. if (!$password) {
  1031. eval {
  1032. require Term::ReadKey;
  1033. Term::ReadKey->import( qw(ReadMode ReadLine) );
  1034. print "Enter IMAP password: ";
  1035. ReadMode(2);
  1036. $password = ReadLine(0);
  1037. chomp $password;
  1038. ReadMode(0);
  1039. print "\n";
  1040. } or croak "cannot get password. Check that Term::ReadKey is installed";
  1041. }
  1042. # Stupid code that does not take care of timeouts etc, just trying to extract messages
  1043. if ($ssl) {
  1044. $sock = $self->_make_ssl_socket ($host, 'imaps');
  1045. }
  1046. else {
  1047. $sock = IO::Socket::INET->new( Proto => "tcp",
  1048. PeerAddr => $host,
  1049. PeerPort => 'imap',
  1050. Blocking => 1,
  1051. );
  1052. }
  1053. unless ($sock) {
  1054. $self->{error} = "Cannot connect to imap server: $!";
  1055. return;
  1056. }
  1057. my $reply = <$sock>;
  1058. if (!defined ($reply) || $reply !~ /^\* OK/) {
  1059. $self->{error} = "Imap server is not ready";
  1060. return;
  1061. }
  1062. syswrite $sock, "$seq LOGIN $user $password$EOL";
  1063. if (!$self->_check_imap_reply ($sock, $seq, $reply)) {
  1064. $self->{error} = "Cannot login to imap server: $reply";
  1065. return;
  1066. }
  1067. $seq ++;
  1068. syswrite $sock, "$seq SELECT $mbox$EOL";
  1069. if (!$self->_check_imap_reply ($sock, $seq, $reply)) {
  1070. $self->{error} = "Cannot select mbox $mbox: $reply";
  1071. return;
  1072. }
  1073. $seq ++;
  1074. syswrite $sock, "$seq SEARCH $self->{imap_search}$EOL";
  1075. my $messages;
  1076. if (!defined ($messages = $self->_parse_imap_sequences ($sock, $seq))) {
  1077. $self->{error} = "Cannot make search";
  1078. return;
  1079. }
  1080. $seq ++;
  1081. foreach my $message (@{ $messages }){
  1082. syswrite $sock, "$seq FETCH $message body[]$EOL";
  1083. if (defined (my $input = $self->_parse_imap_body ($sock, $seq))) {
  1084. $self->do_all_cmd ($input);
  1085. if (defined ($cb) && $res) {
  1086. $cb->($seq, $res);
  1087. }
  1088. }
  1089. $seq ++;
  1090. }
  1091. syswrite $sock, "$seq LOGOUT$EOL";
  1092. close $sock;
  1093. }
  1094. 1;