chiark / gitweb /
Merge branch 'master' of git.distorted.org.uk:~mdw/publish/public-git/misc
[misc] / qmail-checkspam.c
CommitLineData
8d769cc9 1/* -*-c-*-
2 *
9a4b2474 3 * $Id: qmail-checkspam.c,v 1.2 2004/04/08 01:36:26 mdw Exp $
8d769cc9 4 *
5 * Filter messages for spam
6 *
7 * (c) 2003 Mark Wooding
8 */
9
841e5aca 10/*----- Licensing notice --------------------------------------------------*
8d769cc9 11 *
12 * This program is free software; you can redistribute it and/or modify
13 * it under the terms of the GNU General Public License as published by
14 * the Free Software Foundation; either version 2 of the License, or
15 * (at your option) any later version.
841e5aca 16 *
8d769cc9 17 * This program is distributed in the hope that it will be useful,
18 * but WITHOUT ANY WARRANTY; without even the implied warranty of
19 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 * GNU General Public License for more details.
841e5aca 21 *
8d769cc9 22 * You should have received a copy of the GNU General Public License
23 * along with this program; if not, write to the Free Software Foundation,
24 * Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
25 */
26
8d769cc9 27/*----- Header files ------------------------------------------------------*/
28
29#include <ctype.h>
30#include <errno.h>
913eb210 31#include <limits.h>
8d769cc9 32#include <stdio.h>
33#include <stdlib.h>
34#include <string.h>
35
36#include <sys/types.h>
37#include <sys/unistd.h>
38
7bba3c9c
MW
39#include <syslog.h>
40
8d769cc9 41#include <libspamc.h>
42
43/*----- Main code ---------------------------------------------------------*/
44
45static const char *strenv(const char *e, const char *d)
46{
47 const char *p = getenv(e);
48 if (!p) return (d);
49 return (p);
50}
51
52static double dblenv(const char *e, double d)
53{
54 const char *p = getenv(e);
55 char *q;
56 int err = errno;
57 double f;
58 if (!p) return (d);
59 errno = 0;
60 f = strtod(p, &q);
7bba3c9c
MW
61 if (errno) {
62 syslog(LOG_ERR, "bad floating value `%s' for %s'; ignoring", p, e);
63 return (d);
64 }
8d769cc9 65 errno = err;
66 return (f);
67}
68
69static int intenv(const char *e, int d)
70{
71 const char *p = getenv(e);
72 char *q;
73 int err = errno;
74 long l;
75 if (!p) return (d);
76 errno = 0;
77 l = strtol(p, &q, 0);
78 if (errno) return (d);
79 errno = err;
7bba3c9c
MW
80 if (l < 0 || l > INT_MAX) {
81 syslog(LOG_ERR, "bad integer value `%s' for `%s'; ignoring", p, e);
82 return (d);
83 }
8d769cc9 84 return ((int)l);
85}
86
7bba3c9c
MW
87static int safewrite(int fd, const void *p, size_t sz)
88{
89 const char *pp = p;
90 ssize_t n;
91
92 while (sz > 0) {
93 n = write(fd, pp, sz);
94 if (sz <= 0 && errno != EINTR && errno != EAGAIN)
95 return (-1);
96 sz -= n;
97 pp += n;
98 }
99 return (0);
100}
101
102static int shovel(int from, int to, const char *what)
8d769cc9 103{
104 char buf[4096];
105 ssize_t n;
8d769cc9 106
107 for (;;) {
108 n = read(from, buf, sizeof(buf));
7bba3c9c
MW
109 if (n < 0 && errno != EINTR && errno != EAGAIN) {
110 syslog(LOG_ERR, "failed to read %s: %m", what);
8d769cc9 111 return (-1);
7bba3c9c 112 }
8d769cc9 113 else if (!n)
114 return (0);
7bba3c9c
MW
115 if (safewrite(to, buf, n) < 0) {
116 syslog(LOG_ERR, "failed to write %s: %m", what);
117 return (-1);
118 }
119 }
120}
121
122#define ENVBUF 4096
123#define NRCPT 16
124struct envelope {
125 unsigned f;
126#define EF_TRUNC 1u
127 char buf[ENVBUF + 1];
128 size_t n;
129 const char *send;
130 const char *rcpt[NRCPT];
131 size_t nrcpt;
132};
133
134static int readenv(struct envelope *e)
135{
136 ssize_t n;
137 const char *p, *l;
138
139 /* Read the raw envelope data. */
140 e->n = 0;
141 e->f = 0;
142 while (e->n < ENVBUF) {
143 n = read(1, e->buf + e->n, ENVBUF - e->n);
144 if (n < 0 && errno != EINTR && errno != EAGAIN) {
145 syslog(LOG_ERR, "failed to read envelope: %m");
146 return (-1);
8d769cc9 147 }
7bba3c9c
MW
148 if (!n) break;
149 e->n += n;
150 }
151 e->buf[e->n] = 0;
152
153 /* Parse up the envelope data. */
154 p = e->buf; l = p + e->n;
155 e->nrcpt = 0;
156 if (*p++ != 'F') {
157 e->send = "<invalid>";
158 syslog(LOG_ERR, "corrupt envelope (no sender marker)");
159 return (-1);
160 }
161 e->send = p;
162 for (;;) {
163 if (p >= l) goto trunc;
164 else if (!*p++) break;
8d769cc9 165 }
7bba3c9c
MW
166
167 for (;;) {
168 if (p >= l) goto trunc;
169 if (!*p) break;
170 else if (*p++ != 'T') {
171 syslog(LOG_ERR, "corrupt envelope (no recipient marker)");
172 return (-1);
173 }
174 if (p >= l) goto trunc;
175 if (e->nrcpt < NRCPT) e->rcpt[e->nrcpt++] = p;
176 for (;;) {
177 if (p >= l) goto trunc;
178 else if (!*p++) break;
179 }
180 }
181 return (0);
182
183 /* Failed to reach the final terminator; maybe there's more. This isn't a
184 * very bad situation.
185 */
186trunc:
187 e->f |= EF_TRUNC;
188 return (0);
189}
190
191static void real(void)
192{
193 const char *qmq =
194 strenv("QMAIL_CHECKSPAM_QUEUE", "/var/qmail/bin/qmail-queue");
195 execlp(qmq, qmq, (char *)0);
196 syslog(LOG_ERR, "failed to exec %s: %m", qmq);
197 exit(56);
198}
199
200static int split(int *fd_m, int *fd_e)
201{
202 pid_t kid;
203 int pm[2], pe[2];
204
205 if (pipe(pm) || pipe(pe)) return (-1);
206 if ((kid = fork()) < 0) return (-1);
207 if (kid) {
208 dup2(pm[0], 0); close(pm[0]); close(pm[1]);
209 dup2(pe[0], 1); close(pe[0]); close(pe[1]);
210 real();
211 } else {
212 close(pm[0]); *fd_m = pm[1];
213 close(pe[0]); *fd_e = pe[1];
214 }
215 return (0);
216}
217
218static int writemsg(struct message *m, int fd_m, int shovelp)
219{
220 if (message_write(fd_m, m) < 0) {
221 syslog(LOG_ERR, "failed to write message body: %m");
222 return (-1);
223 }
224 if (shovelp && shovel(0, fd_m, "message body"))
225 return (-1);
226 close(fd_m);
227 return (0);
228}
229
230static int writeenv(struct envelope *e, int fd_e)
231{
232 if (safewrite(fd_e, e->buf, e->n) < 0) {
233 syslog(LOG_ERR, "failed to write envelope: %m");
234 return (-1);
235 }
236 if ((e->f & EF_TRUNC) && shovel(1, fd_e, "envelope"))
237 return (-1);
238 close(fd_e);
239 return (0);
240}
241
242static void logenv(struct envelope *e)
243{
244 size_t i;
245
246 syslog(LOG_NOTICE, "sender = %s", e->send);
247 for (i = 0; i < e->nrcpt; i++)
248 syslog(LOG_NOTICE, "recipient %lu = %s", (unsigned long)i, e->rcpt[i]);
8d769cc9 249}
250
251int main(int argc, char *argv[])
252{
253 struct sockaddr sa;
254 struct message m;
7bba3c9c
MW
255 struct envelope e;
256 int fd_m, fd_e;
257 const char *p;
258 double thresh;
259 int rc, port;
260
261 /* Set up logging output. */
262 if ((p = strrchr(argv[0], '/')) == 0) p = argv[0]; else p++;
263 openlog(p, LOG_PID, LOG_MAIL);
8d769cc9 264
7bba3c9c
MW
265 /* Read configuration from the environment. */
266 if (getenv("RELAYCLIENT")) real();
8d769cc9 267 m.max_len = intenv("QMAIL_CHECKSPAM_MAXLEN", 2 * 1024 * 1024);
268 m.timeout = intenv("QMAIL_CHECKSPAM_TIMEOUT", 300);
7bba3c9c
MW
269
270 /* Slurp an initial chunk of the message. */
8d769cc9 271 rc = message_read(0, 0, &m);
7bba3c9c
MW
272
273 switch (rc) {
274
275 case 0:
276 /* We read the message body OK. Now try to slurp in the envelope and
277 * write a useful message.
278 */
279 if (readenv(&e)) return (54);
280 syslog(LOG_NOTICE, "scanning message");
281 logenv(&e);
282
283 /* Filter the message and see what happens. */
284 p = strenv("QMAIL_CHECKSPAM_SPAMDHOST", "localhost");
285 port = intenv("QMAIL_CHECKSPAM_SPAMDPORT", 783);
286 if (lookup_host(p, port, &sa)) {
287 syslog(LOG_ERR, "lookup failed for host `%s', port %d", p, port);
288 return (56);
289 }
290 if (message_filter(&sa, "spamd", 0, &m)) {
291 syslog(LOG_ERR, "filter failed unexpectedly");
292 return (74);
293 }
294
295 /* Check the resulting score. */
296 thresh = dblenv("QMAIL_CHECKSPAM_THRESH", m.threshold);
297 if (m.score >= thresh) {
298 syslog(LOG_NOTICE, "rejecting: score %g >= %g", m.score, thresh);
299 return (31);
300 }
301 syslog(LOG_NOTICE, "accepting: score %g < %g", m.score, thresh);
302
303 /* Send the remaining stuff on to the real server. */
304 if (split(&fd_m, &fd_e)) return (56);
305 if (writemsg(&m, fd_m, 0) || writeenv(&e, fd_e)) _exit(127);
306 break;
307
308 case EX_TOOBIG:
309 /* Message was too big. We must pass it all on to the real server
310 * before picking up the envelope.
311 */
312 syslog(LOG_NOTICE, "message too large for filtering");
313 if (split(&fd_m, &fd_e)) return (56);
314 if (writemsg(&m, fd_m, 1) || readenv(&e)) _exit(127);
315 logenv(&e);
316 if (writeenv(&e, fd_e)) _exit(127);
317 break;
318
319 default:
320 syslog(LOG_ERR, "failed to read message (rc = %d): %m", rc);
321 return (54);
8d769cc9 322 }
323
7bba3c9c 324 return (0);
8d769cc9 325}
326
327/*----- That's all, folks -------------------------------------------------*/