1/*	$OpenBSD: mail.local.c,v 1.43 2024/05/09 08:35:03 florian 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
55int
56main(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
109int
110storemail(char *from)
111{
112	FILE *fp = NULL;
113	time_t tval;
114	int fd, eline = 1;
115	char *tbuf, *line = NULL, *cnow;
116	size_t linesize = 0;
117	ssize_t linelen;
118
119	if ((tbuf = strdup(_PATH_LOCTMP)) == NULL)
120		merr(EX_OSERR, "unable to allocate memory");
121	if ((fd = mkstemp(tbuf)) == -1 || !(fp = fdopen(fd, "w+")))
122		merr(EX_OSERR, "unable to open temporary file");
123	(void)unlink(tbuf);
124	free(tbuf);
125
126	(void)time(&tval);
127	cnow = ctime(&tval);
128	(void)fprintf(fp, "From %s %s", from, cnow ? cnow : "?\n");
129
130	while ((linelen = getline(&line, &linesize, stdin)) != -1) {
131		if (line[linelen - 1] == '\n')
132			line[linelen - 1] = '\0';
133		if (line[0] == '\0')
134			eline = 1;
135		else {
136			if (eline && !strncmp(line, "From ", 5))
137				(void)putc('>', fp);
138			eline = 0;
139		}
140		(void)fprintf(fp, "%s\n", line);
141		if (ferror(fp))
142			break;
143	}
144	free(line);
145
146	/* Output a newline; note, empty messages are allowed. */
147	(void)putc('\n', fp);
148	(void)fflush(fp);
149	if (ferror(fp))
150		merr(EX_OSERR, "temporary file write error");
151	return(fd);
152}
153
154int
155deliver(int fd, char *name, int lockfile)
156{
157	struct stat sb, fsb;
158	struct passwd *pw;
159	int mbfd=-1, lfd=-1, rval=EX_OSERR;
160	char biffmsg[100], buf[8*1024], path[PATH_MAX];
161	off_t curoff;
162	size_t off;
163	ssize_t nr, nw;
164
165	/*
166	 * Disallow delivery to unknown names -- special mailboxes can be
167	 * handled in the sendmail aliases file.
168	 */
169	if (!(pw = getpwnam(name))) {
170		mwarn("unknown name: %s", name);
171		return(EX_NOUSER);
172	}
173
174	(void)snprintf(path, sizeof path, "%s/%s", _PATH_MAILDIR, name);
175
176	if (lockfile) {
177		lfd = lockspool(name, pw);
178		if (lfd == -1)
179			return(EX_OSERR);
180	}
181
182	/* after this point, always exit via bad to remove lockfile */
183retry:
184	if (lstat(path, &sb)) {
185		if (errno != ENOENT) {
186			mwarn("%s: %s", path, strerror(errno));
187			goto bad;
188		}
189		if ((mbfd = open(path, O_APPEND|O_CREAT|O_EXCL|O_WRONLY|O_EXLOCK,
190		    S_IRUSR|S_IWUSR)) == -1) {
191			if (errno == EEXIST) {
192				/* file appeared since lstat */
193				goto retry;
194			} else {
195				mwarn("%s: %s", path, strerror(errno));
196				rval = EX_CANTCREAT;
197				goto bad;
198			}
199		}
200		/*
201		 * Set the owner and group.  Historically, binmail repeated
202		 * this at each mail delivery.  We no longer do this, assuming
203		 * that if the ownership or permissions were changed there
204		 * was a reason for doing so.
205		 */
206		if (fchown(mbfd, pw->pw_uid, pw->pw_gid) == -1) {
207			mwarn("chown %u:%u: %s", pw->pw_uid, pw->pw_gid, name);
208			goto bad;
209		}
210	} else {
211		if (sb.st_nlink != 1 || !S_ISREG(sb.st_mode)) {
212			mwarn("%s: linked or special file", path);
213			goto bad;
214		}
215		if ((mbfd = open(path, O_APPEND|O_WRONLY|O_EXLOCK,
216		    S_IRUSR|S_IWUSR)) == -1) {
217			mwarn("%s: %s", path, strerror(errno));
218			goto bad;
219		}
220		if (fstat(mbfd, &fsb) == -1) {
221			/* relating error to path may be bad style */
222			mwarn("%s: %s", path, strerror(errno));
223			goto bad;
224		}
225		if (sb.st_dev != fsb.st_dev || sb.st_ino != fsb.st_ino) {
226			mwarn("%s: changed after open", path);
227			goto bad;
228		}
229		/* paranoia? */
230		if (fsb.st_nlink != 1 || !S_ISREG(fsb.st_mode)) {
231			mwarn("%s: linked or special file", path);
232			rval = EX_CANTCREAT;
233			goto bad;
234		}
235	}
236
237	curoff = lseek(mbfd, 0, SEEK_END);
238	(void)snprintf(biffmsg, sizeof biffmsg, "%s@%lld\n", name,
239	    (long long)curoff);
240	if (lseek(fd, 0, SEEK_SET) == (off_t)-1) {
241		mwarn("temporary file: %s", strerror(errno));
242		goto bad;
243	}
244
245	while ((nr = read(fd, buf, sizeof(buf))) > 0)
246		for (off = 0; off < nr;  off += nw)
247			if ((nw = write(mbfd, buf + off, nr - off)) == -1) {
248				mwarn("%s: %s", path, strerror(errno));
249				(void)ftruncate(mbfd, curoff);
250				goto bad;
251			}
252
253	if (nr == 0) {
254		rval = 0;
255	} else {
256		(void)ftruncate(mbfd, curoff);
257		mwarn("temporary file: %s", strerror(errno));
258	}
259
260bad:
261	if (lfd != -1)
262		unlockspool();
263
264	if (mbfd != -1) {
265		(void)fsync(mbfd);		/* Don't wait for update. */
266		(void)close(mbfd);		/* Implicit unlock. */
267	}
268
269	if (!rval)
270		notifybiff(biffmsg);
271	return(rval);
272}
273
274void
275notifybiff(char *msg)
276{
277	static struct addrinfo *res0;
278	struct addrinfo hints, *res;
279	static int f = -1;
280	size_t len;
281	int error;
282
283	if (res0 == NULL) {
284		memset(&hints, 0, sizeof(hints));
285		hints.ai_family = PF_UNSPEC;
286		hints.ai_socktype = SOCK_DGRAM;
287
288		error = getaddrinfo("localhost", "biff", &hints, &res0);
289		if (error) {
290			/* Be silent if biff service not available. */
291			if (error != EAI_SERVICE) {
292				mwarn("localhost: %s", gai_strerror(error));
293			}
294			return;
295		}
296	}
297
298	if (f == -1) {
299		for (res = res0; res != NULL; res = res->ai_next) {
300			f = socket(res->ai_family, res->ai_socktype,
301			    res->ai_protocol);
302			if (f != -1)
303				break;
304		}
305	}
306	if (f == -1) {
307		mwarn("socket: %s", strerror(errno));
308		return;
309	}
310
311	len = strlen(msg) + 1;	/* XXX */
312	if (sendto(f, msg, len, 0, res->ai_addr, res->ai_addrlen) != len)
313		mwarn("sendto biff: %s", strerror(errno));
314}
315
316static int lockfd = -1;
317static pid_t lockpid = -1;
318
319int
320lockspool(const char *name, struct passwd *pw)
321{
322	int pfd[2];
323	char ch;
324
325	if (geteuid() == 0)
326		return getlock(name, pw);
327
328	/* If not privileged, open pipe to lockspool(1) instead */
329	if (pipe2(pfd, O_CLOEXEC) == -1) {
330		merr(EX_OSERR, "pipe: %s", strerror(errno));
331		return -1;
332	}
333
334	signal(SIGPIPE, SIG_IGN);
335	switch ((lockpid = fork())) {
336	case -1:
337		merr(EX_OSERR, "fork: %s", strerror(errno));
338		return -1;
339	case 0:
340		/* child */
341		close(pfd[0]);
342		dup2(pfd[1], STDOUT_FILENO);
343		execl(_PATH_LOCKSPOOL, "lockspool", (char *)NULL);
344		merr(EX_OSERR, "execl: lockspool: %s", strerror(errno));
345		/* NOTREACHED */
346		break;
347	default:
348		/* parent */
349		close(pfd[1]);
350		lockfd = pfd[0];
351		break;
352	}
353
354	if (read(lockfd, &ch, 1) != 1 || ch != '1') {
355		unlockspool();
356		merr(EX_OSERR, "lockspool: unable to get lock");
357	}
358
359	return lockfd;
360}
361
362void
363unlockspool(void)
364{
365	if (lockpid != -1) {
366		waitpid(lockpid, NULL, 0);
367		lockpid = -1;
368	} else {
369		rellock();
370	}
371	close(lockfd);
372	lockfd = -1;
373}
374
375void
376usage(void)
377{
378	merr(EX_USAGE, "usage: mail.local [-Ll] [-f from] user ...");
379}
380