]> git.proxmox.com Git - pmg-api.git/blame - PMG/Utils.pm
install pmg-smtp-filter.service
[pmg-api.git] / PMG / Utils.pm
CommitLineData
758c7b6b
DM
1package PMG::Utils;
2
3use strict;
4use warnings;
5use Carp;
6use DBI;
b8ea5d5d 7use Net::Cmd;
758c7b6b 8use Net::SMTP;
26357b0a 9use IO::File;
cad3d400 10use File::stat;
ff1c5a81 11use File::Basename;
758c7b6b
DM
12use MIME::Words;
13use MIME::Parser;
8210c7fa 14use Time::HiRes qw (gettimeofday);
26357b0a 15use Xdgmime;
d0d91cda 16use Data::Dumper;
f609bf7f 17use Net::IP;
758c7b6b 18
f609bf7f 19use PVE::Network;
5953119e 20use PVE::Tools;
758c7b6b 21use PVE::SafeSyslog;
f609bf7f 22use PVE::ProcFSTools;
d0d91cda 23use PMG::AtomicFile;
8210c7fa 24use PMG::MailQueue;
758c7b6b 25
c881fe35
DM
26sub msgquote {
27 my $msg = shift || '';
28 $msg =~ s/%/%%/g;
29 return $msg;
30}
31
758c7b6b
DM
32sub lastid {
33 my ($dbh, $seq) = @_;
34
35 return $dbh->last_insert_id(
36 undef, undef, undef, undef, { sequence => $seq});
37}
38
cad3d400
DM
39sub file_older_than {
40 my ($filename, $lasttime) = @_;
41
42 my $st = stat($filename);
43
44 return 0 if !defined($st);
45
46 return ($lasttime >= $st->ctime);
47}
48
758c7b6b
DM
49sub extract_filename {
50 my ($head) = @_;
51
52 if (my $value = $head->recommended_filename()) {
8210c7fa 53 chomp $value;
758c7b6b
DM
54 if (my $decvalue = MIME::Words::decode_mimewords($value)) {
55 $decvalue =~ s/\0/ /g;
5953119e 56 $decvalue = PVE::Tools::trim($decvalue);
758c7b6b
DM
57 return $decvalue;
58 }
59 }
60
61 return undef;
62}
63
64sub remove_marks {
65 my ($entity, $add_id, $id) = @_;
66
67 $id //= 1;
68
69 foreach my $tag (grep {/^x-proxmox-tmp/i} $entity->head->tags) {
70 $entity->head->delete ($tag);
71 }
72
73 $entity->head->replace('X-Proxmox-tmp-AID', $id) if $add_id;
74
75 foreach my $part ($entity->parts) {
76 $id = remove_marks($part, $add_id, $id + 1);
77 }
78
79 return $id;
80}
81
82sub subst_values {
83 my ($body, $dh) = @_;
84
85 return if !$body;
86
87 foreach my $k (keys %$dh) {
88 my $v = $dh->{$k};
bd98f5a1
DM
89 if (defined($v)) {
90 $body =~ s/__\Q${k}\E__/$v/gs;
758c7b6b
DM
91 }
92 }
93
94 return $body;
95}
96
97sub reinject_mail {
98 my ($entity, $sender, $targets, $xforward, $me, $nodsn) = @_;
99
100 my $smtp;
101 my $resid;
102 my $rescode;
103 my $resmess;
104
105 eval {
106 my $smtp = Net::SMTP->new('127.0.0.1', Port => 10025, Hello => $me) ||
107 die "unable to connect to localhost at port 10025";
108
109 if (defined($xforward)) {
110 my $xfwd;
8210c7fa 111
758c7b6b
DM
112 foreach my $attr (keys %{$xforward}) {
113 $xfwd .= " $attr=$xforward->{$attr}";
114 }
115
116 if ($xfwd && $smtp->command("XFORWARD", $xfwd)->response() != CMD_OK) {
117 syslog('err', "xforward error - got: %s %s", $smtp->code, scalar($smtp->message));
118 }
119 }
120
121 if (!$smtp->mail($sender)) {
122 syslog('err', "smtp error - got: %s %s", $smtp->code, scalar ($smtp->message));
123 die "smtp from: ERROR";
124 }
125
126 my $dsnopts = $nodsn ? {Notify => ['NEVER']} : {};
127
128 if (!$smtp->to (@$targets, $dsnopts)) {
129 syslog ('err', "smtp error - got: %s %s", $smtp->code, scalar($smtp->message));
130 die "smtp to: ERROR";
131 }
132
133 # Output the head:
134 #$entity->sync_headers ();
135 $smtp->data();
136
137 my $out = PMG::SMTPPrinter->new($smtp);
138 $entity->print($out);
8210c7fa
DM
139
140 # make sure we always have a newline at the end of the mail
758c7b6b
DM
141 # else dataend() fails
142 $smtp->datasend("\n");
143
144 if ($smtp->dataend()) {
145 my @msgs = $smtp->message;
8210c7fa
DM
146 $resmess = $msgs[$#msgs];
147 ($resid) = $resmess =~ m/Ok: queued as ([0-9A-Z]+)/;
758c7b6b
DM
148 $rescode = $smtp->code;
149 if (!$resid) {
150 die sprintf("unexpected SMTP result - got: %s %s : WARNING", $smtp->code, $resmess);
8210c7fa 151 }
758c7b6b
DM
152 } else {
153 my @msgs = $smtp->message;
8210c7fa 154 $resmess = $msgs[$#msgs];
758c7b6b
DM
155 $rescode = $smtp->code;
156 die sprintf("sending data failed - got: %s %s : ERROR", $smtp->code, $resmess);
157 }
158 };
159 my $err = $@;
8210c7fa 160
758c7b6b 161 $smtp->quit if $smtp;
8210c7fa 162
758c7b6b
DM
163 if ($err) {
164 syslog ('err', $err);
165 }
166
167 return wantarray ? ($resid, $rescode, $resmess) : $resid;
168}
169
8210c7fa
DM
170sub analyze_virus_clam {
171 my ($queue, $dname, $pmg_cfg) = @_;
172
173 my $timeout = 60*5;
174 my $vinfo;
175
176 my $clamdscan_opts = "--stdout";
177
178 my ($csec, $usec) = gettimeofday();
179
180 my $previous_alarm;
181
182 eval {
183
184 $previous_alarm = alarm($timeout);
185
186 $SIG{ALRM} = sub {
187 die "$queue->{logid}: Maximum time ($timeout sec) exceeded. " .
188 "virus analyze (clamav) failed: ERROR";
189 };
190
191 open(CMD, "/usr/bin/clamdscan $clamdscan_opts '$dname'|") ||
192 die "$queue->{logid}: can't exec clamdscan: $! : ERROR";
193
194 my $ifiles;
8210c7fa 195
54eaf7df
DM
196 my $response = '';
197 while (defined(my $line = <CMD>)) {
198 if ($line =~ m/^$dname.*:\s+([^ :]*)\s+FOUND$/) {
8210c7fa
DM
199 # we just use the first detected virus name
200 $vinfo = $1 if !$vinfo;
54eaf7df 201 } elsif ($line =~ m/^Infected files:\s(\d*)$/i) {
8210c7fa
DM
202 $ifiles = $1;
203 }
204
54eaf7df 205 $response .= $line;
8210c7fa
DM
206 }
207
208 close(CMD);
209
210 alarm(0); # avoid race conditions
211
212 if (!defined($ifiles)) {
213 die "$queue->{logid}: got undefined output from " .
54eaf7df 214 "virus detector: $response : ERROR";
8210c7fa
DM
215 }
216
217 if ($vinfo) {
54eaf7df 218 syslog('info', "$queue->{logid}: virus detected: $vinfo (clamav)");
8210c7fa
DM
219 }
220 };
221 my $err = $@;
222
223 alarm($previous_alarm);
224
225 my ($csec_end, $usec_end) = gettimeofday();
226 $queue->{ptime_clam} =
227 int (($csec_end-$csec)*1000 + ($usec_end - $usec)/1000);
228
229 if ($err) {
230 syslog ('err', $err);
231 $vinfo = undef;
232 $queue->{errors} = 1;
233 }
234
235 $queue->{vinfo_clam} = $vinfo;
236
237 return $vinfo ? "$vinfo (clamav)" : undef;
238}
239
240sub analyze_virus {
241 my ($queue, $filename, $pmg_cfg, $testmode) = @_;
242
243 # TODO: support other virus scanners?
244
245 # always scan with clamav
246 return analyze_virus_clam($queue, $filename, $pmg_cfg);
247}
248
26357b0a
DM
249sub magic_mime_type_for_file {
250 my ($filename) = @_;
251
252 # we do not use get_mime_type_for_file, because that considers
253 # filename extensions - we only want magic type detection
254
255 my $bufsize = Xdgmime::xdg_mime_get_max_buffer_extents();
256 die "got strange value for max_buffer_extents" if $bufsize > 4096*10;
257
258 my $ct = "application/octet-stream";
259
260 my $fh = IO::File->new("<$filename") ||
261 die "unable to open file '$filename' - $!";
262
263 my ($buf, $len);
264 if (($len = $fh->read($buf, $bufsize)) > 0) {
265 $ct = xdg_mime_get_mime_type_for_data($buf, $len);
266 }
267 $fh->close();
268
269 die "unable to read file '$filename' - $!" if ($len < 0);
270
271 return $ct;
272}
758c7b6b 273
1d4193a1
DM
274sub add_ct_marks {
275 my ($entity) = @_;
276
277 if (my $path = $entity->{PMX_decoded_path}) {
278
279 # set a reasonable default if magic does not give a result
280 $entity->{PMX_magic_ct} = $entity->head->mime_attr('content-type');
281
282 if (my $ct = magic_mime_type_for_file($path)) {
283 if ($ct ne 'application/octet-stream' || !$entity->{PMX_magic_ct}) {
284 $entity->{PMX_magic_ct} = $ct;
285 }
286 }
287
288 my $filename = $entity->head->recommended_filename;
289 $filename = basename($path) if !defined($filename) || $filename eq '';
290
291 if (my $ct = xdg_mime_get_mime_type_from_file_name($filename)) {
292 $entity->{PMX_glob_ct} = $ct;
293 }
294 }
295
296 foreach my $part ($entity->parts) {
297 add_ct_marks ($part);
298 }
299}
300
f609bf7f
DM
301# x509 certificate utils
302
303my $proxmox_tls_cert_fn = "/etc/proxmox/proxmox-tls.pem";
304
305sub gen_proxmox_tls_cert {
306 my ($force, $company, $cn) = @_;
307
308 return if !$force && -f $proxmox_tls_cert_fn;
309
310 my $sslconf = <<__EOD__;
311RANDFILE = /root/.rnd
312extensions = v3_req
313
314[ req ]
315default_bits = 4096
316distinguished_name = req_distinguished_name
317req_extensions = v3_req
318prompt = no
319string_mask = nombstr
320
321[ req_distinguished_name ]
322organizationalUnitName = Proxmox Mail Gateway
323organizationName = $company
324commonName = $cn
325
326[ v3_req ]
327basicConstraints = CA:FALSE
328nsCertType = server
329keyUsage = nonRepudiation, digitalSignature, keyEncipherment
330__EOD__
331
332 my $cfgfn = "/tmp/proxmoxtlsconf-$$.tmp";
333 my $fh = IO::File->new ($cfgfn, "w");
334 print $fh $sslconf;
335 close ($fh);
336
337 eval {
338 PVE::Tools::run_command(['openssl', 'req', '-batch', '-x509', '-new', '-sha256',
339 '-config', $cfgfn, '-days', 3650, '-nodes',
340 '-out', $proxmox_tls_cert_fn,
341 '-keyout', $proxmox_tls_cert_fn]);
342 };
343
344 if (my $err = $@) {
345 unlink $proxmox_tls_cert_fn;
346 unlink $cfgfn;
347 die "unable to generate proxmox certificate request:\n$err";
348 }
349
350 unlink $cfgfn;
351}
352
353sub find_local_network_for_ip {
354 my ($ip) = @_;
355
356 my $testip = Net::IP->new($ip);
357
358 my $isv6 = $testip->version == 6;
359 my $routes = $isv6 ?
360 PVE::ProcFSTools::read_proc_net_ipv6_route() :
361 PVE::ProcFSTools::read_proc_net_route();
362
363 foreach my $entry (@$routes) {
364 my $mask;
365 if ($isv6) {
366 $mask = $entry->{prefix};
367 next if !$mask; # skip the default route...
368 } else {
369 $mask = $PVE::Network::ipv4_mask_hash_localnet->{$entry->{mask}};
370 next if !defined($mask);
371 }
372 my $cidr = "$entry->{dest}/$mask";
373 my $testnet = Net::IP->new($cidr);
374 my $overlap = $testnet->overlaps($testip);
375 if ($overlap == $Net::IP::IP_B_IN_A_OVERLAP ||
376 $overlap == $Net::IP::IP_IDENTICAL)
377 {
378 return $cidr;
379 }
380 }
381
382 die "unable to detect local network for ip '$ip'\n";
383}
d0d91cda 384
758c7b6b 3851;