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