xref: /openbsd/usr.sbin/smtpd/mail.lmtp.c (revision 09467b48)
1 /*
2  * Copyright (c) 2017 Gilles Chehade <gilles@poolp.org>
3  *
4  * Permission to use, copy, modify, and distribute this software for any
5  * purpose with or without fee is hereby granted, provided that the above
6  * copyright notice and this permission notice appear in all copies.
7  *
8  * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
9  * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
10  * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
11  * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
12  * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
13  * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
14  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
15  */
16 
17 #include <sys/types.h>
18 #include <sys/socket.h>
19 #include <sys/un.h>
20 
21 #include <ctype.h>
22 #include <err.h>
23 #include <errno.h>
24 #include <netdb.h>
25 #include <stdio.h>
26 #include <stdlib.h>
27 #include <string.h>
28 #include <sysexits.h>
29 #include <unistd.h>
30 
31 enum phase {
32 	PHASE_BANNER,
33 	PHASE_HELO,
34 	PHASE_MAILFROM,
35 	PHASE_RCPTTO,
36 	PHASE_DATA,
37 	PHASE_EOM,
38 	PHASE_QUIT
39 };
40 
41 struct session {
42 	const char	*lhlo;
43 	const char	*mailfrom;
44 	char		*rcptto;
45 
46 	char		**rcpts;
47 	int		n_rcpts;
48 };
49 
50 static int lmtp_connect(const char *);
51 static void lmtp_engine(int, struct session *);
52 static void stream_file(FILE *);
53 
54 int
55 main(int argc, char *argv[])
56 {
57 	int ch;
58 	int conn;
59 	const char *destination = "localhost";
60 	struct session	session;
61 
62 	if (! geteuid())
63 		errx(EX_TEMPFAIL, "mail.lmtp: may not be executed as root");
64 
65 	session.lhlo = "localhost";
66 	session.mailfrom = getenv("SENDER");
67 	session.rcptto = NULL;
68 
69 	while ((ch = getopt(argc, argv, "d:l:f:ru")) != -1) {
70 		switch (ch) {
71 		case 'd':
72 			destination = optarg;
73 			break;
74 		case 'l':
75 			session.lhlo = optarg;
76 			break;
77 		case 'f':
78 			session.mailfrom = optarg;
79 			break;
80 
81 		case 'r':
82 			session.rcptto = getenv("RECIPIENT");
83 			break;
84 
85 		case 'u':
86 			session.rcptto = getenv("USER");
87 			break;
88 
89 		default:
90 			break;
91 		}
92 	}
93 	argc -= optind;
94 	argv += optind;
95 
96 	if (session.mailfrom == NULL)
97 		errx(EX_TEMPFAIL, "sender must be specified with -f");
98 
99 	if (argc == 0 && session.rcptto == NULL)
100 		errx(EX_TEMPFAIL, "no recipient was specified");
101 
102 	if (session.rcptto) {
103 		session.rcpts = &session.rcptto;
104 		session.n_rcpts = 1;
105 	}
106 	else {
107 		session.rcpts = argv;
108 		session.n_rcpts = argc;
109 	}
110 
111 	conn = lmtp_connect(destination);
112 	lmtp_engine(conn, &session);
113 
114 	return (0);
115 }
116 
117 static int
118 lmtp_connect_inet(const char *destination)
119 {
120 	struct addrinfo hints, *res, *res0;
121 	char *destcopy = NULL;
122 	const char *hostname = NULL;
123 	const char *servname = NULL;
124 	const char *cause = NULL;
125 	char *p;
126 	int n, s = -1, save_errno;
127 
128 	if ((destcopy = strdup(destination)) == NULL)
129 		err(EX_TEMPFAIL, NULL);
130 
131 	servname = "25";
132 	hostname = destcopy;
133 	p = destcopy;
134 	if (*p == '[') {
135 		if ((p = strchr(destcopy, ']')) == NULL)
136 			errx(EX_TEMPFAIL, "inet: invalid address syntax");
137 
138 		/* remove [ and ] */
139 		*p = '\0';
140 		hostname++;
141 		if (strncasecmp(hostname, "IPv6:", 5) == 0)
142 			hostname += 5;
143 
144 		/* extract port if any */
145 		switch (*(p+1)) {
146 		case ':':
147 			servname = p+2;
148 			break;
149 		case '\0':
150 			break;
151 		default:
152 			errx(EX_TEMPFAIL, "inet: invalid address syntax");
153 		}
154 	}
155 	else if ((p = strchr(destcopy, ':')) != NULL) {
156 		*p++ = '\0';
157 		servname = p;
158 	}
159 
160 	memset(&hints, 0, sizeof hints);
161 	hints.ai_family = PF_UNSPEC;
162 	hints.ai_socktype = SOCK_STREAM;
163 	hints.ai_flags = AI_NUMERICSERV;
164 	n = getaddrinfo(hostname, servname, &hints, &res0);
165 	if (n)
166 		errx(EX_TEMPFAIL, "inet: %s", gai_strerror(n));
167 
168 	for (res = res0; res; res = res->ai_next) {
169 		s = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
170 		if (s == -1) {
171 			cause = "socket";
172 			continue;
173 		}
174 
175 		if (connect(s, res->ai_addr, res->ai_addrlen) == -1) {
176 			cause = "connect";
177 			save_errno = errno;
178 			close(s);
179 			errno = save_errno;
180 			s = -1;
181 			continue;
182 		}
183 		break;
184 	}
185 
186 	freeaddrinfo(res0);
187 	if (s == -1)
188 		errx(EX_TEMPFAIL, "%s", cause);
189 
190 	free(destcopy);
191 	return s;
192 }
193 
194 static int
195 lmtp_connect_unix(const char *destination)
196 {
197 	struct sockaddr_un addr;
198 	int s;
199 
200 	if (*destination != '/')
201 		errx(EX_TEMPFAIL, "unix: path must be absolute");
202 
203 	if ((s = socket(PF_LOCAL, SOCK_STREAM, 0)) == -1)
204 		err(EX_TEMPFAIL, NULL);
205 
206 	memset(&addr, 0, sizeof addr);
207 	addr.sun_family = AF_UNIX;
208 	if (strlcpy(addr.sun_path, destination, sizeof addr.sun_path)
209 	    >= sizeof addr.sun_path)
210 		errx(EX_TEMPFAIL, "unix: socket path is too long");
211 
212 	if (connect(s, (struct sockaddr *)&addr, sizeof addr) == -1)
213 		err(EX_TEMPFAIL, "connect");
214 
215 	return s;
216 }
217 
218 static int
219 lmtp_connect(const char *destination)
220 {
221 	if (destination[0] == '/')
222 		return lmtp_connect_unix(destination);
223 	return lmtp_connect_inet(destination);
224 }
225 
226 static void
227 lmtp_engine(int fd_read, struct session *session)
228 {
229 	int fd_write = 0;
230 	FILE *file_read = 0;
231 	FILE *file_write = 0;
232 	char *line = NULL;
233 	size_t linesize = 0;
234 	ssize_t linelen;
235 	enum phase phase = PHASE_BANNER;
236 
237 	if ((fd_write = dup(fd_read)) == -1)
238 		err(EX_TEMPFAIL, "dup");
239 
240 	if ((file_read = fdopen(fd_read, "r")) == NULL)
241 		err(EX_TEMPFAIL, "fdopen");
242 
243 	if ((file_write = fdopen(fd_write, "w")) == NULL)
244 		err(EX_TEMPFAIL, "fdopen");
245 
246 	do {
247 		fflush(file_write);
248 
249 		if ((linelen = getline(&line, &linesize, file_read)) == -1) {
250 			if (ferror(file_read))
251 				err(EX_TEMPFAIL, "getline");
252 			else
253 				errx(EX_TEMPFAIL, "unexpected EOF from LMTP server");
254 		}
255 		line[strcspn(line, "\n")] = '\0';
256 		line[strcspn(line, "\r")] = '\0';
257 
258 		if (linelen < 4 ||
259 		    !isdigit((unsigned char)line[0]) ||
260 		    !isdigit((unsigned char)line[1]) ||
261 		    !isdigit((unsigned char)line[2]) ||
262 		    (line[3] != ' ' && line[3] != '-'))
263 			errx(EX_TEMPFAIL, "LMTP server sent an invalid line");
264 
265 		if (line[0] != (phase == PHASE_DATA ? '3' : '2'))
266 			errx(EX_TEMPFAIL, "LMTP server error: %s", line);
267 
268 		if (line[3] == '-')
269 			continue;
270 
271 		switch (phase) {
272 
273 		case PHASE_BANNER:
274 			fprintf(file_write, "LHLO %s\r\n", session->lhlo);
275 			phase++;
276 			break;
277 
278 		case PHASE_HELO:
279 			fprintf(file_write, "MAIL FROM:<%s>\r\n", session->mailfrom);
280 			phase++;
281 			break;
282 
283 		case PHASE_MAILFROM:
284 			fprintf(file_write, "RCPT TO:<%s>\r\n", session->rcpts[session->n_rcpts - 1]);
285 			if (session->n_rcpts - 1 == 0) {
286 				phase++;
287 				break;
288 			}
289 			session->n_rcpts--;
290 			break;
291 
292 		case PHASE_RCPTTO:
293 			fprintf(file_write, "DATA\r\n");
294 			phase++;
295 			break;
296 
297 		case PHASE_DATA:
298 			stream_file(file_write);
299 			fprintf(file_write, ".\r\n");
300 			phase++;
301 			break;
302 
303 		case PHASE_EOM:
304 			fprintf(file_write, "QUIT\r\n");
305 			phase++;
306 			break;
307 
308 		case PHASE_QUIT:
309 			exit(0);
310 		}
311 	} while (1);
312 }
313 
314 static void
315 stream_file(FILE *conn)
316 {
317 	char *line = NULL;
318 	size_t linesize = 0;
319 	ssize_t linelen;
320 
321 	while ((linelen = getline(&line, &linesize, stdin)) != -1) {
322 		line[strcspn(line, "\n")] = '\0';
323 		if (line[0] == '.')
324 			fprintf(conn, ".");
325 		fprintf(conn, "%s\r\n", line);
326 	}
327 	free(line);
328 	if (ferror(stdin))
329 		err(EX_TEMPFAIL, "getline");
330 }
331