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