]>
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; |
7066e935 | 17 | |
be10d7c1 | 18 | our $spooldir = "/var/spool/pmg"; |
7066e935 DM |
19 | |
20 | my $fileseq = rand 1000; | |
21 | ||
be10d7c1 DM |
22 | sub 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 |
28 | sub cleanup_active { | |
29 | ||
30 | while (my $file = <$spooldir/active/*>) { | |
31 | unlink $file; | |
32 | } | |
33 | ||
34 | } | |
35 | ||
36 | sub 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 | ||
74 | sub 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 | ||
108 | sub 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 | ||
118 | sub 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 | ||
190 | sub 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 | ||
205 | sub 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 | ||
222 | sub 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 | ||
244 | sub 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 |
315 | sub 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 | ||
325 | sub close { | |
326 | my $self = shift; | |
327 | ||
328 | close ($self->{fh}); | |
329 | ||
330 | rmtree $self->{dumpdir}; | |
331 | ||
332 | unlink $self->{dataname}; | |
333 | } | |
334 | ||
335 | sub _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 | ||
361 | sub 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 | ||
403 | sub 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 | ||
453 | 1; | |
454 | ||
455 | __END__ |