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 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 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 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 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 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 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