xref: /openbsd-src/usr.sbin/smtpd/mail.maildir.c (revision e7954c7dbaa8774f303d243ee49f779cd469fdbd)
1 /*
2  * Copyright (c) 2017 Gilles Chehade <gilles@poolp.org>
3  *
4  * Permission to use, copy, modify, and distribute this software for any
5  * purpose with or without fee is hereby granted, provided that the above
6  * copyright notice and this permission notice appear in all copies.
7  *
8  * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
9  * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
10  * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
11  * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
12  * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
13  * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
14  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
15  */
16 
17 #ifndef nitems
18 #define nitems(_a) (sizeof((_a)) / sizeof((_a)[0]))
19 #endif
20 
21 #include <sys/stat.h>
22 
23 #include <err.h>
24 #include <errno.h>
25 #include <fcntl.h>
26 #include <limits.h>
27 #include <netdb.h>
28 #include <stdio.h>
29 #include <stdlib.h>
30 #include <string.h>
31 #include <sysexits.h>
32 #include <time.h>
33 #include <unistd.h>
34 
35 #define	MAILADDR_ESCAPE		"!#$%&'*/?^`{|}~"
36 
37 static int	maildir_subdir(const char *, char *, size_t);
38 static void	maildir_mkdirs(const char *);
39 static void	maildir_engine(const char *, int);
40 static int	mkdirs_component(const char *, mode_t);
41 static int	mkdirs(const char *, mode_t);
42 
43 int
main(int argc,char * argv[])44 main(int argc, char *argv[])
45 {
46 	int	ch;
47 	int	junk = 0;
48 
49 	if (! geteuid())
50 		errx(1, "mail.maildir: may not be executed as root");
51 
52 	while ((ch = getopt(argc, argv, "j")) != -1) {
53 		switch (ch) {
54 		case 'j':
55 			junk = 1;
56 			break;
57 		default:
58 			break;
59 		}
60 	}
61 	argc -= optind;
62 	argv += optind;
63 
64 	if (argc > 1)
65 		errx(1, "mail.maildir: only one maildir is allowed");
66 
67 	maildir_engine(argv[0], junk);
68 
69 	return (0);
70 }
71 
72 static int
maildir_subdir(const char * extension,char * dest,size_t len)73 maildir_subdir(const char *extension, char *dest, size_t len)
74 {
75 	char		*sanitized;
76 
77 	if (strlcpy(dest, extension, len) >= len)
78 		return 0;
79 
80 	for (sanitized = dest; *sanitized; sanitized++)
81 		if (strchr(MAILADDR_ESCAPE, *sanitized))
82 			*sanitized = ':';
83 
84 	return 1;
85 }
86 
87 static void
maildir_mkdirs(const char * dirname)88 maildir_mkdirs(const char *dirname)
89 {
90 	uint	i;
91 	int	ret;
92 	char	pathname[PATH_MAX];
93 	char	*subdirs[] = { "cur", "tmp", "new" };
94 
95 	if (mkdirs(dirname, 0700) == -1 && errno != EEXIST) {
96 		if (errno == EINVAL || errno == ENAMETOOLONG)
97 			err(1, NULL);
98 		err(EX_TEMPFAIL, NULL);
99 	}
100 
101 	for (i = 0; i < nitems(subdirs); ++i) {
102 		ret = snprintf(pathname, sizeof pathname, "%s/%s", dirname,
103 		    subdirs[i]);
104 		if (ret < 0 || (size_t)ret >= sizeof pathname)
105 			errc(1, ENAMETOOLONG, "%s/%s", dirname, subdirs[i]);
106 		if (mkdir(pathname, 0700) == -1 && errno != EEXIST)
107 			err(EX_TEMPFAIL, NULL);
108 	}
109 }
110 
111 static void
maildir_engine(const char * dirname,int junk)112 maildir_engine(const char *dirname, int junk)
113 {
114 	char	rootpath[PATH_MAX];
115 	char	junkpath[PATH_MAX];
116 	char	extpath[PATH_MAX];
117 	char	subdir[PATH_MAX];
118 	char	filename[PATH_MAX];
119 	char	hostname[HOST_NAME_MAX+1];
120 
121 	char	tmp[PATH_MAX];
122 	char	new[PATH_MAX];
123 
124 	int	ret;
125 
126 	int	fd;
127 	FILE    *fp;
128 	char	*line = NULL;
129 	size_t	linesize = 0;
130 	struct stat	sb;
131 	char	*home;
132 	char	*extension;
133 
134 	int	is_junk = 0;
135 	int	in_hdr  = 1;
136 
137 	if (dirname == NULL) {
138 		if ((home = getenv("HOME")) == NULL)
139 			err(1, NULL);
140 		ret = snprintf(rootpath, sizeof rootpath, "%s/Maildir", home);
141 		if (ret < 0 || (size_t)ret >= sizeof rootpath)
142 			errc(1, ENAMETOOLONG, "%s/Maildir", home);
143 		dirname = rootpath;
144 	}
145 	maildir_mkdirs(dirname);
146 
147 	if (junk) {
148 		/* create Junk subdirectory */
149 		ret = snprintf(junkpath, sizeof junkpath, "%s/.Junk", dirname);
150 		if (ret < 0 || (size_t)ret >= sizeof junkpath)
151 			errc(1, ENAMETOOLONG, "%s/.Junk", dirname);
152 		maildir_mkdirs(junkpath);
153 	}
154 
155 	if ((extension = getenv("EXTENSION")) != NULL) {
156 		if (maildir_subdir(extension, subdir, sizeof(subdir)) &&
157 		    subdir[0]) {
158 			ret = snprintf(extpath, sizeof extpath, "%s/.%s",
159 			    dirname, subdir);
160 			if (ret < 0 || (size_t)ret >= sizeof extpath)
161 				errc(1, ENAMETOOLONG, "%s/.%s",
162 				    dirname, subdir);
163 			if (stat(extpath, &sb) != -1) {
164 				dirname = extpath;
165 				maildir_mkdirs(dirname);
166 			}
167 		}
168 	}
169 
170 	if (gethostname(hostname, sizeof hostname) != 0)
171 		(void)strlcpy(hostname, "localhost", sizeof hostname);
172 
173 	(void)snprintf(filename, sizeof filename, "%lld.%08x.%s",
174 	    (long long)time(NULL),
175 	    arc4random(),
176 	    hostname);
177 
178 	(void)snprintf(tmp, sizeof tmp, "%s/tmp/%s", dirname, filename);
179 
180 	fd = open(tmp, O_CREAT | O_EXCL | O_WRONLY, 0600);
181 	if (fd == -1)
182 		err(EX_TEMPFAIL, NULL);
183 	if ((fp = fdopen(fd, "w")) == NULL)
184 		err(EX_TEMPFAIL, NULL);
185 
186 	while (getline(&line, &linesize, stdin) != -1) {
187 		line[strcspn(line, "\n")] = '\0';
188 		if (line[0] == '\0')
189 			in_hdr = 0;
190 		if (junk && in_hdr &&
191 		    (strcasecmp(line, "x-spam: yes") == 0 ||
192 			strcasecmp(line, "x-spam-flag: yes") == 0))
193 			is_junk = 1;
194 		fprintf(fp, "%s\n", line);
195 	}
196 	free(line);
197 	if (ferror(stdin))
198 		err(EX_TEMPFAIL, NULL);
199 
200 	if (fflush(fp) == EOF ||
201 	    ferror(fp) ||
202 	    fsync(fd) == -1 ||
203 	    fclose(fp) == EOF)
204 		err(EX_TEMPFAIL, NULL);
205 
206 	(void)snprintf(new, sizeof new, "%s/new/%s",
207 	    is_junk ? junkpath : dirname, filename);
208 
209 	if (rename(tmp, new) == -1)
210 		err(EX_TEMPFAIL, NULL);
211 
212 	exit(0);
213 }
214 
215 
216 static int
mkdirs_component(const char * path,mode_t mode)217 mkdirs_component(const char *path, mode_t mode)
218 {
219 	struct stat	sb;
220 
221 	if (stat(path, &sb) == -1) {
222 		if (errno != ENOENT)
223 			return 0;
224 		if (mkdir(path, mode | S_IWUSR | S_IXUSR) == -1)
225 			return 0;
226 	}
227 	else if (!S_ISDIR(sb.st_mode)) {
228 		errno = ENOTDIR;
229 		return 0;
230 	}
231 
232 	return 1;
233 }
234 
235 static int
mkdirs(const char * path,mode_t mode)236 mkdirs(const char *path, mode_t mode)
237 {
238 	char	 buf[PATH_MAX];
239 	int	 i = 0;
240 	int	 done = 0;
241 	const char	*p;
242 
243 	/* absolute path required */
244 	if (*path != '/') {
245 		errno = EINVAL;
246 		return 0;
247 	}
248 
249 	/* make sure we don't exceed PATH_MAX */
250 	if (strlen(path) >= sizeof buf) {
251 		errno = ENAMETOOLONG;
252 		return 0;
253 	}
254 
255 	memset(buf, 0, sizeof buf);
256 	for (p = path; *p; p++) {
257 		if (*p == '/') {
258 			if (buf[0] != '\0')
259 				if (!mkdirs_component(buf, mode))
260 					return 0;
261 			while (*p == '/')
262 				p++;
263 			buf[i++] = '/';
264 			buf[i++] = *p;
265 			if (*p == '\0' && ++done)
266 				break;
267 			continue;
268 		}
269 		buf[i++] = *p;
270 	}
271 	if (!done)
272 		if (!mkdirs_component(buf, mode))
273 			return 0;
274 
275 	if (chmod(path, mode) == -1)
276 		return 0;
277 
278 	return 1;
279 }
280