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