Clarified description of clobber -auto/-suffix per Ralph's suggestion.
[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         /*
53          * If there is a pid file already around, kill the previously running
54          * fakesmtp process.  Hopefully this will reduce the race conditions
55          * that crop up when running the test suite.
56          */
57
58         if (stat(PIDFILE, &st) == 0) {
59                 long oldpid;
60
61                 if (!(pid = fopen(PIDFILE, "r"))) {
62                         fprintf(stderr, "Cannot open " PIDFILE
63                                 " (%s), continuing ...\n", strerror(errno));
64                 } else {
65                         rc = fscanf(pid, "%ld", &oldpid);
66                         fclose(pid);
67
68                         if (rc != 1) {
69                                 fprintf(stderr, "Unable to parse pid in "
70                                         PIDFILE ", continuing ...\n");
71                         } else {
72                                 kill((pid_t) oldpid, SIGTERM);
73                         }
74                 }
75
76                 unlink(PIDFILE);
77         }
78
79         memset(&hints, 0, sizeof(hints));
80
81         hints.ai_family = PF_INET;
82         hints.ai_socktype = SOCK_STREAM;
83         hints.ai_protocol = IPPROTO_TCP;
84         hints.ai_flags = AI_PASSIVE;
85
86         rc = getaddrinfo("127.0.0.1", argv[2], &hints, &res);
87
88         if (rc) {
89                 fprintf(stderr, "Unable to resolve localhost/%s: %s\n",
90                         argv[2], gai_strerror(rc));
91                 exit(1);
92         }
93
94         l = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
95
96         if (l == -1) {
97                 fprintf(stderr, "Unable to create listening socket: %s\n",
98                         strerror(errno));
99                 exit(1);
100         }
101
102         on = 1;
103
104         if (setsockopt(l, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) == -1) {
105                 fprintf(stderr, "Unable to set SO_REUSEADDR: %s\n",
106                         strerror(errno));
107                 exit(1);
108         }
109
110         if (bind(l, res->ai_addr, res->ai_addrlen) == -1) {
111                 fprintf(stderr, "Unable to bind socket: %s\n", strerror(errno));
112                 exit(1);
113         }
114
115         if (listen(l, 1) == -1) {
116                 fprintf(stderr, "Unable to listen on socket: %s\n",
117                         strerror(errno));
118                 exit(1);
119         }
120
121         /*
122          * Now that our socket & files are set up, wait 30 seconds for
123          * a connection.  If there isn't one, then exit.
124          */
125
126         if (!(pid = fopen(PIDFILE, "w"))) {
127                 fprintf(stderr, "Cannot open " PIDFILE ": %s\n",
128                         strerror(errno));
129                 exit(1);
130         }
131
132         fprintf(pid, "%ld\n", (long) getpid());
133         fclose(pid);
134
135         signal(SIGTERM, handleterm);
136         atexit(killpidfile);
137
138         FD_ZERO(&readfd);
139         FD_SET(l, &readfd);
140         tv.tv_sec = 30;
141         tv.tv_usec = 0;
142
143         rc = select(l + 1, &readfd, NULL, NULL, &tv);
144
145         if (rc < 0) {
146                 fprintf(stderr, "select() failed: %s\n", strerror(errno));
147                 exit(1);
148         }
149
150         /*
151          * I think if we get a timeout, we should just exit quietly.
152          */
153
154         if (rc == 0) {
155                 exit(1);
156         }
157
158         /*
159          * Alright, got a connection!  Accept it.
160          */
161
162         if ((conn = accept(l, NULL, NULL)) == -1) {
163                 fprintf(stderr, "Unable to accept connection: %s\n",
164                         strerror(errno));
165                 exit(1);
166         }
167
168         close(l);
169         
170         /*
171          * Pretend to be an SMTP server.
172          */
173
174         putsmtp(conn, "220 Not really an ESMTP server");
175         datamode = 0;
176
177         for (;;) {
178                 char line[LINESIZE];
179
180                 rc = getsmtp(conn, line);
181
182                 if (rc == -1)
183                         break;  /* EOF */
184
185                 fprintf(f, "%s\n", line);
186
187                 /*
188                  * If we're in DATA mode, then check to see if we've got
189                  * a "."; otherwise, continue
190                  */
191
192                 if (datamode) {
193                         if (strcmp(line, ".") == 0) {
194                                 datamode = 0;
195                                 putsmtp(conn, "250 Thanks for the info!");
196                         }
197                         continue;
198                 }
199
200                 /*
201                  * Most commands we ignore and send the same response to.
202                  */
203
204                 if (strcmp(line, "QUIT") == 0) {
205                         putsmtp(conn, "221 Later alligator!");
206                         close(conn);
207                         break;
208                 } else if (strcmp(line, "DATA") == 0) {
209                         putsmtp(conn, "354 Go ahead");
210                         datamode = 1;
211                 } else {
212                         putsmtp(conn, "250 I'll buy that for a dollar!");
213                 }
214         }
215
216         fclose(f);
217
218         exit(0);
219 }
220
221 /*
222  * Write a line to the SMTP client on the other end
223  */
224
225 static void
226 putsmtp(int socket, char *data)
227 {
228         struct iovec iov[2];
229
230         iov[0].iov_base = data;
231         iov[0].iov_len = strlen(data);
232         iov[1].iov_base = "\r\n";
233         iov[1].iov_len = 2;
234
235         writev(socket, iov, 2);
236 }
237
238 /*
239  * Read a line (up to the \r\n)
240  */
241
242 static int
243 getsmtp(int socket, char *data)
244 {
245         int cc;
246         static unsigned int bytesinbuf = 0;
247         static char buffer[LINESIZE * 2], *p;
248
249         for (;;) {
250                 /*
251                  * Find our \r\n
252                  */
253
254                 if (bytesinbuf > 0 && (p = strchr(buffer, '\r')) &&
255                                                         *(p + 1) == '\n') {
256                         *p = '\0';
257                         strncpy(data, buffer, LINESIZE);
258                         data[LINESIZE - 1] = '\0';
259                         cc = strlen(buffer);
260
261                         /*
262                          * Shuffle leftover bytes back to the beginning
263                          */
264
265                         bytesinbuf -= cc + 2;   /* Don't forget \r\n */
266                         if (bytesinbuf > 0) {
267                                 memmove(buffer, buffer + cc + 2, bytesinbuf);
268                         }
269                         return cc;
270                 }
271
272                 if (bytesinbuf >= sizeof(buffer)) {
273                         fprintf(stderr, "Buffer overflow in getsmtp()!\n");
274                         exit(1);
275                 }
276
277                 memset(buffer + bytesinbuf, 0, sizeof(buffer) - bytesinbuf);
278                 cc = read(socket, buffer + bytesinbuf,
279                           sizeof(buffer) - bytesinbuf);
280
281                 if (cc < 0) {
282                         fprintf(stderr, "Read failed: %s\n", strerror(errno));
283                         exit(1);
284                 }
285
286                 if (cc == 0)
287                         return -1;
288
289                 bytesinbuf += cc;
290         }
291 }
292
293 /*
294  * Handle a SIGTERM
295  */
296
297 static void
298 handleterm(int signal)
299 {
300         (void) signal;
301
302         killpidfile();
303         fflush(NULL);
304         _exit(1);
305 }
306
307 /*
308  * Get rid of our pid file
309  */
310
311 static void
312 killpidfile(void)
313 {
314         unlink(PIDFILE);
315 }