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