#!/usr/bin/perl use strict; use warnings; use English; use Getopt::Long; use Pod::Usage; use NGCP::API::Client; use Readonly; use XML::Simple; use Template; use Email::Sender::Simple qw(); use Email::Simple; use Email::Simple::Creator; use Email::Sender::Transport::Sendmail qw(); use POSIX qw(strftime); use File::Pid; Readonly my @required => qw(); Readonly my $config_file => '/etc/ngcp-panel/provisioning.conf'; my $PROGRAM_BASE = "ngcp-fraud-notifier"; my $retcode = 0; my $piddir = "/run/ngcp-fraud-notifier"; my $pidfile = "$piddir/ngcp-fraud-notifier.pid"; my $pf = File::Pid->new({ file => $pidfile }); local $PROGRAM_NAME = $PROGRAM_BASE; local $OUTPUT_AUTOFLUSH = 1; local $SIG{INT} = \&cleanup; my $page_size = 100; my $opts = { verbose => 0, }; my $config; GetOptions( $opts, "help|h" => sub { usage() }, "verbose", ) or usage(); sub DESTROY { cleanup(); } sub check_params { my @missing; foreach my $param (@required) { push @missing, $param unless $opts->{$param}; } usage(join(' ', @missing)) if scalar @missing; return; } sub usage { my $msg = shift; pod2usage(-exitval => $msg ? 1 : 0, -verbose => 1, -message => $msg ? $msg =~ /not found/i ? $msg : "Missing parameters: $msg" : '', ); return; } sub load_config { $config = XML::Simple->new()->XMLin($config_file, ForceArray => 0) or die "Cannot load config: $config_file: $ERRNO"; return; } sub get_data { my ($uri, $link, $code) = @_; my $client = new NGCP::API::Client; $client->set_verbose($opts->{verbose}); $client->set_page_rows($page_size); my @result = (); while (my $res = $client->next_page($uri)) { die $res->result unless $res->is_success; my $res_hash = $res->as_hash; my $data = $res_hash->{_embedded}{'ngcp:'.$link}; if ('ARRAY' eq ref $data) { push(@result,@$data); } elsif ($data) { push(@result,$data); } if (defined $code and 'CODE' eq ref $code) { &$code(\@result); @result = (); } } return \@result; } sub get_email_template { my $event = shift; my $lock_type = $event->{interval_lock} ? 'lock' : 'warning'; my $reseller_id = $event->{reseller_id}; my @templates_data = (); foreach my $reseller ($event->{reseller_id}, 'NULL') { @templates_data = ( @templates_data, @{get_data(sprintf('/api/emailtemplates/?reseller_id=%s', $reseller), 'emailtemplates')}); } my $selected_template; foreach my $template (@templates_data) { next if $template->{name} ne 'customer_fraud_'.$lock_type.'_default_email' && $template->{name} ne 'customer_fraud_'.$lock_type.'_email'; next if $template->{reseller_id} && $template->{reseller_id} != $reseller_id; $selected_template = $template; last if $template->{reseller_id}; } unless ($selected_template) { die sprintf "No template 'customer_fraud_%s_default_email' OR 'customer_fraud_%s_email' is defined.", $lock_type, $lock_type; } return $selected_template; } sub send_email { my ($event, $subscribers) = @_; my $template = get_email_template($event); my $vars = { adminmail => $config->{adminmail}, customer_id => $event->{contract_id}, interval => $event->{interval}, interval_cost => sprintf('%.2f', $event->{interval_cost}/100), interval_limit => sprintf('%.2f', $event->{interval_limit}/100), type => $event->{type} eq 'profile_limit' ? 'billing profile' : 'customer', }; foreach my $subscriber (@{$subscribers}) { $vars->{subscribers} .= sprintf "%s\@%s %s\n", @{$subscriber}{qw(username domain)}, $subscriber->{external_id} ? '('.$subscriber->{external_id}.')' : ''; } my $tt = Template->new(); map { my $out; $tt->process(\$template->{$_}, $vars, \$out); $out and $template->{$_} = $out; } keys %{$template}; die "'To' header is empty in the email" unless $event->{interval_notify}; die "'From' header is empty in the email" unless $template->{from_email}; die "'Subject' header is empty in the email" unless $template->{subject}; my $transport = Email::Sender::Transport::Sendmail->new; my $email = Email::Simple->create( header => [ To => $event->{interval_notify}, From => $template->{from_email}, Subject => $template->{subject}, ], body => $template->{body}, ); Email::Sender::Simple->send($email, { transport => $transport }) or die sprintf "Cannot send fraud daily lock notification to %s: $ERRNO", $email->header('To'); update_notify_status($event); return; } sub update_notify_status { my $event = shift; my $client = new NGCP::API::Client; $client->set_verbose($opts->{verbose}); my $now = strftime('%Y-%m-%d %H:%M:%S', localtime(time)); my $uri = '/api/customerfraudevents/'.$event->{id}; my $data = [ { op => 'replace', path => '/notify_status', value => 'notified' }, { op => 'replace', path => '/notified_at', value => $now } ]; my $res = $client->request("PATCH", $uri, $data); return; } sub main { if (my $num = $pf->running) { print "$PROGRAM_BASE is already running.\n"; $retcode = 1; exit $retcode; } mkdir $piddir unless -e $piddir; $pf->write; check_params(); load_config(); get_data( sprintf('/api/customerfraudevents/?no_count=true¬ify_status=%s','new'),'customerfraudevents', sub { my $events = shift; foreach my $event (@{$events}) { if ($event->{interval_notify}) { my $subscribers = get_data(sprintf('/api/subscribers/?customer_id=%d', $event->{contract_id}), 'subscribers'); next unless scalar @$subscribers; eval { send_email($event, $subscribers); }; print $EVAL_ERROR if $EVAL_ERROR; } } }); return; } sub cleanup { $pf->remove; rmdir $piddir if $piddir; } eval { main() }; if ($EVAL_ERROR) { print STDERR $EVAL_ERROR; $retcode = 1; } exit $retcode; __END__ =head1 NAME ngcp-fraud-notifier - sends fraud notifications for customers that exceed the threshold =head1 SYNOPSIS B [I...] =head1 DESCRIPTION B checks for contract balances above fraud limit warning thresholds and sends email notifications about the incidents. =head1 OPTIONS =over 8 =item B<--verbose> Show additional debug information. Default false. =item B<--help> Print a brief help message. =back =head1 EXIT STATUS Exit code 0 means everything is ok otherwise 1. =head1 SEE ALSO NGCP::API::Client =head1 BUGS AND LIMITATIONS Please report problems you notice to the Sipwise Development Team . =head1 AUTHOR Kirill Solomko =head1 LICENSE Copyright (C) 2019 Sipwise GmbH, Austria This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . =cut