]> git.proxmox.com Git - pmg-api.git/blame - PMG/MailQueue.pm
PMG/DBTools.pm: new helper copy_selected_data()
[pmg-api.git] / PMG / MailQueue.pm
CommitLineData
30e080a7 1package PMG::MailQueue;
7066e935
DM
2
3use strict;
30e080a7 4use warnings;
30e080a7
DM
5
6use PVE::SafeSyslog;
7066e935
DM
7use MIME::Parser;
8use IO::File;
9use File::Sync;
10use File::Basename;
11use File::Path;
12use File::stat;
30e080a7 13use Time::HiRes qw(gettimeofday);
7066e935
DM
14use Mail::Header;
15
30e080a7 16use PMG::LDAPSet;
7066e935 17
be10d7c1 18our $spooldir = "/var/spool/pmg";
7066e935
DM
19
20my $fileseq = rand 1000;
21
be10d7c1
DM
22sub create_sppoldirs {
23 File::Path::make_path(
24 "$spooldir/active", "$spooldir/spam", "$spooldir/virus");
25}
26
7066e935
DM
27# called on service startup to remove any stale files
28sub cleanup_active {
29
30 while (my $file = <$spooldir/active/*>) {
31 unlink $file;
32 }
33
34}
35
36sub new_fileid {
37 my ($dir, $subdir) = @_;
38
39 # try to create a unique data file
40
41 my ($sec, $usec) = gettimeofday ();
42 my $fname = "$sec.$usec.$$." . $fileseq++;
43 my $path = "$dir/$subdir/$fname";
44 my $fh;
45 my $uid;
46 my $subsubdir = '';
47
48 if (!($fh = IO::File->new ($path, 'w+', 0600))) {
9ef3f143 49 die "unable to create file '$path': $! : ERROR";
30e080a7 50 }
7066e935 51
30e080a7 52 if (my $st = stat ($fh)) {
7066e935
DM
53 $uid = sprintf ("%X%X%05X", $st->ino, $sec, $usec);
54 if ($subdir ne 'active') {
55 $subsubdir .= sprintf ("%02X/", $usec % 256);
56 }
57 } else {
58 unlink $path;
9ef3f143 59 die "unable to stat file: $! : ERROR";
7066e935
DM
60 }
61
62 mkdir "$dir/$subdir/$subsubdir";
63
64 my $subpath = "$subdir/$subsubdir$uid";
65
66 if (!rename ($path, "$dir/$subpath")) {
67 unlink $path;
9ef3f143 68 die "unable to rename file: ERROR";
7066e935
DM
69 }
70
30e080a7 71 return ($fh, $uid, $subpath);
7066e935
DM
72}
73
74sub new {
75 my ($type, $from, $to) = @_;
76
77 my $self = {};
78 bless $self, $type;
79
80 $self->{from} = $from;
81 $self->{msgid} = "";
82
83 $self->{sa_score} = undef;
84 $self->{sa_max} = undef;
85 $self->{sa_data} = undef;
86
87 $self->{vinfo} = undef;
88 $self->{bytes} = 0;
89 $self->{rtime} = time;
90 $self->{ptime_spam} = 0;
91 $self->{ptime_virus} = 0;
92
93 my ($fh, $uid, $path) = new_fileid ($spooldir, 'active');
94
95 $self->{fh} = $fh;
96 $self->{uid} = $uid;
97 $self->{logid} = $uid;
98 #$self->{logid} = sprintf ("%05X${uid}", $self->{rtime});
99 $self->{dataname} = "$spooldir/$path";
100
101 $self->{dumpdir} = "/tmp/.proxdump_${$}_$uid";
102
103 $self->set_status ($to, 'undelivered');
104
105 return $self;
106}
107
108sub set_status {
109 my ($self, $targets, $state, $code, $message) = @_;
110
111 foreach my $r (@$targets) {
112 $self->{status}->{$r} = $state;
113 $self->{status_code}->{$r} = $code;
114 $self->{status_message}->{$r} = $message;
115 }
116}
117
118sub quarantinedb_insert {
119 my ($self, $ruledb, $lcid, $ldap, $qtype, $header, $sender, $file, $targets, $vars) = @_;
120
121 eval {
122 my $dbh = $ruledb->{dbh};
123
124 my $insert_cmds = "SELECT nextval ('cmailstore_id_seq'); INSERT INTO CMailStore " .
30e080a7 125 "(CID, RID, ID, Time, QType, Bytes, Spamlevel, Info, Header, Sender, File) VALUES (" .
7066e935
DM
126 "$lcid, currval ('cmailstore_id_seq'), currval ('cmailstore_id_seq'), ";
127
128 my $spaminfo = $vars->{__spaminfo};
129 my $sa_score = $spaminfo->{sa_score} || 0;
130
131 $insert_cmds .= $self->{rtime} . ',';
132 $insert_cmds .= $dbh->quote ($qtype) . ',';
133 $insert_cmds .= $self->{bytes} . ',';
134 $insert_cmds .= $sa_score . ',';
135
136 if ($qtype eq 'V') {
137 $insert_cmds .= $dbh->quote ($self->{vinfo}) . ',';
138 } else {
139
140 my $sscores = $spaminfo->{sa_data};
141 my $sainfo = 'NULL';
142 if (defined ($sscores) && @$sscores != -1) {
143 $sainfo = '';
144 foreach my $s (@$sscores) {
145 $sainfo .= ',' if $sainfo;
146 $sainfo .= sprintf ("%s:%s", $s->{rule}, $s->{score});
147 }
148 $sainfo = $dbh->quote ($sainfo);
149 }
150
151 $insert_cmds .= $sainfo . ',';
152 }
153
154 $insert_cmds .= $dbh->quote ($header) . ',';
155
156 $insert_cmds .= $dbh->quote ($sender) . ',';
157 $insert_cmds .= $dbh->quote ($file) . ');';
158
159 my $tid = int (rand (0x0fffffff));
160
161 my $now = time();
162
163 foreach my $r (@$targets) {
164 my $pmail = get_primary_mail ($ldap, $r);
165 my $receiver;
166 if ($pmail eq lc ($r)) {
167 $receiver = "NULL";
168 } else {
169 $receiver = $dbh->quote ($r);
170 }
171
172
173 $pmail = $dbh->quote ($pmail);
30e080a7 174 $insert_cmds .= "INSERT INTO CMSReceivers " .
7066e935
DM
175 "(CMailStore_CID, CMailStore_RID, PMail, Receiver, TicketID, Status, MTime) " .
176 "VALUES ($lcid, currval ('cmailstore_id_seq'), $pmail, $receiver, $tid, 'N', $now); ";
177
178 # (Mailstore_ID, TicketID) must be unique
179 $tid = ($tid + 1) & 0x0fffffff;
180 }
181
182 $dbh->do ($insert_cmds);
183 };
184
185 my $err = $@;
186
187 syslog ('err', "ERROR: $err") if $err;
188}
189
190sub get_primary_mail {
191 my ($ldap, $mail) = @_;
192
193 $mail = lc ($mail);
194
195 return $mail if !$ldap;
196
197 if (my $info = $ldap->account_info ($mail)) {
198 return $info->{pmail};
30e080a7 199 }
7066e935
DM
200
201 return $mail;
202}
203
204
205sub extract_header_text {
206 my ($entity) = @_;
207
208 my $subject = $entity->head->get ('subject', 0);
209 my $from = $entity->head->get ('from', 0);
210 my $sender = $entity->head->get ('sender', 0);
211
212 my $head = new Mail::Header;
213 $head->add ('subject', $subject) if $subject;
214 $head->add ('from', $from) if $from;
215 $head->add ('sender', $sender) if $sender;
216
217 my $header = $head->as_string();
218
219 return $header;
220}
221
222sub fsync_file_and_dir {
223 my $filename = shift;
224
225 eval {
226 my $fh = IO::File->new($filename) || die "unable to open file '$filename'";
227 File::Sync::fsync ($fh) || die "fsync file '$filename' failed";
228 close ($fh);
229
230 my $dirname = dirname ($filename);
231 my $dir = IO::File->new($dirname) || die "open dir '$dirname' failed";
232 File::Sync::fsync ($dir) || die "fsync dir '$dirname' failed";
233 close ($dir);
234 };
235
236 my $err = $@;
237
238 if ($err) {
239 syslog ('err', "ERROR: $err");
240 }
241
242}
243
244sub quarantine_mail {
245 my ($self, $ruledb, $qtype, $entity, $tg, $msginfo, $vars, $ldap) = @_;
246
247 my $sender = $msginfo->{sender};
248
249 my $header = extract_header_text ($entity);
250
251 my $subpath = $qtype eq 'V' ? 'virus' : 'spam';
252
253 my $lcid = $msginfo->{lcid};
254
255 my ($fh, $uid, $path);
256
257 eval {
258 if ($lcid) {
259 if ($qtype eq 'V') {
260 mkpath "$spooldir/cluster/$lcid/virus";
261 } else {
262 mkpath "$spooldir/cluster/$lcid/spam";
263 }
264 ($fh, $uid, $path) = new_fileid ($spooldir, "cluster/$lcid/$subpath");
265 } else {
266 ($fh, $uid, $path) = new_fileid ($spooldir, $subpath);
267 }
268
269 # there must be only one Return-Path
270 $entity->head->delete ('Return-Path');
271
272 # prepend Delivered-To and Return-Path (like QMAIL MAILDIR FORMAT)
273 $entity->head->add ('Return-Path', join (',', $sender), 0);
274 $entity->head->add ('Delivered-To', join (',', @$tg), 0);
275
276 $entity->print ($fh);
277
278 close ($fh);
279
280 fsync_file_and_dir ("$spooldir/$path"); # make sure the file is on disk
281
282 $self->quarantinedb_insert ($ruledb, $lcid, $ldap, $qtype, $header, $sender, $path, $tg, $vars);
283 };
284
285 my $err = $@;
286
287 if ($err) {
288 close ($fh) if $fh;
289 unlink "$spooldir/$path" if $path;
290 syslog ('err', "ERROR: $err");
291 return undef;
292 }
293
294 return $uid;
295}
296
297#sub quarantine {
298# my ($self, $targets);
299#
300# $self->set_status ($targets, 'quarantine');
301#}
302
303#sub spamreport {
304# my ($self, $targets);
305#
306# $self->set_status ($targets, 'spam:report');
307#}
308
309#sub delay {
310# my ($self, $targets, $hm);
311#
312# $self->set_status ($targets, "delay|$hm");
313#}
314
30e080a7
DM
315sub msgid {
316 my ($self, $msgid) = @_;
7066e935
DM
317
318 if (defined ($msgid)) {
319 $self->{msgid} = $msgid;
320 }
321
30e080a7 322 $self->{msgid};
7066e935
DM
323}
324
325sub close {
326 my $self = shift;
327
328 close ($self->{fh});
329
330 rmtree $self->{dumpdir};
331
332 unlink $self->{dataname};
333}
334
335sub _new_mime_parser {
336 my ($self, $maxfiles) = shift;
337
338 # Create a new MIME parser:
339 my $parser = new MIME::Parser;
340 #$parser->decode_headers(1);
30e080a7 341 $parser->extract_nested_messages (1);
7066e935
DM
342 $parser->ignore_errors (1);
343 $parser->extract_uuencode (0);
344 $parser->decode_bodies (0);
345
346 $parser->max_parts ($maxfiles) if $maxfiles;
347
348 rmtree $self->{dumpdir};
349
350 # Create and set the output directory:
30e080a7 351 (-d $self->{dumpdir} || mkdir ($self->{dumpdir} ,0755)) ||
9ef3f143 352 die "can't create $self->{dumpdir}: $! : ERROR";
7066e935 353 (-w $self->{dumpdir}) ||
9ef3f143 354 die "can't write to directory $self->{dumpdir}: $! : ERROR";
7066e935
DM
355
356 $parser->output_dir($self->{dumpdir});
357
358 return $parser;
359}
360
361sub parse_mail {
362 my ($self, $maxfiles) = shift;
363
364 my $entity;
365 my $ctime = time;
366
367 my $parser = $self->_new_mime_parser ($maxfiles);
30e080a7 368
7066e935
DM
369 $self->{fh}->seek (0, 0);
370
371 eval {
372 if (!($entity = $parser->read($self->{fh}))) {
9ef3f143 373 die "$self->{logid}: unable to parse message: ERROR";
7066e935
DM
374 }
375 };
376
377 die "$self->{logid}: unable to parse message - $@" if $@;
378
379 # bug fix for bin/tests/content/mimeparser.txt
380 if ($entity->mime_type =~ m|multipart/|i && !$entity->head->multipart_boundary) {
381 $entity->head->mime_attr('Content-type' => "application/x-unparseable-multipart");
30e080a7 382 }
7066e935
DM
383
384 if ((my $idcount = $entity->head->count ('Message-Id')) > 0) {
385 $self->msgid ($entity->head->get ('Message-Id', $idcount - 1));
386 }
387
388 # fixme: add parse_time to statistic database
389 my $parse_time = time() - $ctime;
390
391 # also save decoded data
392 decode_entities ($parser, $self->{logid}, $entity);
393
30e080a7 394 # we also remove all proxmox-marks from the mail and add an unique
7066e935
DM
395 # id to each attachment.
396
743f89f2
DM
397 PMG::Utils::remove_marks ($entity, 1);
398 PMG::Utils::add_ct_marks ($entity);
30e080a7 399
7066e935
DM
400 return $entity;
401}
402
403sub decode_entities {
404 my ($parser, $logid, $entity) = @_;
405
406 if ($entity->bodyhandle && (my $path = $entity->bodyhandle->path)) {
407
408 eval {
409 my $head = $entity->head;
410 my $encoding = $head->mime_encoding;
411 my $decoder = new MIME::Decoder $encoding;
412
413 if (!$decoder || ($decoder eq 'none' || $decoder eq 'binary')) {
414
415 $entity->{PMX_decoded_path} = $path; # no need to decode
416
417 } else {
418
419 my $body = $parser->new_body_for ($head);
420 $body->binmode(1);
421 $body->is_encoded(0);
422
423 my $in = $entity->bodyhandle->open ("r") ||
424 die "unable to read raw data '$path'";
425
30e080a7 426 my $decfh = $body->open ("w") ||
7066e935
DM
427 die "unable to open body: $!";
428
429 $decoder->decode ($in, $decfh);
430
431 $in->close;
30e080a7
DM
432
433 $decfh->close ||
7066e935
DM
434 die "can't close bodyhandle: $!";
435
436 $entity->{PMX_decoded_path} = $body->path;
437 }
438 };
439
440 my $err = $@;
441
442 if ($err) {
443 syslog ('err', "$logid: $err");
444 }
30e080a7 445
7066e935
DM
446 }
447
448 foreach my $part ($entity->parts) {
449 decode_entities ($parser, $logid, $part);
450 }
451}
452
4531;
454
455__END__