Blob


1 /*
2 * Copyright (c) 2024 Stefan Sperling <stsp@openbsd.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 */
17 #include <sys/types.h>
18 #include <sys/socket.h>
20 #include <errno.h>
21 #include <poll.h>
22 #include <stdio.h>
23 #include <stdlib.h>
24 #include <string.h>
25 #include <stdarg.h>
26 #include <getopt.h>
27 #include <err.h>
28 #include <pwd.h>
29 #include <netdb.h>
30 #include <time.h>
31 #include <unistd.h>
33 #include "got_error.h"
35 #include "got_lib_poll.h"
37 #define SMTP_LINE_MAX 65535
39 static int smtp_timeout = 60; /* in seconds */
40 static char smtp_buf[SMTP_LINE_MAX];
41 static size_t smtp_buflen;
43 __dead static void
44 usage(void)
45 {
46 fprintf(stderr, "usage: %s [-f sender] [-r responder] "
47 "[-s subject] [-h hostname] [-p port] recipient\n", getprogname());
48 exit(1);
49 }
51 static int
52 dial(const char *host, const char *port)
53 {
54 struct addrinfo hints, *res, *res0;
55 const char *cause = NULL;
56 int s, error, save_errno;
58 memset(&hints, 0, sizeof(hints));
59 hints.ai_family = AF_UNSPEC;
60 hints.ai_socktype = SOCK_STREAM;
61 error = getaddrinfo(host, port, &hints, &res0);
62 if (error)
63 errx(1, "failed to resolve %s:%s: %s", host, port,
64 gai_strerror(error));
66 s = -1;
67 for (res = res0; res; res = res->ai_next) {
68 s = socket(res->ai_family, res->ai_socktype,
69 res->ai_protocol);
70 if (s == -1) {
71 cause = "socket";
72 continue;
73 }
75 if (connect(s, res->ai_addr, res->ai_addrlen) == -1) {
76 cause = "connect";
77 save_errno = errno;
78 close(s);
79 errno = save_errno;
80 s = -1;
81 continue;
82 }
84 break;
85 }
87 freeaddrinfo(res0);
88 if (s == -1)
89 err(1, "%s", cause);
90 return s;
91 }
93 static char *
94 set_default_fromaddr(void)
95 {
96 struct passwd *pw = NULL;
97 char *s;
98 char hostname[255];
100 pw = getpwuid(getuid());
101 if (pw == NULL) {
102 errx(1, "my UID %d was not found in password database",
103 getuid());
106 if (gethostname(hostname, sizeof(hostname)) == -1)
107 err(1, "gethostname");
109 if (asprintf(&s, "%s@%s", pw->pw_name, hostname) == -1)
110 err(1, "asprintf");
112 return s;
115 static int
116 read_smtp_code(int s, const char *code)
118 const struct got_error *error;
119 char *endl;
120 size_t linelen;
121 ssize_t r;
123 for (;;) {
124 endl = memmem(smtp_buf, smtp_buflen, "\r\n", 2);
125 if (endl != NULL)
126 break;
128 if (smtp_buflen == sizeof(smtp_buf))
129 errx(1, "line too long");
131 error = got_poll_fd(s, POLLIN, smtp_timeout);
132 if (error)
133 errx(1, "poll: %s", error->msg);
135 r = read(s, smtp_buf + smtp_buflen,
136 sizeof(smtp_buf) - smtp_buflen);
137 if (r == -1)
138 err(1, "read");
139 if (r == 0)
140 errx(1, "unexpected EOF");
141 smtp_buflen += r;
144 linelen = endl - smtp_buf;
145 if (linelen < 3)
146 errx(1, "invalid SMTP response");
148 if (strncmp(code, smtp_buf, 3) != 0) {
149 smtp_buf[3] = '\0';
150 warnx("unexpected SMTP message code: %s", smtp_buf);
151 return -1;
154 /*
155 * Normally we would get just one reply, but the regress doesn't
156 * use a real SMTP server and queues all the replies upfront.
157 */
158 linelen += 2;
159 memmove(smtp_buf, smtp_buf + linelen, smtp_buflen - linelen);
160 smtp_buflen -= linelen;
162 return 0;
165 static int
166 send_smtp_msg(int s, const char *fmt, ...)
168 const struct got_error *error;
169 char buf[512];
170 int len;
171 va_list ap;
173 va_start(ap, fmt);
174 len = vsnprintf(buf, sizeof(buf), fmt, ap);
175 va_end(ap);
176 if (len < 0) {
177 warn("vsnprintf");
178 return -1;
180 if (len >= sizeof(buf)) {
181 warnx("%s: buffer too small for message '%s...'",
182 __func__, buf);
183 return -1;
186 error = got_poll_write_full(s, buf, len);
187 if (error) {
188 warnx("write: %s", error->msg);
189 return -1;
192 return 0;
195 static char *
196 get_datestr(time_t *time, char *datebuf)
198 struct tm mytm, *tm;
199 char *p, *s;
201 tm = gmtime_r(time, &mytm);
202 if (tm == NULL)
203 return NULL;
204 s = asctime_r(tm, datebuf);
205 if (s == NULL)
206 return NULL;
207 p = strchr(s, '\n');
208 if (p)
209 *p = '\0';
210 return s;
213 static void
214 send_email(int s, const char *myfromaddr, const char *fromaddr,
215 const char *recipient, const char *replytoaddr,
216 const char *subject)
218 const struct got_error *error;
219 char *line = NULL;
220 size_t linesize = 0;
221 ssize_t linelen;
222 time_t now;
223 char datebuf[26];
224 char *datestr;
226 now = time(NULL);
227 datestr = get_datestr(&now, datebuf);
229 if (read_smtp_code(s, "220"))
230 errx(1, "unexpected SMTP greeting received");
232 if (send_smtp_msg(s, "HELO localhost\r\n"))
233 errx(1, "could not send HELO");
234 if (read_smtp_code(s, "250"))
235 errx(1, "unexpected SMTP response received");
237 if (send_smtp_msg(s, "MAIL FROM:<%s>\r\n", myfromaddr))
238 errx(1, "could not send MAIL FROM");
239 if (read_smtp_code(s, "250"))
240 errx(1, "unexpected SMTP response received");
242 if (send_smtp_msg(s, "RCPT TO:<%s>\r\n", recipient))
243 errx(1, "could not send MAIL FROM");
244 if (read_smtp_code(s, "250"))
245 errx(1, "unexpected SMTP response received");
247 if (send_smtp_msg(s, "DATA\r\n"))
248 errx(1, "could not send MAIL FROM");
249 if (read_smtp_code(s, "354"))
250 errx(1, "unexpected SMTP response received");
252 if (send_smtp_msg(s, "From: %s\r\n", fromaddr))
253 errx(1, "could not send From header");
254 if (send_smtp_msg(s, "To: %s\r\n", recipient))
255 errx(1, "could not send To header");
256 if (replytoaddr) {
257 if (send_smtp_msg(s, "Reply-To: %s\r\n", replytoaddr))
258 errx(1, "could not send Reply-To header");
260 if (send_smtp_msg(s, "Date: %s +0000 (UTC)\r\n", datestr))
261 errx(1, "could not send Date header");
263 if (send_smtp_msg(s, "Subject: %s\r\n", subject))
264 errx(1, "could not send Subject header");
266 if (send_smtp_msg(s, "\r\n"))
267 errx(1, "could not send body delimiter");
269 while ((linelen = getline(&line, &linesize, stdin)) != -1) {
270 if (line[0] == '.') { /* dot stuffing */
271 error = got_poll_write_full(s, ".", 1);
272 if (error)
273 errx(1, "write: %s", error->msg);
275 error = got_poll_write_full(s, line, linelen);
276 if (error)
277 errx(1, "write: %s", error->msg);
280 if (send_smtp_msg(s, "\r\n.\r\n"))
281 errx(1, "could not send data terminator");
282 if (read_smtp_code(s, "250"))
283 errx(1, "unexpected SMTP response received");
285 if (send_smtp_msg(s, "QUIT\r\n"))
286 errx(1, "could not send QUIT");
288 if (read_smtp_code(s, "221"))
289 errx(1, "unexpected SMTP response received");
291 close(s);
292 free(line);
295 int
296 main(int argc, char *argv[])
298 char *default_fromaddr = NULL;
299 const char *fromaddr = NULL, *recipient = NULL, *replytoaddr = NULL;
300 const char *subject = "gotd notification";
301 const char *hostname = "127.0.0.1";
302 const char *port = "25";
303 const char *errstr;
304 char *timeoutstr;
305 int ch, s;
307 while ((ch = getopt(argc, argv, "f:r:s:h:p:")) != -1) {
308 switch (ch) {
309 case 'h':
310 hostname = optarg;
311 break;
312 case 'f':
313 fromaddr = optarg;
314 break;
315 case 'p':
316 port = optarg;
317 break;
318 case 'r':
319 replytoaddr = optarg;
320 break;
321 case 's':
322 subject = optarg;
323 break;
324 default:
325 usage();
326 /* NOTREACHED */
327 break;
331 argc -= optind;
332 argv += optind;
334 if (argc != 1)
335 usage();
337 /* used by the regression test suite */
338 timeoutstr = getenv("GOT_NOTIFY_EMAIL_TIMEOUT");
339 if (timeoutstr) {
340 smtp_timeout = strtonum(timeoutstr, 0, 600, &errstr);
341 if (errstr != NULL)
342 errx(1, "timeout in seconds is %s: %s",
343 errstr, timeoutstr);
346 #ifndef PROFILE
347 if (pledge("stdio dns inet getpw", NULL) == -1)
348 err(1, "pledge");
349 #endif
350 default_fromaddr = set_default_fromaddr();
352 #ifndef PROFILE
353 if (pledge("stdio dns inet", NULL) == -1)
354 err(1, "pledge");
355 #endif
357 recipient = argv[0];
358 if (fromaddr == NULL)
359 fromaddr = default_fromaddr;
361 s = dial(hostname, port);
363 #ifndef PROFILE
364 if (pledge("stdio", NULL) == -1)
365 err(1, "pledge");
366 #endif
368 send_email(s, default_fromaddr, fromaddr, recipient, replytoaddr,
369 subject);
371 free(default_fromaddr);
372 return 0;