]> diplodocus.org Git - mdeliver/blob - mdeliver.c
5d4b900a254fc67727d2c63a44c863d629d37a2a
[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/", HOST_NAME_MAX for the hostname, and two
48 * LEN_ULONGs for time and pid. */
49 #define TMPNAMLEN 1 + 4 + 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 rcsid[] = "$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 tm *t;
97 char *buildfn; /* used to build up the fn */
98 struct stat statbuf;
99
100 pid = getpid();
101
102 if (gethostname(host, sizeof(host)) != 0) {
103 err(MDELIVER_EXITCODE, "Failed gethostname");
104 }
105
106 for (try = 0; ; try++) {
107 now = time(NULL);
108 if (now == -1) {
109 err(MDELIVER_EXITCODE, "Failed time()");
110 }
111
112 t = gmtime(&now);
113 if (!t) {
114 err(MDELIVER_EXITCODE, "Failed gmtime()");
115 }
116
117 buildfn = fn + sprintf(fn, "tmp/");
118 buildfn += strftime(buildfn, LEN_ULONG + 1, "%s.", t);
119 buildfn += snprintf(buildfn, LEN_ULONG + 1, "%d.", pid);
120 buildfn += sprintf(buildfn, "%s", host);
121
122 if (stat(fn, &statbuf) != 0) {
123 if (errno == ENOENT) {
124 /* Every failure point in this program relies on errno
125 * describing what went wrong. This is the one
126 * failure we *want*, so it is important to restore
127 * errno to 0 (success) before returning.
128 */
129 errno = 0;
130 break;
131 }
132 }
133
134 if (try == 2) {
135 errx(MDELIVER_EXITCODE, "Attempted filename exists.");
136 }
137
138 sleep(2);
139 }
140 }
141
142 /* Strip the non-RFC2822-compliant 'From ' line some busted-ass
143 * mailers (such as Postfix) add as the first line of input. This bit
144 * of historical gunk originated with Berkeley mbox files, which use
145 * this line to indicate the beginning of a message. Postfix should
146 * only add such a line when it is acting as an MDA and delivering to
147 * a Berkeley mbox; the data written to an external MDA should be the
148 * raw RFC2822 message. >sigh<
149 */
150 /* buf is passed only as an (early, evil) space optimization). */
151 static void
152 strip_from(int fd, char *buf, char *fn)
153 {
154 ssize_t r;
155
156 r = read(STDIN_FILENO, buf, 5);
157 if (r != 5) {
158 save_errno( unlink(fn) );
159 err(MDELIVER_EXITCODE, "Failed read from stdin");
160 }
161
162 if (strncmp(buf, "From ", 5) == 0) {
163 while ((r = read(STDIN_FILENO, buf, 1)) > 0
164 || (r == -1 && errno == EINTR)) {
165 if (buf[0] == '\n') {
166 break;
167 }
168 }
169
170 if (r == -1) {
171 save_errno( unlink(fn) );
172 err(MDELIVER_EXITCODE, "Failed read from stdin");
173 }
174 } else {
175 if (full_write(fd, buf, r) != r) {
176 save_errno( unlink(fn) );
177 err(MDELIVER_EXITCODE, "Failed write(%s)", fn);
178 }
179 }
180 }
181
182 /* Copy the message from stdin to fd. */
183 /* buf is passed only as an (early, evil) space optimization). */
184 static void
185 copy_message(int fd, char *buf, char *fn)
186 {
187 ssize_t r;
188
189 while ((r = read(STDIN_FILENO, buf, BUFSIZ)) > 0
190 || (r == -1 && errno == EINTR)) {
191 if (full_write(fd, buf, r) != r) {
192 save_errno( unlink(fn) );
193 err(MDELIVER_EXITCODE, "Failed write(%s)", fn);
194 }
195 }
196
197 if (r == -1) {
198 save_errno( unlink(fn) );
199 err(MDELIVER_EXITCODE, "Failed read from stdin");
200 }
201
202 if (fsync(fd) < 0) {
203 save_errno( unlink(fn) );
204 err(MDELIVER_EXITCODE, "Failed fsync(%s)", fn);
205 }
206
207 if (close(fd) < 0) {
208 save_errno( unlink(fn) );
209 err(MDELIVER_EXITCODE, "Failed close(%s)", fn);
210 }
211 }
212
213 static void
214 deliver(char *maildir, char *newfn)
215 {
216 char fn[TMPNAMLEN];
217 int fd;
218 char buf[BUFSIZ]; /* Allocated here only as an (early,
219 * evil) space optimization. */
220
221 if (chdir(maildir) != 0) {
222 err(MDELIVER_EXITCODE, "Failed chdir(%s)", maildir);
223 }
224
225 get_filename(fn);
226
227 fd = open(fn, O_WRONLY | O_EXCL | O_CREAT, 0600);
228 if (fd < 0) {
229 err(MDELIVER_EXITCODE, "Failed open(%s)", fn);
230 }
231
232 /* From this point, we have a tmp file, so if someting fails we
233 * must try to unlink it before bombing. */
234
235 strip_from(fd, buf, fn);
236 copy_message(fd, buf, fn);
237
238 strcpy(newfn, fn);
239 memcpy(newfn, "new", 3);
240
241 if (rename(fn, newfn) != 0) {
242 save_errno( unlink(fn) );
243 err(MDELIVER_EXITCODE, "Failed rename(%s, new)", fn);
244 }
245 }
246
247 int
248 main(int argc, char *argv[])
249 {
250 char *home;
251 char newfn[TMPNAMLEN];
252 pid_t pid;
253 FILE *errfile;
254 char *fail;
255 int fd;
256
257 if (argv[1] && argv[1][0] == '-' && argv[1][1] == 'v') {
258 puts(rcsid);
259 return 0;
260 }
261
262 if (!(home = getenv("HOME"))) {
263 err(MDELIVER_EXITCODE, "Failed getenv(HOME)");
264 }
265
266 if (chdir(home) != 0) {
267 err(MDELIVER_EXITCODE, "Failed chdir(HOME)");
268 }
269
270 deliver("Maildir", newfn);
271
272 if (argv[1]) {
273 if ((pid = fork()) > 0) {
274 return 0;
275 } else if (pid == 0) {
276 argv[0] = argv[1];
277 argv[1] = newfn;
278 execv(argv[0], argv);
279 fail = "exec";
280 } else {
281 fail = "fork";
282 }
283
284 /* If we got here fork or exec failed, so try to write an
285 * error message where later mail processors can find it. We
286 * really can't do anything about it if this fails. */
287 memcpy(newfn, "err", 3);
288 mkdir("err", 0700);
289 fd = open(newfn, O_WRONLY | O_EXCL | O_CREAT, 0644);
290 errfile = fdopen(fd, "w");
291 if (errfile) {
292 fprintf(errfile, "%s:%d\n", fail, errno);
293 }
294 }
295
296 /* Always return 0 because deliver exits if it fails, and if it
297 * succeeds we cannot return a failure code to the MTA. */
298 return 0;
299 }