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