1 #include "common.h"
2 #include "smtpd.h"
3 #include "smtp.h"
4 #include <ctype.h>
5 #include <ip.h>
6 #include <ndb.h>
7 
8 typedef struct {
9 	int	existed;	/* these two are distinct to cope with errors */
10 	int	created;
11 	int	noperm;
12 	long	mtime;		/* mod time, iff it already existed */
13 } Greysts;
14 
15 /*
16  * There's a bit of a problem with yahoo; they apparently have a vast
17  * pool of machines that all run the same queue(s), so a 451 retry can
18  * come from a different IP address for many, many retries, and it can
19  * take ~5 hours for the same IP to call us back.  Various other goofballs,
20  * notably the IEEE, try to send mail just before 9 AM, then refuse to try
21  * again until after 5 PM.  Doh!
22  */
23 enum {
24 	Nonspammax = 14*60*60,  /* must call back within this time if real */
25 };
26 static char *whitelist = "#9/mail/lib/whitelist";
27 
28 /*
29  * matches ip addresses or subnets in whitelist against nci->rsys.
30  * ignores comments and blank lines in /mail/lib/whitelist.
31  */
32 static int
onwhitelist(void)33 onwhitelist(void)
34 {
35 	int lnlen;
36 	char *line, *parse;
37 	char input[128];
38 	uchar ip[IPaddrlen], ipmasked[IPaddrlen];
39 	uchar mask4[IPaddrlen], addr4[IPaddrlen];
40 	uchar mask[IPaddrlen], addr[IPaddrlen], addrmasked[IPaddrlen];
41 	Biobuf *wl;
42 	static int beenhere;
43 
44 	if (!beenhere) {
45 		beenhere = 1;
46 		fmtinstall('I', eipfmt);
47 		whitelist = unsharp(whitelist);
48 	}
49 
50 	parseip(ip, nci->rsys);
51 	wl = Bopen(whitelist, OREAD);
52 	if (wl == nil)
53 		return 1;
54 	while ((line = Brdline(wl, '\n')) != nil) {
55 		if (line[0] == '#' || line[0] == '\n')
56 			continue;
57 		lnlen = Blinelen(wl);
58 		line[lnlen-1] = '\0';		/* clobber newline */
59 
60 		/* default mask is /32 (v4) or /128 (v6) for bare IP */
61 		parse = line;
62 		if (strchr(line, '/') == nil) {
63 			strncpy(input, line, sizeof input - 5);
64 			if (strchr(line, '.') != nil)
65 				strcat(input, "/32");
66 			else
67 				strcat(input, "/128");
68 			parse = input;
69 		}
70 		/* sorry, dave; where's parsecidr for v4 or v6? */
71 		v4parsecidr(addr4, mask4, parse);
72 		v4tov6(addr, addr4);
73 		v4tov6(mask, mask4);
74 
75 		maskip(addr, mask, addrmasked);
76 		maskip(ip, mask, ipmasked);
77 		if (memcmp(ipmasked, addrmasked, IPaddrlen) == 0)
78 			break;
79 	}
80 	Bterm(wl);
81 	return line != nil;
82 }
83 
84 static int mkdirs(char *);
85 
86 /*
87  * if any directories leading up to path don't exist, create them.
88  * modifies but restores path.
89  */
90 static int
mkpdirs(char * path)91 mkpdirs(char *path)
92 {
93 	int rv = 0;
94 	char *sl = strrchr(path, '/');
95 
96 	if (sl != nil) {
97 		*sl = '\0';
98 		rv = mkdirs(path);
99 		*sl = '/';
100 	}
101 	return rv;
102 }
103 
104 /*
105  * if path or any directories leading up to it don't exist, create them.
106  * modifies but restores path.
107  */
108 static int
mkdirs(char * path)109 mkdirs(char *path)
110 {
111 	int fd;
112 
113 	if (access(path, AEXIST) >= 0)
114 		return 0;
115 
116 	/* make presumed-missing intermediate directories */
117 	if (mkpdirs(path) < 0)
118 		return -1;
119 
120 	/* make final directory */
121 	fd = create(path, OREAD, 0777|DMDIR);
122 	if (fd < 0)
123 		/*
124 		 * we may have lost a race; if the directory now exists,
125 		 * it's okay.
126 		 */
127 		return access(path, AEXIST) < 0? -1: 0;
128 	close(fd);
129 	return 0;
130 }
131 
132 static long
getmtime(char * file)133 getmtime(char *file)
134 {
135 	long mtime = -1;
136 	Dir *ds = dirstat(file);
137 
138 	if (ds != nil) {
139 		mtime = ds->mtime;
140 		free(ds);
141 	}
142 	return mtime;
143 }
144 
145 static void
tryaddgrey(char * file,Greysts * gsp)146 tryaddgrey(char *file, Greysts *gsp)
147 {
148 	int fd = create(file, OWRITE|OEXCL, 0444|DMEXCL);
149 
150 	gsp->created = (fd >= 0);
151 	if (fd >= 0) {
152 		close(fd);
153 		gsp->existed = 0;  /* just created; couldn't have existed */
154 	} else {
155 		/*
156 		 * why couldn't we create file? it must have existed
157 		 * (or we were denied perm on parent dir.).
158 		 * if it existed, fill in gsp->mtime; otherwise
159 		 * make presumed-missing intermediate directories.
160 		 */
161 		gsp->existed = access(file, AEXIST) >= 0;
162 		if (gsp->existed)
163 			gsp->mtime = getmtime(file);
164 		else if (mkpdirs(file) < 0)
165 			gsp->noperm = 1;
166 	}
167 }
168 
169 static void
addgreylist(char * file,Greysts * gsp)170 addgreylist(char *file, Greysts *gsp)
171 {
172 	tryaddgrey(file, gsp);
173 	if (!gsp->created && !gsp->existed && !gsp->noperm)
174 		/* retry the greylist entry with parent dirs created */
175 		tryaddgrey(file, gsp);
176 }
177 
178 static int
recentcall(Greysts * gsp)179 recentcall(Greysts *gsp)
180 {
181 	long delay = time(0) - gsp->mtime;
182 
183 	if (!gsp->existed)
184 		return 0;
185 	/* reject immediate call-back; spammers are doing that now */
186 	return delay >= 30 && delay <= Nonspammax;
187 }
188 
189 /*
190  * policy: if (caller-IP, my-IP, rcpt) is not on the greylist,
191  * reject this message as "451 temporary failure".  if the caller is real,
192  * he'll retry soon, otherwise he's a spammer.
193  * at the first rejection, create a greylist entry for (my-ip, caller-ip,
194  * rcpt, time), where time is the file's mtime.  if they call back and there's
195  * already a greylist entry, and it's within the allowed interval,
196  * add their IP to the append-only whitelist.
197  *
198  * greylist files can be removed at will; at worst they'll cause a few
199  * extra retries.
200  */
201 
202 static int
isrcptrecent(char * rcpt)203 isrcptrecent(char *rcpt)
204 {
205 	char *user;
206 	char file[256];
207 	Greysts gs;
208 	Greysts *gsp = &gs;
209 
210 	if (rcpt[0] == '\0' || strchr(rcpt, '/') != nil ||
211 	    strcmp(rcpt, ".") == 0 || strcmp(rcpt, "..") == 0)
212 		return 0;
213 
214 	/* shorten names to fit pre-fossil or pre-9p2000 file servers */
215 	user = strrchr(rcpt, '!');
216 	if (user == nil)
217 		user = rcpt;
218 	else
219 		user++;
220 
221 	/* check & try to update the grey list entry */
222 	snprint(file, sizeof file, "%s/mail/grey/%s/%s/%s",
223 		get9root(), nci->lsys, nci->rsys, user);
224 	memset(gsp, 0, sizeof *gsp);
225 	addgreylist(file, gsp);
226 
227 	/* if on greylist already and prior call was recent, add to whitelist */
228 	if (gsp->existed && recentcall(gsp)) {
229 		syslog(0, "smtpd",
230 			"%s/%s was grey; adding IP to white", nci->rsys, rcpt);
231 		return 1;
232 	} else if (gsp->existed)
233 		syslog(0, "smtpd", "call for %s/%s was seconds ago or long ago",
234 			nci->rsys, rcpt);
235 	else
236 		syslog(0, "smtpd", "no call registered for %s/%s; registering",
237 			nci->rsys, rcpt);
238 	return 0;
239 }
240 
241 void
vfysenderhostok(void)242 vfysenderhostok(void)
243 {
244 	int recent = 0;
245 	Link *l;
246 
247 	if (onwhitelist())
248 		return;
249 
250 	for (l = rcvers.first; l; l = l->next)
251 		if (isrcptrecent(s_to_c(l->p)))
252 			recent = 1;
253 
254 	/* if on greylist already and prior call was recent, add to whitelist */
255 	if (recent) {
256 		int fd = create(whitelist, OWRITE, 0666|DMAPPEND);
257 
258 		if (fd >= 0) {
259 			seek(fd, 0, 2);			/* paranoia */
260 			fprint(fd, "# unknown\n%s\n\n", nci->rsys);
261 			close(fd);
262 		}
263 	} else {
264 		syslog(0, "smtpd",
265 	"no recent call from %s for a rcpt; rejecting with temporary failure",
266 			nci->rsys);
267 		reply("451 please try again soon from the same IP.\r\n");
268 		exits("no recent call for a rcpt");
269 	}
270 }
271