]> git.proxmox.com Git - pmg-api.git/blob - src/PMG/RuleDB/Remove.pm
e7c353c744877eb8e1604d190783b4f9e21d8d66
[pmg-api.git] / src / PMG / RuleDB / Remove.pm
1 package PMG::RuleDB::Remove;
2
3 use strict;
4 use warnings;
5 use DBI;
6 use Digest::SHA;
7 use MIME::Words;
8 use MIME::Entity;
9 use Encode;
10
11 use PVE::SafeSyslog;
12
13 use PMG::Utils;
14 use PMG::ModGroup;
15 use PMG::RuleDB::Object;
16
17 use base qw(PMG::RuleDB::Object);
18
19 sub otype {
20 return 4007;
21 }
22
23 sub otype_text {
24 return 'Remove attachments';
25 }
26
27 sub oclass {
28 return 'action';
29 }
30
31 sub oisedit {
32 return 1;
33 }
34
35 sub final {
36 return 0;
37 }
38
39 sub priority {
40 return 40;
41 }
42
43 sub new {
44 my ($type, $all, $text, $ogroup, $quarantine) = @_;
45
46 my $class = ref($type) || $type;
47
48 $all = 0 if !defined ($all);
49
50 my $self = $class->SUPER::new($class->otype(), $ogroup);
51
52 $self->{all} = $all;
53 $self->{text} = $text;
54 $self->{quarantine} = $quarantine;
55
56 return $self;
57 }
58
59 sub load_attr {
60 my ($type, $ruledb, $id, $ogroup, $value) = @_;
61
62 my $class = ref($type) || $type;
63
64 defined ($value) || die "undefined value: ERROR";
65
66 my ($obj, $text);
67
68 if ($value =~ m/^([01])\,([01])(\:(.*))?$/s) {
69 $text = PMG::Utils::try_decode_utf8($4);
70 $obj = $class->new($1, $text, $ogroup, $2);
71 } elsif ($value =~ m/^([01])(\:(.*))?$/s) {
72 $text = PMG::Utils::try_decode_utf8($3);
73 $obj = $class->new($1, $text, $ogroup);
74 } else {
75 $obj = $class->new(0, undef, $ogroup);
76 }
77
78 $obj->{id} = $id;
79
80 $obj->{digest} = Digest::SHA::sha1_hex($id, $value, $ogroup);
81
82 return $obj;
83 }
84
85 sub save {
86 my ($self, $ruledb) = @_;
87
88 defined($self->{ogroup}) || die "undefined ogroup: ERROR";
89
90 my $value = $self->{all} ? '1' : '0';
91 $value .= ','. ($self->{quarantine} ? '1' : '0');
92
93 if ($self->{text}) {
94 $value .= encode('UTF-8', ":$self->{text}");
95 }
96
97 if (defined ($self->{id})) {
98 # update
99
100 $ruledb->{dbh}->do(
101 "UPDATE Object SET Value = ? WHERE ID = ?",
102 undef, $value, $self->{id});
103
104 } else {
105 # insert
106
107 my $sth = $ruledb->{dbh}->prepare(
108 "INSERT INTO Object (Objectgroup_ID, ObjectType, Value) " .
109 "VALUES (?, ?, ?);");
110
111 $sth->execute($self->ogroup, $self->otype, $value);
112
113 $self->{id} = PMG::Utils::lastid($ruledb->{dbh}, 'object_id_seq');
114 }
115
116 return $self->{id};
117 }
118
119 sub delete_marked_parts {
120 my ($self, $queue, $entity, $html, $rtype, $marks, $rulename) = @_;
121
122 my $nparts = [];
123
124 my $ctype = $entity->head->mime_type;
125 my $pn = $entity->parts;
126 for (my $i = 0; $i < $pn; $i++) {
127 my $part = $entity->parts($i);
128
129 my ($id, $found);
130
131 if ($id = $part->head->mime_attr('x-proxmox-tmp-aid')) {
132 chomp $id;
133
134 if ($self->{all}) {
135 my $ctype_part = $part->head->mime_type;
136 if ($self->{message_seen}) {
137 $found = 1;
138 } else {
139 if ($ctype =~ m|multipart/alternative|i) {
140 if ($ctype_part !~ m{text/(?:plain|html)}i) {
141 $found = 1 ;
142 }
143
144 if ($i == ($pn-1)) {
145 # we have not seen the message and it is the
146 # end of the first multipart/alternative, mark as message seen
147 $self->{message_seen} = 1;
148 }
149 } else {
150 if ($ctype_part =~ m{text/(?:plain|html)}i) {
151 $self->{message_seen} = 1;
152 } elsif ($ctype_part !~ m|multipart/|i) {
153 $found = 1 ;
154 }
155 }
156 }
157 } else {
158 foreach my $m (@$marks) {
159 $found = 1 if $m eq $id;
160 }
161 }
162
163 }
164
165 if ($found) {
166
167 my $on = PMG::Utils::extract_filename($part->head) || '';
168
169 my $text = PMG::Utils::subst_values($html, { FILENAME => $on } );
170
171 my $fname = "REMOVED_ATTACHMENT_$id." . ($rtype eq "text/html" ? "html" : "txt");
172
173 my $ent = MIME::Entity->build(
174 Type => $rtype,
175 Charset => 'UTF-8',
176 Encoding => "quoted-printable",
177 Filename => $fname,
178 Disposition => "attachment",
179 Data => encode('UTF-8', $text));
180
181 push (@$nparts, $ent);
182
183 syslog ('info', "%s: removed attachment $id ('%s', rule: %s)",
184 $queue->{logid}, $on, $rulename);
185
186 } else {
187 $self->delete_marked_parts($queue, $part, $html, $rtype, $marks, $rulename);
188 push (@$nparts, $part);
189 }
190 }
191
192 $entity->parts ($nparts);
193 }
194
195 sub execute {
196 my ($self, $queue, $ruledb, $mod_group, $targets,
197 $msginfo, $vars, $marks, $ldap) = @_;
198
199 my $rulename = encode('UTF-8', $vars->{RULE} // 'unknown');
200
201 if (!$self->{all} && ($#$marks == -1)) {
202 # no marks
203 return;
204 }
205
206 my $subgroups = $mod_group->subgroups ($targets);
207
208 my $html = PMG::Utils::subst_values($self->{text}, $vars);
209
210 if (!$html) {
211 $html = "This attachment was removed: __FILENAME__\n";
212 $html .= "It was put into the Attachment Quarantine, please contact your Administrator\n" if $self->{quarantine};
213 }
214
215 my $rtype = "text/plain";
216
217 if ($html =~ m/\<\w+\>/s) {
218 $rtype = "text/html";
219 }
220
221 foreach my $ta (@$subgroups) {
222 my ($tg, $entity) = (@$ta[0], @$ta[1]);
223
224 # copy original entity to attachment quarantine if configured
225 if ($self->{quarantine}) {
226 my $original_entity = $entity->dup;
227 PMG::Utils::remove_marks($original_entity);
228 if (my $qid = $queue->quarantine_mail($ruledb, 'A', $original_entity, $tg, $msginfo, $vars, $ldap)) {
229 # adapt the Message-ID header of the mail without attachment to
230 # prevent 2 different mails with the same Message-ID
231 my $message_id = $entity->head->get('Message-ID');
232 if (defined($message_id)) {
233 $message_id =~ s/^(<?)(.+)(>?)$/$1pmg-aquar-$$-$2$3/;
234 $entity->head->replace('Message-ID', $message_id);
235 }
236
237 foreach (@$tg) {
238 syslog (
239 'info',
240 "$queue->{logid}: moved mail for <%s> to attachment quarantine - %s (rule: %s)",
241 encode('UTF-8',$_),
242 $qid,
243 $rulename,
244 );
245 }
246 }
247 }
248
249 # handle singlepart mails
250 my $ctype = $entity->head->mime_type;
251 if (!$entity->is_multipart && (!$self->{all} || $ctype !~ m|text/.*|i)) {
252 $entity->make_multipart();
253 my $first_part = $entity->parts(0);
254 $first_part->head->mime_attr('x-proxmox-tmp-aid' => $entity->head->mime_attr('x-proxmox-tmp-aid'));
255 $entity->head->delete('x-proxmox-tmp-aid');
256 }
257
258 $self->{message_seen} = 0;
259 $self->delete_marked_parts($queue, $entity, $html, $rtype, $marks, $rulename);
260 delete $self->{message_seen};
261
262 if ($msginfo->{testmode}) {
263 $entity->head->mime_attr('Content-type.boundary' => '------=_TEST123456') if $entity->is_multipart;
264 }
265 }
266 }
267
268 sub short_desc {
269 my $self = shift;
270
271 if ($self->{all}) {
272 return "remove all attachments";
273 } else {
274 return "remove matching attachments";
275 }
276 }
277
278 sub properties {
279 my ($class) = @_;
280
281 return {
282 all => {
283 description => "Remove all attachments",
284 type => 'boolean',
285 optional => 1,
286 },
287 quarantine => {
288 description => "Copy original mail to attachment Quarantine.",
289 type => 'boolean',
290 default => 0,
291 optional => 1,
292 },
293 text => {
294 description => "The replacement text.",
295 type => 'string',
296 maxLength => 2048
297 }
298 };
299 }
300
301 sub get {
302 my ($self) = @_;
303
304 return {
305 text => $self->{text},
306 all => $self->{all},
307 quarantine => $self->{quarantine},
308 };
309 }
310
311 sub update {
312 my ($self, $param) = @_;
313
314 $self->{text} = $param->{text};
315 $self->{all} = $param->{all} ? 1 : 0;
316 $self->{quarantine} = $param->{quarantine} ? 1 : 0;
317 }
318
319 1;
320 __END__
321
322 =head1 PMG::RuleDB::Remove
323
324 Remove attachments.