]> git.proxmox.com Git - pmg-api.git/blobdiff - src/PMG/API2/Quarantine.pm
quarantine: user self service: add some response delays
[pmg-api.git] / src / PMG / API2 / Quarantine.pm
index 5cb0f8e6765a0360e27da66fd9395e5d6a6fa19d..991c04d0b8eaa03c09d6ea060c1d516f09ac9aeb 100644 (file)
@@ -2,12 +2,16 @@ package PMG::API2::Quarantine;
 
 use strict;
 use warnings;
+
 use Time::Local;
 use Time::Zone;
 use Data::Dumper;
 use Encode;
 use File::Path;
 use IO::File;
+use MIME::Entity;
+use URI::Escape qw(uri_escape);
+use File::stat ();
 
 use Mail::Header;
 use Mail::SpamAssassin;
@@ -131,9 +135,7 @@ my $parse_header_info = sub {
 
     $res->{subject} = PMG::Utils::decode_rfc1522(PVE::Tools::trim($head->get('subject'))) // '';
 
-    my @fromarray = split('\s*,\s*', $head->get('from') || $ref->{sender});
-
-    $res->{from} = PMG::Utils::decode_rfc1522(PVE::Tools::trim ($fromarray[0])) // '';
+    $res->{from} = PMG::Utils::decode_rfc1522(PVE::Tools::trim($head->get('from') || $ref->{sender})) // '';
 
     my $sender = PMG::Utils::decode_rfc1522(PVE::Tools::trim($head->get('sender')));
     $res->{sender} = $sender if $sender && ($sender ne $res->{from});
@@ -195,6 +197,7 @@ __PACKAGE__->register_method ({
            { name => 'attachment' },
            { name => 'listattachments' },
            { name => 'download' },
+           { name => 'sendlink' },
        ];
 
        return $result;
@@ -275,6 +278,35 @@ __PACKAGE__->register_method ({
        return undef;
     }});
 
+__PACKAGE__->register_method ({
+    name => 'whitelist_delete_base',
+    path => 'whitelist',
+    method => 'DELETE',
+    description => "Delete user whitelist entries.",
+    permissions => { check => [ 'admin', 'qmanager', 'audit', 'quser'] },
+    protected => 1,
+    parameters => {
+       additionalProperties => 0,
+       properties => {
+           pmail => $pmail_param_type,
+           address => get_standard_option('pmg-whiteblacklist-entry-list', {
+               pattern => '',
+               description => "The address, or comma-separated list of addresses, you want to remove.",
+           }),
+       },
+    },
+    returns => { type => 'null' },
+    code => sub {
+       my ($param) = @_;
+
+       my $addresses = [split(',', $param->{address})];
+       $read_or_modify_user_bw_list->('WL', $param, $addresses, 1);
+
+       return undef;
+    }});
+
+# FIXME: remove for PMG 7.0 - addresses can contain stuff like '/' which breaks
+# API path resolution, thus we replaced it by above "un-templated" call
 __PACKAGE__->register_method ({
     name => 'whitelist_delete',
     path => 'whitelist/{address}',
@@ -356,6 +388,35 @@ __PACKAGE__->register_method ({
        return undef;
     }});
 
+__PACKAGE__->register_method ({
+    name => 'blacklist_delete_base',
+    path => 'blacklist',
+    method => 'DELETE',
+    description => "Delete user blacklist entries.",
+    permissions => { check => [ 'admin', 'qmanager', 'audit', 'quser'] },
+    protected => 1,
+    parameters => {
+       additionalProperties => 0,
+       properties => {
+           pmail => $pmail_param_type,
+           address => get_standard_option('pmg-whiteblacklist-entry-list', {
+               pattern => '',
+               description => "The address, or comma-separated list of addresses, you want to remove.",
+           }),
+       },
+    },
+    returns => { type => 'null' },
+    code => sub {
+       my ($param) = @_;
+
+       my $addresses = [split(',', $param->{address})];
+       $read_or_modify_user_bw_list->('BL', $param, $addresses, 1);
+
+       return undef;
+    }});
+
+# FIXME: remove for PMG 7.0 - addresses can contain stuff like '/' which breaks
+# API path resolution, thus we replaced it by above "un-templated" call
 __PACKAGE__->register_method ({
     name => 'blacklist_delete',
     path => 'blacklist/{address}',
@@ -1181,4 +1242,129 @@ __PACKAGE__->register_method ({
        return undef;
     }});
 
+my $link_map_fn = "/run/pmgproxy/quarantinelink.map";
+my $per_user_limit = 60*60; # 1 hour
+
+my sub send_link_mail {
+    my ($cfg, $receiver) = @_;
+
+    my $hostname = PVE::INotify::nodename();
+    my $fqdn = $cfg->get('spamquar', 'hostname') //
+    PVE::Tools::get_fqdn($hostname);
+
+    my $port = $cfg->get('spamquar', 'port') // 8006;
+
+    my $protocol = $cfg->get('spamquar', 'protocol') // 'https';
+
+    my $protocol_fqdn_port = "$protocol://$fqdn";
+    if (($protocol eq 'https' && $port != 443) ||
+       ($protocol eq 'http' && $port != 80)) {
+       $protocol_fqdn_port .= ":$port";
+    }
+
+    my $mailfrom = $cfg->get ('spamquar', 'mailfrom') //
+    "Proxmox Mail Gateway <postmaster>";
+
+    my $ticket = PMG::Ticket::assemble_quarantine_ticket($receiver);
+    my $esc_ticket = uri_escape($ticket);
+    my $link = "$protocol_fqdn_port/quarantine?ticket=${esc_ticket}";
+
+    my $text = "Here is your Link for the Spam Quarantine on $fqdn:\n\n$link\n";
+
+    my $mail = MIME::Entity->build(
+       Type    => "text/plain",
+       To      => $receiver,
+       From    => $mailfrom,
+       Subject => "Proxmox Mail Gateway - Quarantine Link",
+       Data    => $text,
+    );
+
+    # we use an empty envelope sender (we dont want to receive NDRs)
+    PMG::Utils::reinject_mail ($mail, '', [$receiver], undef, $fqdn);
+}
+
+__PACKAGE__->register_method ({
+    name =>'sendlink',
+    path => 'sendlink',
+    method => 'POST',
+    description => "Send Quarantine link to given e-mail.",
+    permissions => { user => 'world' },
+    protected => 1,
+    parameters => {
+       additionalProperties => 0,
+       properties => {
+           mail => get_standard_option('pmg-email-address'),
+       },
+    },
+    returns => { type => "null" },
+    code => sub {
+       my ($param) = @_;
+
+       my $starttime = time();
+
+       my $cfg = PMG::Config->new();
+       my $is_enabled = $cfg->get('spamquar', 'quarantinelink');
+       if (!$is_enabled) {
+           die "This feature is not enabled\n";
+       }
+
+       my $stat = File::stat::stat($link_map_fn);
+
+       if (defined($stat) && ($stat->mtime + 5) > $starttime) {
+           sleep(3);
+           die "Too many requests. Please try again later\n";
+       }
+       warn "mtime: ". $stat->mtime ."\n";
+
+       my $domains = PVE::INotify::read_file('domains');
+       my $domainregex = PMG::Utils::domain_regex([keys %$domains]);
+
+       my $receiver = $param->{mail};
+
+       if ($receiver !~ $domainregex) {
+           sleep(3);
+           return undef; # silently ignore invalid mails
+       }
+
+       PVE::Tools::lock_file_full("${link_map_fn}.lck", 10, 1, sub {
+           return if !-f $link_map_fn;
+           # check if user is allowed to request mail
+           my $data = PVE::Tools::file_get_contents($link_map_fn);
+           for my $line (split("\n", $data)) {
+               next if $line !~ m/^\Q$receiver\E (\d+)$/;
+               if (($1 + $per_user_limit) > $starttime) {
+                   sleep(3);
+                   die "Too many requests for '$receiver', only one request per"
+                       ."hour is permitted. Please try again later\n";
+               } else {
+                   last;
+               }
+           }
+       });
+       die $@ if $@;
+
+       # we are allowed to send mail, lock and update file and send
+       PVE::Tools::lock_file("${link_map_fn}.lck", 10, sub {
+           my $newdata = "$receiver $starttime\n";
+
+           if (-f $link_map_fn) {
+               my $data = PVE::Tools::file_get_contents($link_map_fn);
+               for my $line (split("\n", $data)) {
+                   if ($line =~ m/^(?:.*) (\d+)$/) {
+                       if (($1 + $per_user_limit) > $starttime) {
+                           $newdata .= $line . "\n";
+                       }
+                   }
+               }
+           }
+           PVE::Tools::file_set_contents($link_map_fn, $newdata);
+       });
+       die $@ if $@;
+
+       send_link_mail($cfg, $receiver);
+       sleep(1); # always delay for a bit
+
+       return undef;
+    }});
+
 1;