xref: /openbsd/libexec/mail.local/mail.local.c (revision 4cfece93)
1 /*	$OpenBSD: mail.local.c,v 1.39 2020/02/09 14:59:20 millert Exp $	*/
2 
3 /*-
4  * Copyright (c) 1996-1998 Theo de Raadt <deraadt@theos.com>
5  * Copyright (c) 1996-1998 David Mazieres <dm@lcs.mit.edu>
6  * Copyright (c) 1990 The Regents of the University of California.
7  * All rights reserved.
8  *
9  * Redistribution and use in source and binary forms, with or without
10  * modification, are permitted provided that the following conditions
11  * are met:
12  * 1. Redistributions of source code must retain the above copyright
13  *    notice, this list of conditions and the following disclaimer.
14  * 2. Redistributions in binary form must reproduce the above copyright
15  *    notice, this list of conditions and the following disclaimer in the
16  *    documentation and/or other materials provided with the distribution.
17  * 3. Neither the name of the University nor the names of its contributors
18  *    may be used to endorse or promote products derived from this software
19  *    without specific prior written permission.
20  *
21  * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
22  * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
23  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
24  * ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
25  * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
26  * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
27  * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
28  * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
29  * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
30  * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
31  * SUCH DAMAGE.
32  */
33 
34 #include <sys/types.h>
35 #include <sys/stat.h>
36 #include <sys/socket.h>
37 #include <sys/wait.h>
38 #include <netinet/in.h>
39 #include <sysexits.h>
40 #include <syslog.h>
41 #include <fcntl.h>
42 #include <netdb.h>
43 #include <pwd.h>
44 #include <time.h>
45 #include <unistd.h>
46 #include <limits.h>
47 #include <errno.h>
48 #include <stdio.h>
49 #include <stdlib.h>
50 #include <string.h>
51 #include <signal.h>
52 #include "pathnames.h"
53 #include "mail.local.h"
54 
55 int
56 main(int argc, char *argv[])
57 {
58 	struct passwd *pw;
59 	int ch, fd, eval, lockfile=1;
60 	uid_t uid;
61 	char *from;
62 
63 	openlog("mail.local", LOG_PERROR, LOG_MAIL);
64 
65 	from = NULL;
66 	while ((ch = getopt(argc, argv, "lLdf:r:")) != -1)
67 		switch (ch) {
68 		case 'd':		/* backward compatible */
69 			break;
70 		case 'f':
71 		case 'r':		/* backward compatible */
72 			if (from)
73 				merr(EX_USAGE, "multiple -f options");
74 			from = optarg;
75 			break;
76 		case 'l':
77 			lockfile=1;
78 			break;
79 		case 'L':
80 			lockfile=0;
81 			break;
82 		default:
83 			usage();
84 		}
85 	argc -= optind;
86 	argv += optind;
87 
88 	if (!*argv)
89 		usage();
90 
91 	/*
92 	 * If from not specified, use the name from getlogin() if the
93 	 * uid matches, otherwise, use the name from the password file
94 	 * corresponding to the uid.
95 	 */
96 	uid = getuid();
97 	if (!from && (!(from = getlogin()) ||
98 	    !(pw = getpwnam(from)) || pw->pw_uid != uid))
99 		from = (pw = getpwuid(uid)) ? pw->pw_name : "???";
100 
101 	fd = storemail(from);
102 	for (eval = 0; *argv; ++argv) {
103 		if ((ch = deliver(fd, *argv, lockfile)) != 0)
104 			eval = ch;
105 	}
106 	exit(eval);
107 }
108 
109 int
110 storemail(char *from)
111 {
112 	FILE *fp = NULL;
113 	time_t tval;
114 	int fd, eline;
115 	size_t len;
116 	char *line, *tbuf;
117 
118 	if ((tbuf = strdup(_PATH_LOCTMP)) == NULL)
119 		merr(EX_OSERR, "unable to allocate memory");
120 	if ((fd = mkstemp(tbuf)) == -1 || !(fp = fdopen(fd, "w+")))
121 		merr(EX_OSERR, "unable to open temporary file");
122 	(void)unlink(tbuf);
123 	free(tbuf);
124 
125 	(void)time(&tval);
126 	(void)fprintf(fp, "From %s %s", from, ctime(&tval));
127 
128 	for (eline = 1, tbuf = NULL; (line = fgetln(stdin, &len));) {
129 		/* We have to NUL-terminate the line since fgetln does not */
130 		if (line[len - 1] == '\n')
131 			line[len - 1] = '\0';
132 		else {
133 			/* No trailing newline, so alloc space and copy */
134 			if ((tbuf = malloc(len + 1)) == NULL)
135 				merr(EX_OSERR, "unable to allocate memory");
136 			memcpy(tbuf, line, len);
137 			tbuf[len] = '\0';
138 			line = tbuf;
139 		}
140 		if (line[0] == '\0')
141 			eline = 1;
142 		else {
143 			if (eline && line[0] == 'F' && len > 5 &&
144 			    !memcmp(line, "From ", 5))
145 				(void)putc('>', fp);
146 			eline = 0;
147 		}
148 		(void)fprintf(fp, "%s\n", line);
149 		if (ferror(fp))
150 			break;
151 	}
152 	free(tbuf);
153 
154 	/* Output a newline; note, empty messages are allowed. */
155 	(void)putc('\n', fp);
156 	(void)fflush(fp);
157 	if (ferror(fp))
158 		merr(EX_OSERR, "temporary file write error");
159 	return(fd);
160 }
161 
162 int
163 deliver(int fd, char *name, int lockfile)
164 {
165 	struct stat sb, fsb;
166 	struct passwd *pw;
167 	int mbfd=-1, lfd=-1, rval=EX_OSERR;
168 	char biffmsg[100], buf[8*1024], path[PATH_MAX];
169 	off_t curoff;
170 	size_t off;
171 	ssize_t nr, nw;
172 
173 	/*
174 	 * Disallow delivery to unknown names -- special mailboxes can be
175 	 * handled in the sendmail aliases file.
176 	 */
177 	if (!(pw = getpwnam(name))) {
178 		mwarn("unknown name: %s", name);
179 		return(EX_NOUSER);
180 	}
181 
182 	(void)snprintf(path, sizeof path, "%s/%s", _PATH_MAILDIR, name);
183 
184 	if (lockfile) {
185 		lfd = lockspool(name, pw);
186 		if (lfd == -1)
187 			return(EX_OSERR);
188 	}
189 
190 	/* after this point, always exit via bad to remove lockfile */
191 retry:
192 	if (lstat(path, &sb)) {
193 		if (errno != ENOENT) {
194 			mwarn("%s: %s", path, strerror(errno));
195 			goto bad;
196 		}
197 		if ((mbfd = open(path, O_APPEND|O_CREAT|O_EXCL|O_WRONLY|O_EXLOCK,
198 		    S_IRUSR|S_IWUSR)) == -1) {
199 			if (errno == EEXIST) {
200 				/* file appeared since lstat */
201 				goto retry;
202 			} else {
203 				mwarn("%s: %s", path, strerror(errno));
204 				rval = EX_CANTCREAT;
205 				goto bad;
206 			}
207 		}
208 		/*
209 		 * Set the owner and group.  Historically, binmail repeated
210 		 * this at each mail delivery.  We no longer do this, assuming
211 		 * that if the ownership or permissions were changed there
212 		 * was a reason for doing so.
213 		 */
214 		if (fchown(mbfd, pw->pw_uid, pw->pw_gid) == -1) {
215 			mwarn("chown %u:%u: %s", pw->pw_uid, pw->pw_gid, name);
216 			goto bad;
217 		}
218 	} else {
219 		if (sb.st_nlink != 1 || !S_ISREG(sb.st_mode)) {
220 			mwarn("%s: linked or special file", path);
221 			goto bad;
222 		}
223 		if ((mbfd = open(path, O_APPEND|O_WRONLY|O_EXLOCK,
224 		    S_IRUSR|S_IWUSR)) == -1) {
225 			mwarn("%s: %s", path, strerror(errno));
226 			goto bad;
227 		}
228 		if (fstat(mbfd, &fsb) == -1) {
229 			/* relating error to path may be bad style */
230 			mwarn("%s: %s", path, strerror(errno));
231 			goto bad;
232 		}
233 		if (sb.st_dev != fsb.st_dev || sb.st_ino != fsb.st_ino) {
234 			mwarn("%s: changed after open", path);
235 			goto bad;
236 		}
237 		/* paranoia? */
238 		if (fsb.st_nlink != 1 || !S_ISREG(fsb.st_mode)) {
239 			mwarn("%s: linked or special file", path);
240 			rval = EX_CANTCREAT;
241 			goto bad;
242 		}
243 	}
244 
245 	curoff = lseek(mbfd, 0, SEEK_END);
246 	(void)snprintf(biffmsg, sizeof biffmsg, "%s@%lld\n", name, curoff);
247 	if (lseek(fd, 0, SEEK_SET) == (off_t)-1) {
248 		mwarn("temporary file: %s", strerror(errno));
249 		goto bad;
250 	}
251 
252 	while ((nr = read(fd, buf, sizeof(buf))) > 0)
253 		for (off = 0; off < nr;  off += nw)
254 			if ((nw = write(mbfd, buf + off, nr - off)) == -1) {
255 				mwarn("%s: %s", path, strerror(errno));
256 				(void)ftruncate(mbfd, curoff);
257 				goto bad;
258 			}
259 
260 	if (nr == 0) {
261 		rval = 0;
262 	} else {
263 		(void)ftruncate(mbfd, curoff);
264 		mwarn("temporary file: %s", strerror(errno));
265 	}
266 
267 bad:
268 	if (lfd != -1)
269 		unlockspool();
270 
271 	if (mbfd != -1) {
272 		(void)fsync(mbfd);		/* Don't wait for update. */
273 		(void)close(mbfd);		/* Implicit unlock. */
274 	}
275 
276 	if (!rval)
277 		notifybiff(biffmsg);
278 	return(rval);
279 }
280 
281 void
282 notifybiff(char *msg)
283 {
284 	static struct addrinfo *res0;
285 	struct addrinfo hints, *res;
286 	static int f = -1;
287 	size_t len;
288 	int error;
289 
290 	if (res0 == NULL) {
291 		memset(&hints, 0, sizeof(hints));
292 		hints.ai_family = PF_UNSPEC;
293 		hints.ai_socktype = SOCK_DGRAM;
294 
295 		error = getaddrinfo("localhost", "biff", &hints, &res0);
296 		if (error) {
297 			/* Be silent if biff service not available. */
298 			if (error != EAI_SERVICE) {
299 				mwarn("localhost: %s", gai_strerror(error));
300 			}
301 			return;
302 		}
303 	}
304 
305 	if (f == -1) {
306 		for (res = res0; res != NULL; res = res->ai_next) {
307 			f = socket(res->ai_family, res->ai_socktype,
308 			    res->ai_protocol);
309 			if (f != -1)
310 				break;
311 		}
312 	}
313 	if (f == -1) {
314 		mwarn("socket: %s", strerror(errno));
315 		return;
316 	}
317 
318 	len = strlen(msg) + 1;	/* XXX */
319 	if (sendto(f, msg, len, 0, res->ai_addr, res->ai_addrlen) != len)
320 		mwarn("sendto biff: %s", strerror(errno));
321 }
322 
323 static int lockfd = -1;
324 static pid_t lockpid = -1;
325 
326 int
327 lockspool(const char *name, struct passwd *pw)
328 {
329 	int pfd[2];
330 	char ch;
331 
332 	if (geteuid() == 0)
333 		return getlock(name, pw);
334 
335 	/* If not privileged, open pipe to lockspool(1) instead */
336 	if (pipe2(pfd, O_CLOEXEC) == -1) {
337 		merr(EX_OSERR, "pipe: %s", strerror(errno));
338 		return -1;
339 	}
340 
341 	signal(SIGPIPE, SIG_IGN);
342 	switch ((lockpid = fork())) {
343 	case -1:
344 		merr(EX_OSERR, "fork: %s", strerror(errno));
345 		return -1;
346 	case 0:
347 		/* child */
348 		close(pfd[0]);
349 		dup2(pfd[1], STDOUT_FILENO);
350 		execl(_PATH_LOCKSPOOL, "lockspool", (char *)NULL);
351 		merr(EX_OSERR, "execl: lockspool: %s", strerror(errno));
352 		/* NOTREACHED */
353 		break;
354 	default:
355 		/* parent */
356 		close(pfd[1]);
357 		lockfd = pfd[0];
358 		break;
359 	}
360 
361 	if (read(lockfd, &ch, 1) != 1 || ch != '1') {
362 		unlockspool();
363 		merr(EX_OSERR, "lockspool: unable to get lock");
364 	}
365 
366 	return lockfd;
367 }
368 
369 void
370 unlockspool(void)
371 {
372 	if (lockpid != -1) {
373 		waitpid(lockpid, NULL, 0);
374 		lockpid = -1;
375 	} else {
376 		rellock();
377 	}
378 	close(lockfd);
379 	lockfd = -1;
380 }
381 
382 void
383 usage(void)
384 {
385 	merr(EX_USAGE, "usage: mail.local [-Ll] [-f from] user ...");
386 }
387