Added whom, it's the fifth program referred to in mh-draft man page.
[mmh] / test / fakesmtp.c
1 /*
2  * fakesmtp - A fake SMTP server used by the nmh test suite
3  *
4  * This code is Copyright (c) 2012, by the authors of nmh.  See the
5  * COPYRIGHT file in the root directory of the nmh distribution for
6  * complete copyright information.
7  */
8
9 #include <stdio.h>
10 #include <stdlib.h>
11 #include <string.h>
12 #include <unistd.h>
13 #include <netdb.h>
14 #include <errno.h>
15 #include <sys/socket.h>
16 #include <sys/types.h>
17 #include <sys/select.h>
18 #include <sys/stat.h>
19 #include <sys/uio.h>
20 #include <signal.h>
21
22 #define PIDFILE "/tmp/fakesmtp.pid"
23
24 #define LINESIZE 1024
25
26 static void killpidfile(void);
27 static void handleterm(int);
28 static void putsmtp(int, char *);
29 static int getsmtp(int, char *);
30
31 int
32 main(int argc, char *argv[])
33 {
34         struct addrinfo hints, *res;
35         int rc, l, conn, on, datamode;
36         FILE *f, *pid;
37         fd_set readfd;
38         struct stat st;
39         struct timeval tv;
40
41         if (argc != 3) {
42                 fprintf(stderr, "Usage: %s output-filename port\n", argv[0]);
43                 exit(1);
44         }
45
46         if (!(f = fopen(argv[1], "w"))) {
47                 fprintf(stderr, "Unable to open output file \"%s\": %s\n",
48                         argv[1], strerror(errno));
49                 exit(1);
50         }
51
52         memset(&hints, 0, sizeof(hints));
53
54         hints.ai_family = PF_INET;
55         hints.ai_socktype = SOCK_STREAM;
56         hints.ai_protocol = IPPROTO_TCP;
57         hints.ai_flags = AI_PASSIVE;
58
59         rc = getaddrinfo("127.0.0.1", argv[2], &hints, &res);
60
61         if (rc) {
62                 fprintf(stderr, "Unable to resolve localhost/%s: %s\n",
63                         argv[2], gai_strerror(rc));
64                 exit(1);
65         }
66
67         l = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
68
69         if (l == -1) {
70                 fprintf(stderr, "Unable to create listening socket: %s\n",
71                         strerror(errno));
72                 exit(1);
73         }
74
75         on = 1;
76
77         if (setsockopt(l, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) == -1) {
78                 fprintf(stderr, "Unable to set SO_REUSEADDR: %s\n",
79                         strerror(errno));
80                 exit(1);
81         }
82
83         if (bind(l, res->ai_addr, res->ai_addrlen) == -1) {
84                 fprintf(stderr, "Unable to bind socket: %s\n", strerror(errno));
85                 exit(1);
86         }
87
88         if (listen(l, 1) == -1) {
89                 fprintf(stderr, "Unable to listen on socket: %s\n",
90                         strerror(errno));
91                 exit(1);
92         }
93
94         /*
95          * Now that our socket & files are set up, do the following things:
96          *
97          * - Check for a PID file.  If there is one, kill the old version.
98          * - Wait 30 seconds for a connection.  If there isn't one, then
99          *   exit.
100          */
101
102         if (stat(PIDFILE, &st) == 0) {
103                 long oldpid;
104
105                 if (!(pid = fopen(PIDFILE, "r"))) {
106                         fprintf(stderr, "Cannot open " PIDFILE
107                                 " (%s), continuing ...\n", strerror(errno));
108                 } else {
109                         rc = fscanf(pid, "%ld", &oldpid);
110                         fclose(pid);
111
112                         if (rc != 1) {
113                                 fprintf(stderr, "Unable to parse pid in "
114                                         PIDFILE ", continuing ...\n");
115                         } else {
116                                 kill((pid_t) oldpid, SIGTERM);
117                         }
118                 }
119
120                 unlink(PIDFILE);
121         }
122
123         if (!(pid = fopen(PIDFILE, "w"))) {
124                 fprintf(stderr, "Cannot open " PIDFILE ": %s\n",
125                         strerror(errno));
126                 exit(1);
127         }
128
129         fprintf(pid, "%ld\n", (long) getpid());
130         fclose(pid);
131
132         signal(SIGTERM, handleterm);
133         atexit(killpidfile);
134
135         FD_ZERO(&readfd);
136         FD_SET(l, &readfd);
137         tv.tv_sec = 30;
138         tv.tv_usec = 0;
139
140         rc = select(l + 1, &readfd, NULL, NULL, &tv);
141
142         if (rc < 0) {
143                 fprintf(stderr, "select() failed: %s\n", strerror(errno));
144                 exit(1);
145         }
146
147         /*
148          * I think if we get a timeout, we should just exit quietly.
149          */
150
151         if (rc == 0) {
152                 exit(1);
153         }
154
155         /*
156          * Alright, got a connection!  Accept it.
157          */
158
159         if ((conn = accept(l, NULL, NULL)) == -1) {
160                 fprintf(stderr, "Unable to accept connection: %s\n",
161                         strerror(errno));
162                 exit(1);
163         }
164
165         close(l);
166         
167         /*
168          * Pretend to be an SMTP server.
169          */
170
171         putsmtp(conn, "220 Not really an ESMTP server");
172         datamode = 0;
173
174         for (;;) {
175                 char line[LINESIZE];
176
177                 rc = getsmtp(conn, line);
178
179                 if (rc == -1)
180                         break;  /* EOF */
181
182                 fprintf(f, "%s\n", line);
183
184                 /*
185                  * If we're in DATA mode, then check to see if we've got
186                  * a "."; otherwise, continue
187                  */
188
189                 if (datamode) {
190                         if (strcmp(line, ".") == 0) {
191                                 datamode = 0;
192                                 putsmtp(conn, "250 Thanks for the info!");
193                         }
194                         continue;
195                 }
196
197                 /*
198                  * Most commands we ignore and send the same response to.
199                  */
200
201                 if (strcmp(line, "QUIT") == 0) {
202                         putsmtp(conn, "221 Later alligator!");
203                         close(conn);
204                         break;
205                 } else if (strcmp(line, "DATA") == 0) {
206                         putsmtp(conn, "354 Go ahead");
207                         datamode = 1;
208                 } else {
209                         putsmtp(conn, "250 I'll buy that for a dollar!");
210                 }
211         }
212
213         fclose(f);
214
215         exit(0);
216 }
217
218 /*
219  * Write a line to the SMTP client on the other end
220  */
221
222 static void
223 putsmtp(int socket, char *data)
224 {
225         struct iovec iov[2];
226
227         iov[0].iov_base = data;
228         iov[0].iov_len = strlen(data);
229         iov[1].iov_base = "\r\n";
230         iov[1].iov_len = 2;
231
232         writev(socket, iov, 2);
233 }
234
235 /*
236  * Read a line (up to the \r\n)
237  */
238
239 static int
240 getsmtp(int socket, char *data)
241 {
242         int cc;
243         static unsigned int bytesinbuf = 0;
244         static char buffer[LINESIZE * 2], *p;
245
246         for (;;) {
247                 /*
248                  * Find our \r\n
249                  */
250
251                 if (bytesinbuf > 0 && (p = strchr(buffer, '\r')) &&
252                                                         *(p + 1) == '\n') {
253                         *p = '\0';
254                         strncpy(data, buffer, LINESIZE);
255                         data[LINESIZE - 1] = '\0';
256                         cc = strlen(buffer);
257
258                         /*
259                          * Shuffle leftover bytes back to the beginning
260                          */
261
262                         bytesinbuf -= cc + 2;   /* Don't forget \r\n */
263                         if (bytesinbuf > 0) {
264                                 memmove(buffer, buffer + cc + 2, bytesinbuf);
265                         }
266                         return cc;
267                 }
268
269                 if (bytesinbuf >= sizeof(buffer)) {
270                         fprintf(stderr, "Buffer overflow in getsmtp()!\n");
271                         exit(1);
272                 }
273
274                 memset(buffer + bytesinbuf, 0, sizeof(buffer) - bytesinbuf);
275                 cc = read(socket, buffer + bytesinbuf,
276                           sizeof(buffer) - bytesinbuf);
277
278                 if (cc < 0) {
279                         fprintf(stderr, "Read failed: %s\n", strerror(errno));
280                         exit(1);
281                 }
282
283                 if (cc == 0)
284                         return -1;
285
286                 bytesinbuf += cc;
287         }
288 }
289
290 /*
291  * Handle a SIGTERM
292  */
293
294 static void
295 handleterm(int signal)
296 {
297         (void) signal;
298
299         killpidfile();
300         fflush(NULL);
301         _exit(1);
302 }
303
304 /*
305  * Get rid of our pid file
306  */
307
308 static void
309 killpidfile(void)
310 {
311         unlink(PIDFILE);
312 }