]> diplodocus.org Git - mdeliver/blob - mdeliver.c
0073c477c34538d24bc8c914cb80e8bcd781350c
[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 static char rcsid[] = "$Id$";
52
53 /* Write all of buf, even if write(2) is interrupted. */
54 static ssize_t
55 full_write(int fd, const char *buf, size_t nbytes)
56 {
57 ssize_t r, w = 0;
58
59 /* Loop until nbytes of buf have been written. */
60 while (w < nbytes) {
61 /* Keep trying until write succeeds without interruption. */
62 do {
63 r = write(fd, buf + w, nbytes - w);
64 } while (r < 0 && errno == EINTR);
65
66 if (r < 0) {
67 return r;
68 }
69
70 w += r;
71 }
72
73 return w;
74 }
75
76 /* Find a filename for the message according to the algorithm
77 * documented in the maildir(5) manual page from qmail
78 * (http://www.qmail.org/qmail-manual-html/man5/maildir.html).
79 */
80 static void
81 get_filename(char *fn)
82 {
83 pid_t pid;
84 char host[HOST_NAME_MAX + 1];
85 int try;
86 time_t now;
87 struct tm *t;
88 char *buildfn; /* used to build up the fn */
89 struct stat statbuf;
90
91 pid = getpid();
92
93 if (gethostname(host, sizeof(host)) != 0) {
94 err(MDELIVER_EXITCODE, "Failed gethostname");
95 }
96
97 for (try = 0; ; try++) {
98 now = time(NULL);
99 if (now == -1) {
100 err(MDELIVER_EXITCODE, "Failed time()");
101 }
102
103 t = gmtime(&now);
104 if (!t) {
105 err(MDELIVER_EXITCODE, "Failed gmtime()");
106 }
107
108 buildfn = fn + sprintf(fn, "tmp/");
109 buildfn += strftime(buildfn, LEN_ULONG + 1, "%s.", t);
110 buildfn += snprintf(buildfn, LEN_ULONG + 1, "%d.", pid);
111 buildfn += sprintf(buildfn, "%s", 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 r = read(STDIN_FILENO, buf, 5);
148 if (r != 5) {
149 unlink(fn);
150 err(MDELIVER_EXITCODE, "Failed read from stdin");
151 }
152
153 if (strncmp(buf, "From ", 5) == 0) {
154 while ((r = read(STDIN_FILENO, buf, 1)) > 0
155 || (r == -1 && errno == EINTR)) {
156 if (buf[0] == '\n') {
157 break;
158 }
159 }
160
161 if (r == -1) {
162 unlink(fn);
163 err(MDELIVER_EXITCODE, "Failed read from stdin");
164 }
165 } else {
166 if (full_write(fd, buf, r) != r) {
167 unlink(fn);
168 err(MDELIVER_EXITCODE, "Failed write(%s)", fn);
169 }
170 }
171 }
172
173 /* Copy the message from stdin to fd. */
174 /* buf is passed only as an (early, evil) space optimization). */
175 static void
176 copy_message(int fd, char *buf, char *fn)
177 {
178 ssize_t r;
179
180 while ((r = read(STDIN_FILENO, buf, BUFSIZ)) > 0
181 || (r == -1 && errno == EINTR)) {
182 if (full_write(fd, buf, r) != r) {
183 unlink(fn);
184 err(MDELIVER_EXITCODE, "Failed write(%s)", fn);
185 }
186 }
187
188 if (r == -1) {
189 unlink(fn);
190 err(MDELIVER_EXITCODE, "Failed read from stdin");
191 }
192
193 if (fsync(fd) < 0) {
194 unlink(fn);
195 err(MDELIVER_EXITCODE, "Failed fsync(%s)", fn);
196 }
197
198 if (close(fd) < 0) {
199 unlink(fn);
200 err(MDELIVER_EXITCODE, "Failed close(%s)", fn);
201 }
202 }
203
204 static void
205 deliver(char *maildir, char *newfn)
206 {
207 char fn[TMPNAMLEN];
208 int fd;
209 char buf[BUFSIZ]; /* Allocated here only as an (early,
210 * evil) space optimization. */
211
212 if (chdir(maildir) != 0) {
213 err(MDELIVER_EXITCODE, "Failed chdir(%s)", maildir);
214 }
215
216 get_filename(fn);
217
218 fd = open(fn, O_WRONLY | O_EXCL | O_CREAT, 0600);
219 if (fd < 0) {
220 err(MDELIVER_EXITCODE, "Failed open(%s)", fn);
221 }
222
223 /* From this point, we have a tmp file, so if someting fails we
224 * must try to unlink it before bombing. */
225
226 strip_from(fd, buf, fn);
227 copy_message(fd, buf, fn);
228
229 strcpy(newfn, fn);
230 memcpy(newfn, "new", 3);
231
232 if (rename(fn, newfn) != 0) {
233 unlink(fn);
234 err(MDELIVER_EXITCODE, "Failed rename(%s, new)", fn);
235 }
236 }
237
238 int
239 main(int argc, char *argv[])
240 {
241 char *home;
242 char newfn[TMPNAMLEN];
243 pid_t pid;
244 FILE *errfile;
245 char *fail;
246 int fd;
247
248 if (argv[1] && argv[1][0] == '-' && argv[1][1] == 'v') {
249 puts(rcsid);
250 return 0;
251 }
252
253 if (!(home = getenv("HOME"))) {
254 err(MDELIVER_EXITCODE, "Failed getenv(HOME)");
255 }
256
257 if (chdir(home) != 0) {
258 err(MDELIVER_EXITCODE, "Failed chdir(HOME)");
259 }
260
261 deliver("Maildir", newfn);
262
263 if (argv[1]) {
264 if ((pid = fork()) > 0) {
265 return 0;
266 } else if (pid == 0) {
267 argv[0] = argv[1];
268 argv[1] = newfn;
269 execv(argv[0], argv);
270 fail = "exec";
271 } else {
272 fail = "fork";
273 }
274
275 /* If we got here fork or exec failed, so try to write an
276 * error message where later mail processors can find it. We
277 * really can't do anything about it if this fails. */
278 memcpy(newfn, "err", 3);
279 mkdir("err", 0700);
280 fd = open(newfn, O_WRONLY | O_EXCL | O_CREAT, 0644);
281 errfile = fdopen(fd, "w");
282 if (errfile) {
283 fprintf(errfile, "%s:%d\n", fail, errno);
284 }
285 }
286
287 /* Always return 0 because deliver exits if it fails, and if it
288 * succeeds we cannot return a failure code to the MTA. */
289 return 0;
290 }