163
|
1 /*
|
|
2
|
|
3 Copyright (c) 2007 Carl Byington - 510 Software Group, released under
|
|
4 the GPL version 3 or any later version at your choice available at
|
|
5 http://www.gnu.org/licenses/gpl-3.0.txt
|
|
6
|
|
7 Based on spamass-milter by Georg C. F. Greve <greve@gnu.org>
|
|
8
|
|
9 */
|
|
10
|
|
11 #include "config.h"
|
|
12 #include "dnsbl.h"
|
|
13 #include <errno.h>
|
|
14 #include <fcntl.h>
|
|
15 #include <poll.h>
|
|
16 #include <signal.h>
|
|
17 #include <string>
|
|
18 #include <sys/types.h>
|
|
19 #include <sys/wait.h>
|
|
20 #include <unistd.h>
|
|
21
|
|
22
|
|
23 static const char Id[] = "$Id$";
|
|
24
|
|
25 bool warnedmacro = false; /* have we logged that we couldn't fetch a macro? */
|
|
26 const int maxlen = 1000; // used for snprintf buffers
|
|
27
|
|
28
|
|
29 SpamAssassin::SpamAssassin()
|
|
30 {
|
|
31 error = false;
|
|
32 running = false;
|
|
33 first_recipient = true;
|
|
34 }
|
|
35
|
|
36
|
|
37 SpamAssassin::~SpamAssassin()
|
|
38 {
|
|
39 // close all pipes that are still open
|
|
40 if (pipe_io[0][0] > -1) close(pipe_io[0][0]);
|
|
41 if (pipe_io[0][1] > -1) close(pipe_io[0][1]);
|
|
42 if (pipe_io[1][0] > -1) close(pipe_io[1][0]);
|
|
43 if (pipe_io[1][1] > -1) close(pipe_io[1][1]);
|
|
44 // child still running?
|
|
45 if (running) {
|
|
46 // make sure the pid is valid
|
|
47 if (pid > 0) {
|
|
48 // slaughter child
|
|
49 kill(pid, SIGKILL);
|
|
50 // wait for child to terminate
|
|
51 int status;
|
|
52 waitpid(pid, &status, 0);
|
|
53 }
|
|
54 }
|
|
55 }
|
|
56
|
|
57
|
|
58 void SpamAssassin::mlfi_connect(mlfiPriv *priv_, int ip)
|
|
59 {
|
|
60 priv = priv_;
|
|
61 ip4 = ip;
|
|
62 }
|
|
63
|
|
64
|
|
65 void SpamAssassin::mlfi_helo(char *helohost)
|
|
66 {
|
|
67 helo = helohost;
|
|
68 }
|
|
69
|
|
70
|
|
71 void SpamAssassin::mlfi_envfrom(char *from, char *qid)
|
|
72 {
|
|
73 envfrom = from;
|
|
74 queueid = qid;
|
|
75 }
|
|
76
|
|
77
|
|
78 void SpamAssassin::mlfi_envrcpt(SMFICTX *ctx, char *envrcpt)
|
|
79 {
|
|
80 if (first_recipient) {
|
|
81 /* Send the envelope headers as X-Envelope-From: and
|
|
82 X-Envelope-To: so that SpamAssassin can use them in its
|
|
83 whitelist checks. Also forge as complete a dummy
|
|
84 Received: header as possible because SA gets a lot of
|
|
85 info from it.
|
|
86
|
|
87 HReceived: $?sfrom $s $.$?_($?s$|from $.$_)
|
|
88 $.$?{auth_type}(authenticated$?{auth_ssf} bits=${auth_ssf}$.)
|
|
89 $.by $j ($v/$Z)$?r with $r$. id $i$?{tls_version}
|
|
90 (version=${tls_version} cipher=${cipher} bits=${cipher_bits} verify=${verify})$.$?u
|
|
91 for $u; $|;
|
|
92 $.$b$?g
|
|
93 (envelope-from $g)$.
|
|
94
|
|
95 */
|
|
96 const char *macro_b, *macro_i, *macro_j, *macro_r,
|
|
97 *macro_s, *macro_v, *macro_Z, *macro__;
|
|
98 char date[32];
|
|
99 time_t tval;
|
|
100 time(&tval);
|
|
101 strftime(date, sizeof(date), "%a, %d %b %Y %H:%M:%S %z", localtime(&tval));
|
|
102 macro_b = date;
|
|
103
|
|
104 /* queue ID */
|
|
105 macro_i = queueid;
|
|
106
|
|
107 /* FQDN of this site */
|
|
108 macro_j = smfi_getsymval(ctx, "j");
|
|
109 if (!macro_j) {
|
|
110 macro_j = "localhost";
|
|
111 warnmacro("j", "ENVRCPT");
|
|
112 }
|
|
113
|
|
114 /* Protocol used to receive the message */
|
|
115 macro_r = smfi_getsymval(ctx, "r");
|
|
116 if (!macro_r) {
|
|
117 macro_r = "SMTP";
|
|
118 warnmacro("r", "ENVRCPT");
|
|
119 }
|
|
120
|
|
121 macro_s = helo;
|
|
122
|
|
123 /* Sendmail binary version */
|
|
124 macro_v = smfi_getsymval(ctx, "v");
|
|
125 if (!macro_v) {
|
|
126 macro_v = "8.13.0";
|
|
127 warnmacro("v", "ENVRCPT");
|
|
128 }
|
|
129
|
|
130 /* Sendmail .cf version */
|
|
131 macro_Z = smfi_getsymval(ctx, "Z");
|
|
132 if (!macro_Z) {
|
|
133 macro_Z = "8.13.0";
|
|
134 warnmacro("Z", "ENVRCPT");
|
|
135 }
|
|
136
|
|
137 /* Validated sending site's address */
|
|
138 macro__ = smfi_getsymval(ctx, "_");
|
|
139 if (!macro__) {
|
|
140 macro__ = "unknown";
|
|
141 warnmacro("_", "ENVRCPT");
|
|
142 }
|
|
143
|
|
144 output(string("Received: from ") + macro_s + " (" + macro__+ ")\r\n\t" +
|
|
145 "by " + macro_j + " (" + macro_v + "/" + macro_Z + ") with " + macro_r + " id " + macro_i + "\r\n\t" +
|
|
146 "for " + envfrom + ";\r\n\t" +
|
|
147 macro_b + "\r\n");
|
|
148
|
|
149 output(string("X-Envelope-From: ") + envfrom + "\r\n");
|
|
150 }
|
|
151 output(string("X-Envelope-To: ") + envrcpt + "\r\n");
|
|
152 first_recipient = false;
|
|
153 }
|
|
154
|
|
155
|
|
156 void SpamAssassin::mlfi_header(char* headerf, char* headerv)
|
|
157 {
|
|
158 if (!running) Connect();
|
|
159 output(spamc_input);
|
|
160 output(headerf);
|
|
161 output(": ");
|
|
162 output(headerv);
|
|
163 output("\r\n");
|
|
164 spamc_input.empty();
|
|
165 }
|
|
166
|
|
167
|
|
168 void SpamAssassin::mlfi_eoh()
|
|
169 {
|
|
170 output("\r\n");
|
|
171 }
|
|
172
|
|
173
|
|
174 void SpamAssassin::mlfi_body(u_char *bodyp, size_t bodylen)
|
|
175 {
|
|
176 output((char *)bodyp, bodylen);
|
|
177 }
|
|
178
|
|
179
|
|
180 int SpamAssassin::mlfi_eom()
|
|
181 {
|
|
182 close_output(); // signal EOF to SpamAssassin
|
|
183 input(); // read what the Assassin is telling us
|
|
184 my_syslog(priv, "spamc returned " + spamc_output);
|
|
185 return atoi(spamc_output.c_str());
|
|
186 }
|
|
187
|
|
188
|
|
189 void SpamAssassin::Connect()
|
|
190 {
|
|
191 if (error) return;
|
|
192 // set up pipes for in- and output
|
|
193 error |= (pipe(pipe_io[0]));
|
|
194 error |= (pipe(pipe_io[1]));
|
|
195 if (error) return;
|
|
196
|
|
197 // now execute SpamAssassin client for contact with SpamAssassin spamd
|
|
198 // start child process
|
|
199 pid = fork();
|
|
200 switch (pid) {
|
|
201 case -1:
|
|
202 // forking trouble.
|
|
203 my_syslog(priv, "unable to fork for spamc");
|
|
204 error = true;
|
|
205 close(pipe_io[0][0]);
|
|
206 close(pipe_io[0][1]);
|
|
207 close(pipe_io[1][0]);
|
|
208 close(pipe_io[1][1]);
|
|
209 pipe_io[0][0] = -1;
|
|
210 pipe_io[0][1] = -1;
|
|
211 pipe_io[1][0] = -1;
|
|
212 pipe_io[1][1] = -1;
|
|
213 return;
|
|
214 case 0:
|
|
215 // +++ CHILD +++
|
|
216
|
|
217 // close unused pipes
|
|
218 close(pipe_io[1][0]);
|
|
219 close(pipe_io[0][1]);
|
|
220
|
|
221 // redirect stdin(0), stdout(1) and stderr(2)
|
|
222 dup2(pipe_io[0][0],0);
|
|
223 dup2(pipe_io[1][1],1);
|
|
224 dup2(pipe_io[1][1],2);
|
|
225
|
|
226 closeall(3);
|
|
227
|
|
228 // execute spamc
|
|
229 char* argv[3];
|
|
230 argv[0] = "@SPAMC@";
|
|
231 argv[0] = "/usr/bin/testspamc";
|
|
232 argv[1] = "-c";
|
|
233 argv[2] = NULL;
|
|
234 execvp(argv[0] , argv); // does not return!
|
|
235 _exit(1); // exec failed
|
|
236 break;
|
|
237 }
|
|
238
|
|
239 // +++ PARENT +++
|
|
240
|
|
241 // close unused pipes
|
|
242 close(pipe_io[0][0]);
|
|
243 close(pipe_io[1][1]);
|
|
244 pipe_io[0][0] = -1;
|
|
245 pipe_io[1][1] = -1;
|
|
246
|
|
247 // mark the pipes non-blocking
|
|
248 if (fcntl(pipe_io[0][1], F_SETFL, O_NONBLOCK) == -1)
|
|
249 error = true;
|
|
250 #if 0 /* don't really need to make the sink pipe nonblocking */
|
|
251 if (fcntl(pipe_io[1][0], F_SETFL, O_NONBLOCK) == -1)
|
|
252 error = true;
|
|
253 #endif
|
|
254
|
|
255 // we have to assume the client is running now.
|
|
256 running = true;
|
|
257 }
|
|
258
|
|
259
|
|
260 void SpamAssassin::output(const char* buffer, size_t size)
|
|
261 {
|
|
262 // if there are problems, fail.
|
|
263 if (error) return;
|
|
264
|
|
265 if (!running) {
|
|
266 // buffer it
|
|
267 spamc_input.append(buffer, size);
|
|
268 return;
|
|
269 }
|
|
270
|
|
271 // send to SpamAssassin
|
|
272 long total = 0;
|
|
273 long wsize = 0;
|
|
274 string reason;
|
|
275 int status;
|
|
276 do {
|
|
277 struct pollfd fds[2];
|
|
278 int nfds = 2, nready;
|
|
279 fds[0].fd = pipe_io[0][1];
|
|
280 fds[0].events = POLLOUT;
|
|
281 fds[0].revents = 0;
|
|
282 fds[1].fd = pipe_io[1][0];
|
|
283 fds[1].events = POLLIN;
|
|
284 fds[1].revents = 0;
|
|
285
|
|
286 nready = poll(fds, nfds, 1000);
|
|
287 if (nready == -1) {
|
|
288 my_syslog(priv, "poll failed");
|
|
289 error = true;
|
|
290 return;
|
|
291 }
|
|
292
|
|
293 if (fds[1].revents & (POLLERR|POLLNVAL|POLLHUP)) {
|
|
294 my_syslog(priv, "poll says my read pipe is busted");
|
|
295 error = true;
|
|
296 return;
|
|
297 }
|
|
298
|
|
299 if (fds[0].revents & (POLLERR|POLLNVAL|POLLHUP)) {
|
|
300 my_syslog(priv, "poll says my write pipe is busted");
|
|
301 error = true;
|
|
302 return;
|
|
303 }
|
|
304
|
|
305 if (fds[1].revents & POLLIN) {
|
|
306 read_pipe();
|
|
307 }
|
|
308
|
|
309 if (fds[0].revents & POLLOUT) {
|
|
310 switch(wsize = write(pipe_io[0][1], (char *)buffer + total, size - total)) {
|
|
311 case -1:
|
|
312 if (errno == EAGAIN) continue;
|
|
313 reason = string(strerror(errno));
|
|
314 // close the pipes
|
|
315 close(pipe_io[0][1]);
|
|
316 close(pipe_io[1][0]);
|
|
317 pipe_io[0][1] = -1;
|
|
318 pipe_io[1][0] = -1;
|
|
319 // Slaughter child
|
|
320 kill(pid, SIGKILL);
|
|
321 // wait for child to terminate
|
|
322 waitpid(pid, &status, 0);
|
|
323 my_syslog(priv, "write error: " + reason);
|
|
324 error = true;
|
|
325 running = false;
|
|
326 return;
|
|
327 default:
|
|
328 total += wsize;
|
|
329 break;
|
|
330 }
|
|
331 }
|
|
332 } while ( total < size );
|
|
333 }
|
|
334
|
|
335
|
|
336 void SpamAssassin::output(const char* buffer)
|
|
337 {
|
|
338 output(buffer, strlen(buffer));
|
|
339 }
|
|
340
|
|
341
|
|
342 void SpamAssassin::output(string buffer)
|
|
343 {
|
|
344 output(buffer.c_str(), buffer.size());
|
|
345 }
|
|
346
|
|
347
|
|
348 void SpamAssassin::close_output()
|
|
349 {
|
|
350 if (close(pipe_io[0][1]))
|
|
351 my_syslog(priv, "close error: " + string(strerror(errno)));
|
|
352 pipe_io[0][1] = -1;
|
|
353 }
|
|
354
|
|
355
|
|
356 void SpamAssassin::input()
|
|
357 {
|
|
358 if (!running || error) return;
|
|
359 empty_and_close_pipe();
|
|
360 if (running) {
|
|
361 // wait until child is dead
|
|
362 int status;
|
|
363 if (waitpid(pid, &status, 0) < 0) {
|
|
364 error = true;
|
|
365 };
|
|
366 }
|
|
367 running = false;
|
|
368 }
|
|
369
|
|
370
|
|
371 int SpamAssassin::read_pipe()
|
|
372 {
|
|
373 long size;
|
|
374 int status;
|
|
375 char iobuff[1024];
|
|
376 string reason;
|
|
377
|
|
378 if (pipe_io[1][0] == -1) return 0;
|
|
379
|
|
380 size = read(pipe_io[1][0], iobuff, 1024);
|
|
381
|
|
382 if (size < 0) {
|
|
383 reason = string(strerror(errno));
|
|
384 // Close remaining pipe.
|
|
385 close(pipe_io[1][0]);
|
|
386 pipe_io[1][0] = -1;
|
|
387 // Slaughter child
|
|
388 kill(pid, SIGKILL);
|
|
389 // wait for child to terminate
|
|
390 waitpid(pid, &status, 0);
|
|
391 my_syslog(priv, "read error: " + reason);
|
|
392 size = 0;
|
|
393 error = true;
|
|
394 running = false;
|
|
395 } else if (size == 0) {
|
|
396 // EOF. Close the pipe
|
|
397 if (close(pipe_io[1][0])) {
|
|
398 error = true;
|
|
399 my_syslog(priv, "close error: " + string(strerror(errno)));
|
|
400 }
|
|
401 pipe_io[1][0] = -1;
|
|
402 } else {
|
|
403 // append to mail buffer
|
|
404 spamc_output.append(iobuff, size);
|
|
405 }
|
|
406 return size;
|
|
407 }
|
|
408
|
|
409
|
|
410 void SpamAssassin::empty_and_close_pipe()
|
|
411 {
|
|
412 while (read_pipe())
|
|
413 ;
|
|
414 }
|
|
415
|
|
416
|
|
417 void SpamAssassin::closeall(int fd)
|
|
418 {
|
|
419 int fdlimit = sysconf(_SC_OPEN_MAX);
|
|
420 while (fd < fdlimit)
|
|
421 close(fd++);
|
|
422 }
|
|
423
|
|
424
|
|
425 void SpamAssassin::warnmacro(char *macro, char *scope)
|
|
426 {
|
|
427 if (warnedmacro) return;
|
|
428 char buf[maxlen];
|
|
429 snprintf(buf, sizeof(buf), "Could not retrieve sendmail macro %s. Add it to confMILTER_MACROS_%s for better results.", macro, scope);
|
|
430 my_syslog(priv, buf);
|
|
431 warnedmacro = true;
|
|
432 }
|
|
433
|