]> diplodocus.org Git - mdeliver/blob - mdeliver.c
44e7e1641c5f0631b0af9fe47ab53dac17baa2e3
[mdeliver] / mdeliver.c
1 /* mdeliver.c -- Deliver a message to a Maildir.
2 *
3 * $Id$
4 *
5 * This program is in the public domain.
6 *
7 * mdeliver is a simple, robust MDA. It delivers a single message
8 * from stdin to the user's Maildir and executes (in the background) a
9 * single program with the message filename as the first argument.
10 * Additional arguments may be specified; see main() below for
11 * details.
12 */
13
14 #include <sys/types.h>
15 #include <sys/stat.h>
16
17 #include <errno.h>
18 #include <fcntl.h>
19 #include <limits.h>
20 #include <stdarg.h>
21 #include <stdbool.h>
22 #include <stdio.h>
23 #include <stdlib.h>
24 #include <string.h>
25 #include <time.h>
26 #include <unistd.h>
27
28 #ifndef EX_TEMPFAIL
29 #ifdef QMAIL
30 #define EX_TEMPFAIL 111
31 #else
32 #include <sysexits.h>
33 #endif
34 #endif
35
36 #if !defined(HOST_NAME_MAX)
37 #if defined MAXHOSTNAMELEN
38 #define HOST_NAME_MAX MAXHOSTNAMELEN
39 #else
40 #define HOST_NAME_MAX 64
41 #endif
42 #endif
43
44 /* holds 2^128 - 1 in decimal, plus '\0' */
45 #define LEN_ULONG 40
46
47 /* 4 for "tmp/", 2 for the two dots ('.'), HOST_NAME_MAX for the
48 * hostname, two LEN_ULONGs for time and pid, and 1 for '\0', . */
49 #define LEN_FILENAME 4 + 2 + HOST_NAME_MAX + LEN_ULONG + LEN_ULONG + 1
50
51 #define save_errno(op) { \
52 int olderrno = errno; \
53 op; \
54 errno = olderrno; \
55 }
56 #define try_unlink(fn) save_errno( unlink(fn) )
57
58 static char ident[] = "$Id$";
59
60 #define err(...) _err(true, __LINE__, __VA_ARGS__)
61 #define errx(...) _err(false, __LINE__, __VA_ARGS__)
62
63 static void
64 _err(bool want_strerror, int lineno, char *format, ...)
65 {
66 va_list ap;
67 int olderrno = errno;
68
69 fprintf(stderr, "mdeliver:%d:", lineno);
70
71 va_start(ap, format);
72 vfprintf(stderr, format, ap);
73 va_end(ap);
74
75 if (want_strerror) {
76 fputs(": ", stderr);
77 fputs(strerror(olderrno), stderr);
78 }
79 fputs("\n", stderr);
80
81 exit(EX_TEMPFAIL);
82 }
83
84 static void
85 full_sleep(unsigned seconds)
86 {
87 while (seconds > 0) {
88 seconds = sleep(seconds);
89 }
90 }
91
92 /* Write all of buf, even if write(2) is interrupted. */
93 static ssize_t
94 full_write(int fd, const char *buf, size_t nbytes)
95 {
96 ssize_t r, w = 0;
97
98 /* Loop until nbytes of buf have been written. */
99 while (w < nbytes) {
100 /* Keep trying until write succeeds without interruption. */
101 do {
102 r = write(fd, buf + w, nbytes - w);
103 } while (r < 0 && errno == EINTR);
104
105 if (r < 0) {
106 return r;
107 }
108
109 w += r;
110 }
111
112 return w;
113 }
114
115 /* Find a filename for the message according to the algorithm
116 * documented in the maildir(5) manual page from qmail
117 * (http://www.qmail.org/qmail-manual-html/man5/maildir.html).
118 */
119 static void
120 get_filename(char *fn)
121 {
122 pid_t pid;
123 char host[HOST_NAME_MAX + 1];
124 int try;
125 time_t now;
126 struct stat st;
127
128 pid = getpid();
129
130 if (gethostname(host, sizeof(host)) != 0) {
131 err("gethostname");
132 }
133 /* http://www.opengroup.org/onlinepubs/009695399/functions/gethostname.html
134 * "it is unspecified whether the returned name is null-terminated" */
135 host[sizeof(host) - 1] = '\0';
136
137 for (try = 0; ; try++) {
138 now = time(NULL);
139 if (now == -1) {
140 err("time");
141 }
142
143 sprintf(fn, "tmp/%lu.%lu.%s",
144 (long unsigned)now, (long unsigned)pid, host);
145
146 if (stat(fn, &st) != 0) {
147 if (errno == ENOENT) {
148 /* Every failure point in this program relies on errno
149 * describing what went wrong. This is the one
150 * failure we *want*, so it is important to restore
151 * errno to 0 (success) before returning.
152 */
153 errno = 0;
154 break;
155 }
156 }
157
158 if (try == 2) {
159 errx("Attempted filename exists.");
160 }
161
162 full_sleep(2);
163 }
164 }
165
166 #ifndef QMAIL
167 /* Strip the non-RFC2822-compliant 'From ' line some busted-ass
168 * mailers (such as Postfix) add as the first line of input. This bit
169 * of historical gunk originated with Berkeley mbox files, which use
170 * this line to indicate the beginning of a message. Postfix should
171 * only add such a line when it is acting as an MDA and delivering to
172 * a Berkeley mbox; the data written to an external MDA should be the
173 * raw RFC2822 message. >sigh<
174 */
175 /* buf is passed only as an (early, evil) space optimization). */
176 static void
177 strip_from(int fd, char *buf, char *fn)
178 {
179 ssize_t r;
180
181 /* Yes, read(2) can be interrupted and read fewer bytes than
182 * requested. However, i'm only asking for five bytes here. I
183 * have tested this program extensively and it handles large
184 * volumes of mail correctly every day on my systems. This holds
185 * up my original opinion that expecting to be able to get five
186 * bytes safely is not an unreasonable demand. If i can't get
187 * five measly bytes, something is probably wrong, so bomb.
188 */
189 r = read(0, buf, 5);
190 if (r != 5) {
191 try_unlink(fn);
192 err("read(0, 5)");
193 }
194
195 if (strncmp(buf, "From ", 5) == 0) {
196 while ((r = read(0, buf, 1)) > 0
197 || (r == -1 && errno == EINTR)) {
198 if (buf[0] == '\n') {
199 break;
200 }
201 }
202
203 if (r == -1) {
204 try_unlink(fn);
205 err("read(0, 1)");
206 }
207 } else {
208 if (full_write(fd, buf, r) != r) {
209 try_unlink(fn);
210 err("write(%s)", fn);
211 }
212 }
213 }
214 #endif
215
216 /* Copy the file from INFD to OUTFD in blocks of BUFSIZE bytes (stored
217 * in BUF). BUF must point to a buffer of at least BUFSIZE bytes;
218 * we're not dealing with null-terminated strings here.
219 *
220 * Note that OUTFD is closed before returning; this is necessary to
221 * ensure the file was fully written on some filesystems, such as NFS.
222 */
223 static void
224 copy_file(int infd, int outfd, char *buf, size_t bufsize, char *fn)
225 {
226 ssize_t r;
227
228 while ((r = read(infd, buf, bufsize)) > 0
229 || (r == -1 && errno == EINTR)) {
230 if (full_write(outfd, buf, r) != r) {
231 try_unlink(fn);
232 err("write(%s)", fn);
233 }
234 }
235
236 if (r == -1) {
237 try_unlink(fn);
238 err("read(%d, %d)", infd, bufsize);
239 }
240
241 if (fsync(outfd) < 0) {
242 try_unlink(fn);
243 err("fsync(%s)", fn);
244 }
245
246 if (close(outfd) < 0) {
247 try_unlink(fn);
248 err("close(%s)", fn);
249 }
250 }
251
252 static void
253 deliver(char *maildir, char *sub, char *newfn)
254 {
255 char fn[LEN_FILENAME];
256 int fd;
257 char buf[BUFSIZ]; /* Allocated here only as an (early,
258 * evil) space optimization. */
259
260 if (chdir(maildir) != 0) {
261 err("chdir(%s)", maildir);
262 }
263
264 get_filename(fn);
265
266 fd = open(fn, O_WRONLY | O_EXCL | O_CREAT, 0600);
267 if (fd < 0) {
268 err("open(%s)", fn);
269 }
270
271 /* From this point, we have a tmp file, so if someting fails we
272 * must try to unlink it before bombing. */
273
274 #ifdef QMAIL
275 {
276 char *buf;
277 size_t len;
278
279 buf = getenv("RPLINE");
280 len = strlen(buf);
281 if (full_write(fd, buf, len) != len) {
282 try_unlink(fn);
283 err("write(%s)", fn);
284 }
285 buf = getenv("DTLINE");
286 len = strlen(buf);
287 if (full_write(fd, buf, len) != len) {
288 try_unlink(fn);
289 err("write(%s)", fn);
290 }
291 }
292 #else
293 strip_from(fd, buf, fn);
294 #endif
295
296 copy_file(0, fd, buf, BUFSIZ, fn);
297
298 strcpy(newfn, fn);
299 memcpy(newfn, sub, 3);
300
301 if (link(fn, newfn) != 0) {
302 try_unlink(fn);
303 err("link(%s, %s)", fn, newfn);
304 }
305 try_unlink(fn);
306 }
307
308 /* Run a program to process the new message. */
309 static void
310 run_processor(char *home, char *newfn)
311 {
312 pid_t pid;
313 char command[PATH_MAX];
314 FILE *errfile;
315 char *fail;
316 int olderrno;
317 int fd;
318
319 if ((pid = fork()) > 0) {
320 return;
321 } else if (pid == 0) {
322 snprintf(command, sizeof(command), "%s/%s",
323 home, ".mdeliver-processor");
324 execl(command, command, newfn, NULL);
325 if (errno == ENOENT) {
326 /* Not having a processor is not an error. */
327 _exit(0);
328 }
329 fail = "exec";
330 } else {
331 fail = "fork";
332 }
333 olderrno = errno;
334
335 /* If we got here fork or exec failed, so try to write an error
336 * message where later mail processors can find it. We really
337 * can't do anything about it if this fails. */
338 memcpy(newfn, "err", 3);
339 mkdir("err", 0700);
340 fd = open(newfn, O_WRONLY | O_EXCL | O_CREAT, 0644);
341 if (fd >= 0) {
342 errfile = fdopen(fd, "w");
343 if (errfile) {
344 fprintf(errfile, "mdeliver:mdeliver.c:%d:%s: %s\n",
345 __LINE__, fail, strerror(olderrno));
346 fflush(errfile);
347 }
348 fsync(fd);
349 close(fd);
350 }
351
352 if (fail[0] == 'e') {
353 _exit(0);
354 }
355 }
356
357 int
358 main(int argc, char *argv[])
359 {
360 char *home;
361 char newfn[LEN_FILENAME];
362 struct stat st;
363
364 if (argv[1] && argv[1][0] == '-' && argv[1][1] == 'v') {
365 puts(ident);
366 return 0;
367 }
368
369 if ((home = getenv("HOME")) == NULL) {
370 errx("HOME is not set.");
371 }
372
373 if (chdir(home) != 0) {
374 err("chdir(%s)", home);
375 }
376
377 if (stat(".mdeliver-processor", &st) == 0) {
378 deliver("Maildir", "mdp", newfn);
379 run_processor(home, newfn);
380 } else {
381 deliver("Maildir", "new", newfn);
382 }
383
384 return 0;
385 }