1*db328f6cSDavid du Colombier /*
2*db328f6cSDavid du Colombier * greylisting is the practice of making unknown callers call twice, with
3*db328f6cSDavid du Colombier * a pause between them, before accepting their mail and adding them to a
4*db328f6cSDavid du Colombier * whitelist of known callers.
5*db328f6cSDavid du Colombier *
6*db328f6cSDavid du Colombier * There's a bit of a problem with yahoo and other large sources of mail;
7*db328f6cSDavid du Colombier * they have a vast pool of machines that all run the same queue(s), so a
8*db328f6cSDavid du Colombier * 451 retry can come from a different IP address for many, many retries,
9*db328f6cSDavid du Colombier * and it can take ~5 hours for the same IP to call us back. To cope
10*db328f6cSDavid du Colombier * better with this, we immediately accept mail from any system on the
11*db328f6cSDavid du Colombier * same class C subnet (IPv4 /24) as anybody on our whitelist, since the
12*db328f6cSDavid du Colombier * mail-sending machines tend to be clustered within a class C subnet.
13*db328f6cSDavid du Colombier *
14*db328f6cSDavid du Colombier * Various other goofballs, notably the IEEE, try to send mail just
15*db328f6cSDavid du Colombier * before 9 AM, then refuse to try again until after 5 PM. D'oh!
16*db328f6cSDavid du Colombier */
17e288d156SDavid du Colombier #include "common.h"
18e288d156SDavid du Colombier #include "smtpd.h"
19e288d156SDavid du Colombier #include "smtp.h"
20e288d156SDavid du Colombier #include <ctype.h>
21e288d156SDavid du Colombier #include <ip.h>
22fd597ed8SDavid du Colombier #include <ndb.h>
23e288d156SDavid du Colombier
24*db328f6cSDavid du Colombier enum {
25*db328f6cSDavid du Colombier Nonspammax = 14*60*60, /* must call back within this time if real */
26*db328f6cSDavid du Colombier Nonspammin = 5*60, /* must wait this long to retry */
27*db328f6cSDavid du Colombier };
28*db328f6cSDavid du Colombier
2908863f9aSDavid du Colombier typedef struct {
3008863f9aSDavid du Colombier int existed; /* these two are distinct to cope with errors */
3108863f9aSDavid du Colombier int created;
3208863f9aSDavid du Colombier int noperm;
3308863f9aSDavid du Colombier long mtime; /* mod time, iff it already existed */
3408863f9aSDavid du Colombier } Greysts;
3508863f9aSDavid du Colombier
363b56890dSDavid du Colombier static char whitelist[] = "/mail/grey/whitelist";
37e288d156SDavid du Colombier
38e288d156SDavid du Colombier /*
39e288d156SDavid du Colombier * matches ip addresses or subnets in whitelist against nci->rsys.
403b56890dSDavid du Colombier * ignores comments and blank lines in /mail/grey/whitelist.
41e288d156SDavid du Colombier */
42e288d156SDavid du Colombier static int
onwhitelist(void)43e288d156SDavid du Colombier onwhitelist(void)
44e288d156SDavid du Colombier {
45e288d156SDavid du Colombier int lnlen;
463b56890dSDavid du Colombier char *line, *parse, *p;
47e288d156SDavid du Colombier char input[128];
48*db328f6cSDavid du Colombier uchar *mask;
49e288d156SDavid du Colombier uchar mask4[IPaddrlen], addr4[IPaddrlen];
50*db328f6cSDavid du Colombier uchar rmask[IPaddrlen], addr[IPaddrlen];
51*db328f6cSDavid du Colombier uchar ipmasked[IPaddrlen], addrmasked[IPaddrlen];
52e288d156SDavid du Colombier Biobuf *wl;
53e288d156SDavid du Colombier
54e288d156SDavid du Colombier wl = Bopen(whitelist, OREAD);
55e288d156SDavid du Colombier if (wl == nil)
56e288d156SDavid du Colombier return 1;
57e288d156SDavid du Colombier while ((line = Brdline(wl, '\n')) != nil) {
58e288d156SDavid du Colombier lnlen = Blinelen(wl);
59e288d156SDavid du Colombier line[lnlen-1] = '\0'; /* clobber newline */
60e288d156SDavid du Colombier
613b56890dSDavid du Colombier p = strpbrk(line, " \t");
623b56890dSDavid du Colombier if (p)
633b56890dSDavid du Colombier *p = 0;
643b56890dSDavid du Colombier if (line[0] == '#' || line[0] == 0)
653b56890dSDavid du Colombier continue;
663b56890dSDavid du Colombier
67*db328f6cSDavid du Colombier /* default mask is /24 (v4) or /128 (v6) for bare IP */
68e288d156SDavid du Colombier parse = line;
69e288d156SDavid du Colombier if (strchr(line, '/') == nil) {
703b56890dSDavid du Colombier strecpy(input, input + sizeof input - 5, line);
71*db328f6cSDavid du Colombier if (strchr(line, ':') != nil) /* v6? */
72e288d156SDavid du Colombier strcat(input, "/128");
73*db328f6cSDavid du Colombier else if (strchr(line, '.') != nil)
74*db328f6cSDavid du Colombier strcat(input, "/24"); /* was /32 */
75e288d156SDavid du Colombier parse = input;
76e288d156SDavid du Colombier }
77*db328f6cSDavid du Colombier mask = rmask;
78*db328f6cSDavid du Colombier if (strchr(line, ':') != nil) { /* v6? */
79*db328f6cSDavid du Colombier parseip(addr, parse);
80*db328f6cSDavid du Colombier p = strchr(parse, '/');
81*db328f6cSDavid du Colombier if (p != nil)
82*db328f6cSDavid du Colombier parseipmask(mask, p);
83*db328f6cSDavid du Colombier else
84*db328f6cSDavid du Colombier mask = IPallbits;
85*db328f6cSDavid du Colombier } else {
86e288d156SDavid du Colombier v4parsecidr(addr4, mask4, parse);
87e288d156SDavid du Colombier v4tov6(addr, addr4);
88e288d156SDavid du Colombier v4tov6(mask, mask4);
89*db328f6cSDavid du Colombier }
90e288d156SDavid du Colombier maskip(addr, mask, addrmasked);
9146595261SDavid du Colombier maskip(rsysip, mask, ipmasked);
92*db328f6cSDavid du Colombier if (equivip6(ipmasked, addrmasked))
93e288d156SDavid du Colombier break;
94e288d156SDavid du Colombier }
95e288d156SDavid du Colombier Bterm(wl);
96e288d156SDavid du Colombier return line != nil;
97e288d156SDavid du Colombier }
98e288d156SDavid du Colombier
99e288d156SDavid du Colombier static int mkdirs(char *);
100e288d156SDavid du Colombier
101e288d156SDavid du Colombier /*
102e288d156SDavid du Colombier * if any directories leading up to path don't exist, create them.
103e288d156SDavid du Colombier * modifies but restores path.
104e288d156SDavid du Colombier */
105e288d156SDavid du Colombier static int
mkpdirs(char * path)106e288d156SDavid du Colombier mkpdirs(char *path)
107e288d156SDavid du Colombier {
108e288d156SDavid du Colombier int rv = 0;
109e288d156SDavid du Colombier char *sl = strrchr(path, '/');
110e288d156SDavid du Colombier
111e288d156SDavid du Colombier if (sl != nil) {
112e288d156SDavid du Colombier *sl = '\0';
113e288d156SDavid du Colombier rv = mkdirs(path);
114e288d156SDavid du Colombier *sl = '/';
115e288d156SDavid du Colombier }
116e288d156SDavid du Colombier return rv;
117e288d156SDavid du Colombier }
118e288d156SDavid du Colombier
119e288d156SDavid du Colombier /*
120e288d156SDavid du Colombier * if path or any directories leading up to it don't exist, create them.
121e288d156SDavid du Colombier * modifies but restores path.
122e288d156SDavid du Colombier */
123e288d156SDavid du Colombier static int
mkdirs(char * path)124e288d156SDavid du Colombier mkdirs(char *path)
125e288d156SDavid du Colombier {
126e288d156SDavid du Colombier int fd;
127e288d156SDavid du Colombier
128e288d156SDavid du Colombier if (access(path, AEXIST) >= 0)
129e288d156SDavid du Colombier return 0;
130e288d156SDavid du Colombier
131e288d156SDavid du Colombier /* make presumed-missing intermediate directories */
132e288d156SDavid du Colombier if (mkpdirs(path) < 0)
133e288d156SDavid du Colombier return -1;
134e288d156SDavid du Colombier
135e288d156SDavid du Colombier /* make final directory */
136e288d156SDavid du Colombier fd = create(path, OREAD, 0777|DMDIR);
137e288d156SDavid du Colombier if (fd < 0)
138e288d156SDavid du Colombier /*
139e288d156SDavid du Colombier * we may have lost a race; if the directory now exists,
140e288d156SDavid du Colombier * it's okay.
141e288d156SDavid du Colombier */
142e288d156SDavid du Colombier return access(path, AEXIST) < 0? -1: 0;
143e288d156SDavid du Colombier close(fd);
144e288d156SDavid du Colombier return 0;
145e288d156SDavid du Colombier }
146e288d156SDavid du Colombier
14708863f9aSDavid du Colombier static long
getmtime(char * file)14808863f9aSDavid du Colombier getmtime(char *file)
149e288d156SDavid du Colombier {
15046595261SDavid du Colombier int fd;
15108863f9aSDavid du Colombier long mtime = -1;
15246595261SDavid du Colombier Dir *ds;
153e288d156SDavid du Colombier
15446595261SDavid du Colombier fd = open(file, ORDWR);
15546595261SDavid du Colombier if (fd < 0)
15646595261SDavid du Colombier return mtime;
15746595261SDavid du Colombier ds = dirfstat(fd);
15808863f9aSDavid du Colombier if (ds != nil) {
15908863f9aSDavid du Colombier mtime = ds->mtime;
1607e254d1cSDavid du Colombier /*
1617e254d1cSDavid du Colombier * new twist: update file's mtime after reading it,
1627e254d1cSDavid du Colombier * so each call resets the future time after which
1637e254d1cSDavid du Colombier * we'll accept calls. thus spammers who keep pounding
1647e254d1cSDavid du Colombier * us lose, but just pausing for a few minutes and retrying
1657e254d1cSDavid du Colombier * will succeed.
1667e254d1cSDavid du Colombier */
16746595261SDavid du Colombier if (0) {
16846595261SDavid du Colombier /*
16946595261SDavid du Colombier * apparently none can't do this wstat
17046595261SDavid du Colombier * (permission denied);
17146595261SDavid du Colombier * more undocumented whacky none behaviour.
17246595261SDavid du Colombier */
1737e254d1cSDavid du Colombier ds->mtime = time(0);
17446595261SDavid du Colombier if (dirfwstat(fd, ds) < 0)
17546595261SDavid du Colombier syslog(0, "smtpd", "dirfwstat %s: %r", file);
17608863f9aSDavid du Colombier }
17746595261SDavid du Colombier free(ds);
17846595261SDavid du Colombier write(fd, "x", 1);
17946595261SDavid du Colombier }
18046595261SDavid du Colombier close(fd);
18108863f9aSDavid du Colombier return mtime;
18208863f9aSDavid du Colombier }
183e288d156SDavid du Colombier
18408863f9aSDavid du Colombier static void
tryaddgrey(char * file,Greysts * gsp)18508863f9aSDavid du Colombier tryaddgrey(char *file, Greysts *gsp)
18608863f9aSDavid du Colombier {
18746595261SDavid du Colombier int fd = create(file, OWRITE|OEXCL, 0666);
188e288d156SDavid du Colombier
18908863f9aSDavid du Colombier gsp->created = (fd >= 0);
19008863f9aSDavid du Colombier if (fd >= 0) {
19108863f9aSDavid du Colombier close(fd);
19208863f9aSDavid du Colombier gsp->existed = 0; /* just created; couldn't have existed */
1937e254d1cSDavid du Colombier gsp->mtime = time(0);
19408863f9aSDavid du Colombier } else {
19508863f9aSDavid du Colombier /*
19608863f9aSDavid du Colombier * why couldn't we create file? it must have existed
19708863f9aSDavid du Colombier * (or we were denied perm on parent dir.).
19808863f9aSDavid du Colombier * if it existed, fill in gsp->mtime; otherwise
19908863f9aSDavid du Colombier * make presumed-missing intermediate directories.
20008863f9aSDavid du Colombier */
20108863f9aSDavid du Colombier gsp->existed = access(file, AEXIST) >= 0;
20208863f9aSDavid du Colombier if (gsp->existed)
20308863f9aSDavid du Colombier gsp->mtime = getmtime(file);
20408863f9aSDavid du Colombier else if (mkpdirs(file) < 0)
20508863f9aSDavid du Colombier gsp->noperm = 1;
20608863f9aSDavid du Colombier }
20708863f9aSDavid du Colombier }
20808863f9aSDavid du Colombier
20908863f9aSDavid du Colombier static void
addgreylist(char * file,Greysts * gsp)21008863f9aSDavid du Colombier addgreylist(char *file, Greysts *gsp)
21108863f9aSDavid du Colombier {
21208863f9aSDavid du Colombier tryaddgrey(file, gsp);
21308863f9aSDavid du Colombier if (!gsp->created && !gsp->existed && !gsp->noperm)
21408863f9aSDavid du Colombier /* retry the greylist entry with parent dirs created */
21508863f9aSDavid du Colombier tryaddgrey(file, gsp);
216e288d156SDavid du Colombier }
217e288d156SDavid du Colombier
218e288d156SDavid du Colombier static int
recentcall(Greysts * gsp)21908863f9aSDavid du Colombier recentcall(Greysts *gsp)
220e288d156SDavid du Colombier {
22108863f9aSDavid du Colombier long delay = time(0) - gsp->mtime;
22208863f9aSDavid du Colombier
22308863f9aSDavid du Colombier if (!gsp->existed)
22408863f9aSDavid du Colombier return 0;
22508863f9aSDavid du Colombier /* reject immediate call-back; spammers are doing that now */
226f097827aSDavid du Colombier return delay >= Nonspammin && delay <= Nonspammax;
227e288d156SDavid du Colombier }
228e288d156SDavid du Colombier
229e288d156SDavid du Colombier /*
230e288d156SDavid du Colombier * policy: if (caller-IP, my-IP, rcpt) is not on the greylist,
231e288d156SDavid du Colombier * reject this message as "451 temporary failure". if the caller is real,
232e288d156SDavid du Colombier * he'll retry soon, otherwise he's a spammer.
233e288d156SDavid du Colombier * at the first rejection, create a greylist entry for (my-ip, caller-ip,
234e288d156SDavid du Colombier * rcpt, time), where time is the file's mtime. if they call back and there's
235e288d156SDavid du Colombier * already a greylist entry, and it's within the allowed interval,
236e288d156SDavid du Colombier * add their IP to the append-only whitelist.
237e288d156SDavid du Colombier *
238e288d156SDavid du Colombier * greylist files can be removed at will; at worst they'll cause a few
239e288d156SDavid du Colombier * extra retries.
240e288d156SDavid du Colombier */
241e288d156SDavid du Colombier
242e288d156SDavid du Colombier static int
isrcptrecent(char * rcpt)243e288d156SDavid du Colombier isrcptrecent(char *rcpt)
244e288d156SDavid du Colombier {
245e288d156SDavid du Colombier char *user;
246e288d156SDavid du Colombier char file[256];
24708863f9aSDavid du Colombier Greysts gs;
24808863f9aSDavid du Colombier Greysts *gsp = &gs;
249e288d156SDavid du Colombier
250e288d156SDavid du Colombier if (rcpt[0] == '\0' || strchr(rcpt, '/') != nil ||
251e288d156SDavid du Colombier strcmp(rcpt, ".") == 0 || strcmp(rcpt, "..") == 0)
252e288d156SDavid du Colombier return 0;
253e288d156SDavid du Colombier
254e288d156SDavid du Colombier /* shorten names to fit pre-fossil or pre-9p2000 file servers */
255e288d156SDavid du Colombier user = strrchr(rcpt, '!');
256e288d156SDavid du Colombier if (user == nil)
257e288d156SDavid du Colombier user = rcpt;
258e288d156SDavid du Colombier else
259e288d156SDavid du Colombier user++;
260e288d156SDavid du Colombier
26108863f9aSDavid du Colombier /* check & try to update the grey list entry */
2623b56890dSDavid du Colombier snprint(file, sizeof file, "/mail/grey/tmp/%s/%s/%s",
263e288d156SDavid du Colombier nci->lsys, nci->rsys, user);
26408863f9aSDavid du Colombier memset(gsp, 0, sizeof *gsp);
26508863f9aSDavid du Colombier addgreylist(file, gsp);
266e288d156SDavid du Colombier
267e288d156SDavid du Colombier /* if on greylist already and prior call was recent, add to whitelist */
26808863f9aSDavid du Colombier if (gsp->existed && recentcall(gsp)) {
269e288d156SDavid du Colombier syslog(0, "smtpd",
270e288d156SDavid du Colombier "%s/%s was grey; adding IP to white", nci->rsys, rcpt);
271e288d156SDavid du Colombier return 1;
27208863f9aSDavid du Colombier } else if (gsp->existed)
27346595261SDavid du Colombier syslog(0, "smtpd", "call for %s/%s was just minutes ago "
27446595261SDavid du Colombier "or long ago", nci->rsys, rcpt);
275e288d156SDavid du Colombier else
27608863f9aSDavid du Colombier syslog(0, "smtpd", "no call registered for %s/%s; registering",
277e288d156SDavid du Colombier nci->rsys, rcpt);
278e288d156SDavid du Colombier return 0;
279e288d156SDavid du Colombier }
280e288d156SDavid du Colombier
281e288d156SDavid du Colombier void
vfysenderhostok(void)282e288d156SDavid du Colombier vfysenderhostok(void)
283e288d156SDavid du Colombier {
284fd597ed8SDavid du Colombier char *fqdn;
285e288d156SDavid du Colombier int recent = 0;
286e288d156SDavid du Colombier Link *l;
287e288d156SDavid du Colombier
288e288d156SDavid du Colombier if (onwhitelist())
289e288d156SDavid du Colombier return;
290e288d156SDavid du Colombier
291e288d156SDavid du Colombier for (l = rcvers.first; l; l = l->next)
292e288d156SDavid du Colombier if (isrcptrecent(s_to_c(l->p)))
293e288d156SDavid du Colombier recent = 1;
294e288d156SDavid du Colombier
295e288d156SDavid du Colombier /* if on greylist already and prior call was recent, add to whitelist */
296e288d156SDavid du Colombier if (recent) {
29708863f9aSDavid du Colombier int fd = create(whitelist, OWRITE, 0666|DMAPPEND);
298e288d156SDavid du Colombier
299e288d156SDavid du Colombier if (fd >= 0) {
300e288d156SDavid du Colombier seek(fd, 0, 2); /* paranoia */
301e06f534bSDavid du Colombier fqdn = csgetvalue(nci->root, "ip", nci->rsys, "dom",
302e06f534bSDavid du Colombier nil);
303e06f534bSDavid du Colombier if (fqdn != nil)
3043b56890dSDavid du Colombier fprint(fd, "%s %s\n", nci->rsys, fqdn);
305fd597ed8SDavid du Colombier else
3063b56890dSDavid du Colombier fprint(fd, "%s\n", nci->rsys);
307e06f534bSDavid du Colombier free(fqdn);
308e288d156SDavid du Colombier close(fd);
309e288d156SDavid du Colombier }
310e288d156SDavid du Colombier } else {
311e288d156SDavid du Colombier syslog(0, "smtpd",
312e288d156SDavid du Colombier "no recent call from %s for a rcpt; rejecting with temporary failure",
313e288d156SDavid du Colombier nci->rsys);
314e288d156SDavid du Colombier reply("451 please try again soon from the same IP.\r\n");
315e288d156SDavid du Colombier exits("no recent call for a rcpt");
316e288d156SDavid du Colombier }
317e288d156SDavid du Colombier }
318