]> diplodocus.org Git - mdeliver/blob - mdeliver.c
d6e49e7b80c7e51f0750a411ba5d2538ce7723ad
[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 * This program does not conform strictly to POSIX. However, the two
14 * nonstandard interfaces it relies upon (<err.h> and <sysexits.h>)
15 * are widespread and easily enough replaced if necessary.
16 */
17
18 #include <sys/types.h>
19 #include <sys/stat.h>
20
21 #include <err.h>
22 #include <errno.h>
23 #include <fcntl.h>
24 #include <limits.h>
25 #include <stdio.h>
26 #include <stdlib.h>
27 #include <string.h>
28 #include <time.h>
29 #include <unistd.h>
30
31 #ifndef MDELIVER_EXITCODE
32 #include <sysexits.h>
33 #define MDELIVER_EXITCODE EX_TEMPFAIL
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 /* 1 for '\0', 4 for "tmp/", 2 for the two dots ('.'), HOST_NAME_MAX
48 * for the hostname, and two LEN_ULONGs for time and pid. */
49 #define TMPNAMLEN 1 + 4 + 2 + HOST_NAME_MAX + LEN_ULONG + LEN_ULONG
50
51 /* Use this macro to perform some operation and saving the value errno
52 * had before that operation.
53 */
54 #define save_errno(op) { \
55 int olderrno = errno; \
56 op; \
57 errno = olderrno; \
58 }
59
60 static char ident[] = "$Id$";
61
62 /* Write all of buf, even if write(2) is interrupted. */
63 static ssize_t
64 full_write(int fd, const char *buf, size_t nbytes)
65 {
66 ssize_t r, w = 0;
67
68 /* Loop until nbytes of buf have been written. */
69 while (w < nbytes) {
70 /* Keep trying until write succeeds without interruption. */
71 do {
72 r = write(fd, buf + w, nbytes - w);
73 } while (r < 0 && errno == EINTR);
74
75 if (r < 0) {
76 return r;
77 }
78
79 w += r;
80 }
81
82 return w;
83 }
84
85 /* Find a filename for the message according to the algorithm
86 * documented in the maildir(5) manual page from qmail
87 * (http://www.qmail.org/qmail-manual-html/man5/maildir.html).
88 */
89 static void
90 get_filename(char *fn)
91 {
92 pid_t pid;
93 char host[HOST_NAME_MAX + 1];
94 int try;
95 time_t now;
96 struct stat statbuf;
97
98 pid = getpid();
99
100 if (gethostname(host, sizeof(host)) != 0) {
101 err(MDELIVER_EXITCODE, "Failed gethostname");
102 }
103
104 for (try = 0; ; try++) {
105 now = time(NULL);
106 if (now == -1) {
107 err(MDELIVER_EXITCODE, "Failed time()");
108 }
109
110 sprintf(fn, "tmp/%lu.%lu.%s",
111 (long unsigned)now, (long unsigned)pid, host);
112
113 if (stat(fn, &statbuf) != 0) {
114 if (errno == ENOENT) {
115 /* Every failure point in this program relies on errno
116 * describing what went wrong. This is the one
117 * failure we *want*, so it is important to restore
118 * errno to 0 (success) before returning.
119 */
120 errno = 0;
121 break;
122 }
123 }
124
125 if (try == 2) {
126 errx(MDELIVER_EXITCODE, "Attempted filename exists.");
127 }
128
129 sleep(2);
130 }
131 }
132
133 /* Strip the non-RFC2822-compliant 'From ' line some busted-ass
134 * mailers (such as Postfix) add as the first line of input. This bit
135 * of historical gunk originated with Berkeley mbox files, which use
136 * this line to indicate the beginning of a message. Postfix should
137 * only add such a line when it is acting as an MDA and delivering to
138 * a Berkeley mbox; the data written to an external MDA should be the
139 * raw RFC2822 message. >sigh<
140 */
141 /* buf is passed only as an (early, evil) space optimization). */
142 static void
143 strip_from(int fd, char *buf, char *fn)
144 {
145 ssize_t r;
146
147 /* Yes, read(2) can be interrupted and read fewer bytes than
148 * requested. However, i'm only asking for five bytes here. I
149 * have tested this program extensively and it handles large
150 * volumes of mail correctly every day on my systems. This holds
151 * up my original opinion that expecting to be able to get five
152 * bytes safely is not an unreasonable demand. If i can't get
153 * five measly bytes, something is probably wrong, so bomb.
154 */
155 r = read(STDIN_FILENO, buf, 5);
156 if (r != 5) {
157 save_errno( unlink(fn) );
158 err(MDELIVER_EXITCODE, "Failed read from stdin");
159 }
160
161 if (strncmp(buf, "From ", 5) == 0) {
162 while ((r = read(STDIN_FILENO, buf, 1)) > 0
163 || (r == -1 && errno == EINTR)) {
164 if (buf[0] == '\n') {
165 break;
166 }
167 }
168
169 if (r == -1) {
170 save_errno( unlink(fn) );
171 err(MDELIVER_EXITCODE, "Failed read from stdin");
172 }
173 } else {
174 if (full_write(fd, buf, r) != r) {
175 save_errno( unlink(fn) );
176 err(MDELIVER_EXITCODE, "Failed write(%s)", fn);
177 }
178 }
179 }
180
181 /* Copy the file from INFD to OUTFD in blocks of BUFSIZE bytes (stored
182 * in BUF). BUF must point to a buffer of at least BUFSIZE bytes;
183 * we're not dealing with null-terminated strings here.
184 *
185 * Note that OUTFD is closed before returning; this is necessary to
186 * ensure the file was fully written on some filesystems, such as NFS.
187 */
188 static void
189 copy_file(int infd, int outfd, char *buf, size_t bufsize, char *fn)
190 {
191 ssize_t r;
192
193 while ((r = read(infd, buf, bufsize)) > 0
194 || (r == -1 && errno == EINTR)) {
195 if (full_write(outfd, buf, r) != r) {
196 save_errno( unlink(fn) );
197 err(MDELIVER_EXITCODE, "Failed write(%s)", fn);
198 }
199 }
200
201 if (r == -1) {
202 save_errno( unlink(fn) );
203 err(MDELIVER_EXITCODE, "Failed read");
204 }
205
206 if (fsync(outfd) < 0) {
207 save_errno( unlink(fn) );
208 err(MDELIVER_EXITCODE, "Failed fsync(%s)", fn);
209 }
210
211 if (close(outfd) < 0) {
212 save_errno( unlink(fn) );
213 err(MDELIVER_EXITCODE, "Failed close(%s)", fn);
214 }
215 }
216
217 static void
218 deliver(char *maildir, char *newfn)
219 {
220 char fn[TMPNAMLEN];
221 int fd, tmpfd;
222 char buf[BUFSIZ]; /* Allocated here only as an (early,
223 * evil) space optimization. */
224
225 if (chdir(maildir) != 0) {
226 err(MDELIVER_EXITCODE, "Failed chdir(%s)", maildir);
227 }
228
229 get_filename(fn);
230
231 fd = open(fn, O_WRONLY | O_EXCL | O_CREAT, 0600);
232 if (fd < 0) {
233 err(MDELIVER_EXITCODE, "Failed open(%s)", fn);
234 }
235
236 /* From this point, we have a tmp file, so if someting fails we
237 * must try to unlink it before bombing. */
238
239 strip_from(fd, buf, fn);
240 copy_file(STDIN_FILENO, fd, buf, BUFSIZ, fn);
241
242 strcpy(newfn, fn);
243 memcpy(newfn, "new", 3);
244
245 tmpfd = open(newfn, O_WRONLY | O_EXCL | O_CREAT, 0600);
246 if (tmpfd < 0) {
247 /* XXX: what error message to use? */
248 err(MDELIVER_EXITCODE, "Failed open(%s)", newfn);
249 }
250 close(tmpfd);
251
252 if (rename(fn, newfn) != 0) {
253 save_errno( unlink(newfn) );
254 save_errno( unlink(fn) );
255 err(MDELIVER_EXITCODE, "Failed rename(%s, new)", fn);
256 }
257 }
258
259 /* Run a program to process the new message. */
260 static void
261 run_processor(char *home, char *newfn)
262 {
263 pid_t pid;
264 char command[PATH_MAX];
265 FILE *errfile;
266 char *fail;
267 int olderrno;
268 int fd;
269
270 if ((pid = fork()) > 0) {
271 return;
272 } else if (pid == 0) {
273 snprintf(command, sizeof(command), "%s/%s",
274 home, ".mdeliver-processor");
275 execl(command, command, newfn, NULL);
276 if (errno == ENOENT) {
277 /* Not having a processor is not an error. */
278 _exit(0);
279 }
280 fail = "exec";
281 } else {
282 fail = "fork";
283 }
284 olderrno = errno;
285
286 /* If we got here fork or exec failed, so try to write an error
287 * message where later mail processors can find it. We really
288 * can't do anything about it if this fails. */
289 memcpy(newfn, "err", 3);
290 mkdir("err", 0700);
291 fd = open(newfn, O_WRONLY | O_EXCL | O_CREAT, 0644);
292 if (fd >= 0) {
293 errfile = fdopen(fd, "w");
294 if (errfile) {
295 fprintf(errfile, "%s:%d\n", fail, olderrno);
296 fflush(errfile);
297 }
298 fsync(fd);
299 close(fd);
300 }
301
302 if (fail[0] == 'e') {
303 _exit(0);
304 }
305 }
306
307 int
308 main(int argc, char *argv[])
309 {
310 char *home;
311 char newfn[TMPNAMLEN];
312
313 if (argv[1] && argv[1][0] == '-' && argv[1][1] == 'v') {
314 puts(ident);
315 return 0;
316 }
317
318 if (!(home = getenv("HOME"))) {
319 err(MDELIVER_EXITCODE, "Failed getenv(HOME)");
320 }
321
322 if (chdir(home) != 0) {
323 err(MDELIVER_EXITCODE, "Failed chdir(HOME)");
324 }
325
326 deliver("Maildir", newfn);
327
328 run_processor(home, newfn);
329
330 /* Always return 0 because deliver exits if it fails, and if it
331 * succeeds we cannot return a failure code to the MTA. */
332 return 0;
333 }