]> diplodocus.org Git - mdeliver/blob - mdeliver.c
fb266d725943706d707453d13808330adf40edb1
[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 <sysexits.h>
29 #include <time.h>
30 #include <unistd.h>
31
32 #if !defined(HOST_NAME_MAX)
33 #if defined MAXHOSTNAMELEN
34 #define HOST_NAME_MAX MAXHOSTNAMELEN
35 #else
36 #define HOST_NAME_MAX 64
37 #endif
38 #endif
39
40 /* holds 2^128 - 1 in decimal, plus '\0' */
41 #define LEN_ULONG 40
42
43 /* 1 for '\0', 4 for "tmp/", HOST_NAME_MAX for the hostname, and two
44 * LEN_ULONGs for time and pid. */
45 #define TMPNAMLEN 1 + 4 + HOST_NAME_MAX + LEN_ULONG + LEN_ULONG
46
47 static char rcsid[] = "$Id$";
48
49 /* Write all of buf, even if write(2) is interrupted. */
50 static ssize_t
51 full_write(int fd, const char *buf, size_t nbytes)
52 {
53 ssize_t r, w = 0;
54
55 /* Loop until nbytes of buf have been written. */
56 while (w < nbytes) {
57 /* Keep trying until write succeeds without interruption. */
58 do {
59 r = write(fd, buf + w, nbytes - w);
60 } while (r < 0 && errno == EINTR);
61
62 if (r < 0) {
63 return r;
64 }
65
66 w += r;
67 }
68
69 return w;
70 }
71
72 /* Find a filename for the message according to the algorithm
73 * documented in the maildir(5) manual page from qmail
74 * (http://www.qmail.org/qmail-manual-html/man5/maildir.html).
75 */
76 static void
77 get_filename(char *fn)
78 {
79 pid_t pid;
80 char host[HOST_NAME_MAX + 1];
81 int try;
82 time_t now;
83 struct tm *t;
84 char *buildfn; /* used to build up the fn */
85 struct stat statbuf;
86
87 pid = getpid();
88
89 if (gethostname(host, sizeof(host)) != 0) {
90 err(EX_TEMPFAIL, "Failed gethostname");
91 }
92
93 for (try = 0; ; try++) {
94 now = time(NULL);
95 t = gmtime(&now);
96
97 buildfn = fn + sprintf(fn, "tmp/");
98 buildfn += strftime(buildfn, LEN_ULONG + 1, "%s.", t);
99 buildfn += snprintf(buildfn, LEN_ULONG + 1, "%d.", pid);
100 buildfn += sprintf(buildfn, "%s", host);
101
102 if (stat(fn, &statbuf) != 0) {
103 if (errno == ENOENT) {
104 break;
105 }
106 }
107
108 if (try == 2) {
109 errx(EX_TEMPFAIL, "Attempted filename exists.");
110 }
111
112 sleep(2);
113 }
114 }
115
116 /* Strip the non-RFC2822-compliant 'From ' line some busted-ass
117 * mailers (such as Postfix) add as the first line of input. This bit
118 * of historical gunk originated with Berkeley mbox files, which use
119 * this line to indicate the beginning of a message. Postfix should
120 * only add such a line when it is acting as an MDA and delivering to
121 * a Berkeley mbox; the data written to an external MDA should be the
122 * raw RFC2822 message. >sigh<
123 */
124 /* buf is passed only as an (early, evil) space optimization). */
125 static void
126 strip_from(int fd, char *buf, char *fn)
127 {
128 ssize_t r;
129
130 r = read(STDIN_FILENO, buf, 5);
131 if (r != 5) {
132 unlink(fn);
133 err(EX_TEMPFAIL, "Failed read from stdin");
134 }
135
136 if (strncmp(buf, "From ", 5)) {
137 while ((r = read(STDIN_FILENO, buf, 1)) > 0
138 || (r == -1 && errno == EINTR)) {
139 if (buf[0] == '\n') {
140 break;
141 }
142 }
143
144 if (r == -1) {
145 unlink(fn);
146 err(EX_TEMPFAIL, "Failed read from stdin");
147 }
148 } else {
149 if (full_write(fd, buf, r) != r) {
150 unlink(fn);
151 err(EX_TEMPFAIL, "Failed write(%s)", fn);
152 }
153 }
154 }
155
156 /* Copy the message from stdin to fd. */
157 /* buf is passed only as an (early, evil) space optimization). */
158 static void
159 copy_message(int fd, char *buf, char *fn)
160 {
161 ssize_t r;
162
163 while ((r = read(STDIN_FILENO, buf, BUFSIZ)) > 0
164 || (r == -1 && errno == EINTR)) {
165 if (full_write(fd, buf, r) != r) {
166 unlink(fn);
167 err(EX_TEMPFAIL, "Failed write(%s)", fn);
168 }
169 }
170
171 if (r == -1) {
172 unlink(fn);
173 err(EX_TEMPFAIL, "Failed read from stdin");
174 }
175
176 if (fsync(fd) < 0) {
177 unlink(fn);
178 err(EX_TEMPFAIL, "Failed fsync(%s)", fn);
179 }
180
181 if (close(fd) < 0) {
182 unlink(fn);
183 err(EX_TEMPFAIL, "Failed close(%s)", fn);
184 }
185 }
186
187 static void
188 deliver(char *maildir, char *newfn)
189 {
190 char fn[TMPNAMLEN];
191 int fd;
192 char buf[BUFSIZ]; /* Allocated here only as an (early,
193 * evil) space optimization. */
194
195 if (chdir(maildir) != 0) {
196 err(EX_TEMPFAIL, "Failed chdir(%s)", maildir);
197 }
198
199 get_filename(fn);
200
201 fd = open(fn, O_WRONLY | O_EXCL | O_CREAT, 0600);
202 if (fd < 0) {
203 err(EX_TEMPFAIL, "Failed open(%s)", fn);
204 }
205
206 /* From this point, we have a tmp file, so if someting fails we
207 * must try to unlink it before bombing. */
208
209 strip_from(fd, buf, fn);
210 copy_message(fd, buf, fn);
211
212 strcpy(newfn, fn);
213 memcpy(newfn, "new", 3);
214
215 if (rename(fn, newfn) != 0) {
216 unlink(fn);
217 err(EX_TEMPFAIL, "Failed rename(%s, new)", fn);
218 }
219 }
220
221 int
222 main(int argc, char *argv[])
223 {
224 char *home;
225 char newfn[TMPNAMLEN];
226 pid_t pid;
227 FILE *errfile;
228 char *fail;
229 int fd;
230
231 if (argv[1] && argv[1][0] == '-' && argv[1][1] == 'v') {
232 puts(rcsid);
233 return 0;
234 }
235
236 if (!(home = getenv("HOME"))) {
237 err(EX_TEMPFAIL, "Failed getenv(HOME)");
238 }
239
240 if (chdir(home) != 0) {
241 err(EX_TEMPFAIL, "Failed chdir(HOME)");
242 }
243
244 deliver("Maildir", newfn);
245
246 if (argv[1]) {
247 if ((pid = fork()) > 0) {
248 return 0;
249 } else if (pid == 0) {
250 argv[0] = argv[1];
251 argv[1] = newfn;
252 execv(argv[0], argv);
253 fail = "exec";
254 } else {
255 fail = "fork";
256 }
257
258 /* If we got here fork or exec failed, so try to write an
259 * error message where later mail processors can find it. We
260 * really can't do anything about it if this fails. */
261 memcpy(newfn, "err", 3);
262 mkdir("err", 0700);
263 fd = open(newfn, O_WRONLY | O_EXCL | O_CREAT, 0644);
264 errfile = fdopen(fd, "w");
265 if (errfile) {
266 fprintf(errfile, "%s:%d\n", fail, errno);
267 }
268 }
269
270 /* Always return 0 because deliver exits if it fails, and if it
271 * succeeds we cannot return a failure code to the MTA. */
272 return 0;
273 }