commit 169c5c8
Merge pull request 'do email notifications with CURL' (#283) from shtrophic/snac2:curl-smtp into master Reviewed-on: https://codeberg.org/grunfink/snac2/pulls/283
+2,
-1
1@@ -1,2 +1,3 @@
2-*.o
3+**/*.o
4+tests/smtp
5 snac
M
Makefile
+8,
-2
1@@ -8,11 +8,16 @@ snac: snac.o main.o sandbox.o data.o http.o httpd.o webfinger.o \
2 activitypub.o html.o utils.o format.o upgrade.o mastoapi.o
3 $(CC) $(CFLAGS) -L$(PREFIX)/lib *.o -lcurl -lcrypto $(LDFLAGS) -pthread -o $@
4
5+test: tests/smtp
6+
7+tests/smtp: tests/smtp.o
8+ $(CC) $(CFLAGS) -L$(PREFIX)/lib $< -lcurl $(LDFLAGS) -o $@
9+
10 .c.o:
11- $(CC) $(CFLAGS) $(CPPFLAGS) -I$(PREFIX)/include -c $<
12+ $(CC) $(CFLAGS) $(CPPFLAGS) -I$(PREFIX)/include -c $< -o $@
13
14 clean:
15- rm -rf *.o *.core snac makefile.depend
16+ rm -rf *.o tests/*.o tests/smtp *.core snac makefile.depend
17
18 dep:
19 $(CC) -I$(PREFIX)/include -MM *.c > makefile.depend
20@@ -72,3 +77,4 @@ utils.o: utils.c xs.h xs_io.h xs_json.h xs_time.h xs_openssl.h \
21 xs_random.h xs_glob.h xs_curl.h xs_regex.h snac.h http_codes.h
22 webfinger.o: webfinger.c xs.h xs_json.h xs_curl.h xs_mime.h snac.h \
23 http_codes.h
24+tests/smtp.o: tests/smtp.c xs.h xs_curl.h
+21,
-30
1@@ -1044,17 +1044,20 @@ void notify(snac *snac, const char *type, const char *utype, const char *actor,
2
3 xs *subject = xs_fmt("snac notify for @%s@%s",
4 xs_dict_get(snac->config, "uid"), xs_dict_get(srv_config, "host"));
5- xs *from = xs_fmt("snac-daemon <snac-daemon@%s>", xs_dict_get(srv_config, "host"));
6+ xs *from = xs_fmt("<snac-daemon@%s>", xs_dict_get(srv_config, "host"));
7 xs *header = xs_fmt(
8- "From: %s\n"
9+ "From: snac-daemon %s\n"
10 "To: %s\n"
11 "Subject: %s\n"
12 "\n",
13 from, email, subject);
14
15- xs *email_body = xs_fmt("%s%s", header, body);
16+ xs *mailinfo = xs_dict_new();
17+ mailinfo = xs_dict_append(mailinfo, "from", from);
18+ mailinfo = xs_dict_append(mailinfo, "to", email);
19+ mailinfo = xs_dict_append(mailinfo, "body", xs_fmt("%s%s", header, body));
20
21- enqueue_email(email_body, 0);
22+ enqueue_email(mailinfo, 0);
23 }
24
25 /* telegram */
26@@ -2563,32 +2566,20 @@ int process_input_message(snac *snac, const xs_dict *msg, const xs_dict *req)
27 }
28
29
30-int send_email(const char *msg)
31-/* invoke sendmail with email headers and body in msg */
32+int send_email(const xs_dict *mailinfo)
33+/* invoke curl */
34 {
35- FILE *f;
36- int status;
37- int fds[2];
38- pid_t pid;
39- if (pipe(fds) == -1) return -1;
40- pid = vfork();
41- if (pid == -1) return -1;
42- else if (pid == 0) {
43- dup2(fds[0], 0);
44- close(fds[0]);
45- close(fds[1]);
46- execl("/usr/sbin/sendmail", "sendmail", "-t", (char *) NULL);
47- _exit(1);
48- }
49- close(fds[0]);
50- if ((f = fdopen(fds[1], "w")) == NULL) {
51- close(fds[1]);
52- return -1;
53- }
54- fprintf(f, "%s\n", msg);
55- fclose(f);
56- if (waitpid(pid, &status, 0) == -1) return -1;
57- return status;
58+ const xs_dict *smtp_cfg = xs_dict_get(srv_config, "email_notifications");
59+ const char
60+ *url = xs_dict_get_def(smtp_cfg, "url", "smtp://localhost"),
61+ *user = xs_dict_get(smtp_cfg, "username"),
62+ *pass = xs_dict_get(smtp_cfg, "password"),
63+ *from = xs_dict_get(mailinfo, "from"),
64+ *to = xs_dict_get(mailinfo, "to"),
65+ *body = xs_dict_get(mailinfo, "body");
66+ int smtp_port = parse_port(url, NULL);
67+
68+ return xs_smtp_request(url, user, pass, from, to, body, smtp_port == 465 || smtp_port == 587);
69 }
70
71
72@@ -2861,7 +2852,7 @@ void process_queue_item(xs_dict *q_item)
73 else
74 if (strcmp(type, "email") == 0) {
75 /* send this email */
76- const xs_str *msg = xs_dict_get(q_item, "message");
77+ const xs_dict *msg = xs_dict_get(q_item, "message");
78 int retries = xs_number_get(xs_dict_get(q_item, "retries"));
79
80 if (!send_email(msg))
M
data.c
+1,
-1
1@@ -3357,7 +3357,7 @@ void enqueue_output_by_actor(snac *snac, const xs_dict *msg,
2 }
3
4
5-void enqueue_email(const xs_str *msg, int retries)
6+void enqueue_email(const xs_dict *msg, int retries)
7 /* enqueues an email message to be sent */
8 {
9 xs *qmsg = _new_qmsg("email", msg, retries);
+15,
-16
1@@ -8,8 +8,6 @@ void sbox_enter(const char *basedir)
2 {
3 const char *address = xs_dict_get(srv_config, "address");
4
5- int smail = !xs_is_true(xs_dict_get(srv_config, "disable_email_notifications"));
6-
7 if (xs_is_true(xs_dict_get(srv_config, "disable_openbsd_security"))) {
8 srv_log(xs_dup("OpenBSD security disabled by admin"));
9 return;
10@@ -24,9 +22,6 @@ void sbox_enter(const char *basedir)
11 unveil("/etc/ssl/cert.pem", "r");
12 unveil("/usr/share/zoneinfo", "r");
13
14- if (smail)
15- unveil("/usr/sbin/sendmail", "x");
16-
17 if (*address == '/')
18 unveil(address, "rwc");
19
20@@ -36,9 +31,6 @@ void sbox_enter(const char *basedir)
21
22 xs *p = xs_str_new("stdio rpath wpath cpath flock inet proc dns fattr");
23
24- if (smail)
25- p = xs_str_cat(p, " exec");
26-
27 if (*address == '/')
28 p = xs_str_cat(p, " unix");
29
30@@ -55,7 +47,7 @@ void sbox_enter(const char *basedir)
31 #include "landloc.h"
32
33 static
34-LL_BEGIN(sbox_enter_linux_, const char* basedir, const char *address, int smail) {
35+LL_BEGIN(sbox_enter_linux_, const char* basedir, const char *address, int smtp_port) {
36
37 const unsigned long long
38 rd = LANDLOCK_ACCESS_FS_READ_DIR,
39@@ -101,9 +93,6 @@ LL_BEGIN(sbox_enter_linux_, const char* basedir, const char *address, int smail)
40 LL_PATH(sdir, s);
41 }
42
43- if (smail && mtime("/usr/sbin/sendmail") > 0)
44- LL_PATH("/usr/sbin/sendmail", x);
45-
46 if (*address != '/') {
47 unsigned short listen_port = xs_number_get(xs_dict_get(srv_config, "port"));
48 LL_PORT(listen_port, LANDLOCK_ACCESS_NET_BIND_TCP_COMPAT);
49@@ -111,24 +100,34 @@ LL_BEGIN(sbox_enter_linux_, const char* basedir, const char *address, int smail)
50
51 LL_PORT(80, LANDLOCK_ACCESS_NET_CONNECT_TCP_COMPAT);
52 LL_PORT(443, LANDLOCK_ACCESS_NET_CONNECT_TCP_COMPAT);
53+ if (smtp_port > 0)
54+ LL_PORT((unsigned short)smtp_port, LANDLOCK_ACCESS_NET_CONNECT_TCP_COMPAT);
55
56 } LL_END
57
58 void sbox_enter(const char *basedir)
59 {
60+ const xs_val *v;
61+ const char *errstr;
62 const char *address = xs_dict_get(srv_config, "address");
63-
64- int smail = !xs_is_true(xs_dict_get(srv_config, "disable_email_notifications"));
65+ int smtp_port = -1;
66
67 if (xs_is_true(xs_dict_get(srv_config, "disable_sandbox"))) {
68 srv_debug(1, xs_dup("Linux sandbox disabled by admin"));
69 return;
70 }
71
72- if (sbox_enter_linux_(basedir, address, smail) == 0)
73+ if ((v = xs_dict_get(srv_config, "email_notifications")) &&
74+ (v = xs_dict_get(v, "url"))) {
75+ smtp_port = parse_port((const char *)v, &errstr);
76+ if (errstr)
77+ srv_debug(0, xs_fmt("Couldn't determine port from '%s': %s", (const char *)v, errstr));
78+ }
79+
80+ if (sbox_enter_linux_(basedir, address, smtp_port) == 0)
81 srv_debug(1, xs_dup("Linux sandbox enabled"));
82 else
83- srv_debug(1, xs_dup("Linux sandbox failed"));
84+ srv_debug(0, xs_dup("Linux sandbox failed"));
85 }
86
87 #else /* defined(WITH_LINUX_SANDBOX) */
M
snac.h
+1,
-0
1@@ -434,6 +434,7 @@ void import_blocked_accounts_csv(snac *user, const char *fn);
2 void import_following_accounts_csv(snac *user, const char *fn);
3 void import_list_csv(snac *user, const char *fn);
4 void import_csv(snac *user);
5+int parse_port(const char *url, const char **errstr);
6
7 typedef enum {
8 #define HTTP_STATUS(code, name, text) HTTP_STATUS_ ## name = code,
+24,
-0
1@@ -0,0 +1,24 @@
2+/* snac - A simple, minimalistic ActivityPub instance */
3+/* copyright (c) 2022 - 2025 grunfink et al. / MIT license */
4+
5+#define XS_IMPLEMENTATION
6+#include "../xs.h"
7+#include "../xs_curl.h"
8+
9+#define FROM "<snac-smtp-test@locahost>"
10+
11+int main(void) {
12+ xs *to = xs_fmt("<%s@localhost>", getenv("USER")),
13+ *body = xs_fmt(""
14+ "To: %s \r\n"
15+ "From: " FROM "\r\n"
16+ "Subject: snac smtp test\r\n"
17+ "\r\n"
18+ "If you read this as an email, it probably worked!\r\n",
19+ to);
20+
21+ return xs_smtp_request("smtp://localhost", NULL, NULL,
22+ FROM,
23+ to,
24+ body, 0);
25+}
M
utils.c
+57,
-0
1@@ -912,3 +912,60 @@ void import_csv(snac *user)
2 else
3 snac_log(user, xs_fmt("Cannot open file %s", fn));
4 }
5+
6+static const struct {
7+ const char *proto;
8+ unsigned short default_port;
9+} FALLBACK_PORTS[] = {
10+ /* caution: https > http, smpts > smtp */
11+ {"https", 443},
12+ {"http", 80},
13+ {"smtps", 465},
14+ {"smtp", 25}
15+};
16+
17+int parse_port(const char *url, const char **errstr)
18+{
19+ const char *col, *rcol;
20+ int tmp, ret = -1;
21+
22+ if (errstr)
23+ *errstr = NULL;
24+
25+ if (!(col = strchr(url, ':'))) {
26+ if (errstr)
27+ *errstr = "bad url";
28+
29+ return -1;
30+ }
31+
32+ for (size_t i = 0; i < sizeof(FALLBACK_PORTS) / sizeof(*FALLBACK_PORTS); ++i) {
33+ if (memcmp(url, FALLBACK_PORTS[i].proto, strlen(FALLBACK_PORTS[i].proto)) == 0) {
34+ ret = FALLBACK_PORTS[i].default_port;
35+ break;
36+ }
37+ }
38+
39+ if (!(rcol = strchr(col + 1, ':')))
40+ rcol = col;
41+
42+ if (rcol) {
43+ tmp = atoi(rcol + 1);
44+ if (tmp == 0) {
45+ if (ret != -1)
46+ return ret;
47+
48+ if (errstr)
49+ *errstr = strerror(errno);
50+
51+ return -1;
52+ }
53+
54+ return tmp;
55+ }
56+
57+ if (errstr)
58+ *errstr = "unknown protocol";
59+
60+ return -1;
61+}
+46,
-0
1@@ -9,6 +9,10 @@ xs_dict *xs_http_request(const char *method, const char *url,
2 const xs_str *body, int b_size, int *status,
3 xs_str **payload, int *p_size, int timeout);
4
5+int xs_smtp_request(const char *url, const char *user, const char *pass,
6+ const char *from, const char *to, const xs_str *body,
7+ int use_ssl);
8+
9 const char *xs_curl_strerr(int errnum);
10
11 #ifdef XS_IMPLEMENTATION
12@@ -196,6 +200,48 @@ xs_dict *xs_http_request(const char *method, const char *url,
13 return response;
14 }
15
16+int xs_smtp_request(const char *url, const char *user, const char *pass,
17+ const char *from, const char *to, const xs_str *body,
18+ int use_ssl)
19+{
20+ CURL *curl;
21+ CURLcode res = CURLE_OK;
22+ struct curl_slist *rcpt = NULL;
23+ struct _payload_data pd = {
24+ .data = (char *)body,
25+ .size = xs_size(body),
26+ .offset = 0
27+ };
28+
29+ curl = curl_easy_init();
30+
31+ curl_easy_setopt(curl, CURLOPT_URL, url);
32+ if (user && pass) {
33+ /* allow authless connections, to, e.g. localhost */
34+ curl_easy_setopt(curl, CURLOPT_USERNAME, user);
35+ curl_easy_setopt(curl, CURLOPT_PASSWORD, pass);
36+ }
37+
38+ if (use_ssl)
39+ curl_easy_setopt(curl, CURLOPT_USE_SSL, (long)CURLUSESSL_ALL);
40+
41+ curl_easy_setopt(curl, CURLOPT_MAIL_FROM, from);
42+
43+ rcpt = curl_slist_append(rcpt, to);
44+ curl_easy_setopt(curl, CURLOPT_MAIL_RCPT, rcpt);
45+
46+ curl_easy_setopt(curl, CURLOPT_READDATA, &pd);
47+ curl_easy_setopt(curl, CURLOPT_READFUNCTION, _post_callback);
48+ curl_easy_setopt(curl, CURLOPT_UPLOAD, 1L);
49+
50+ res = curl_easy_perform(curl);
51+
52+ curl_easy_cleanup(curl);
53+ curl_slist_free_all(rcpt);
54+
55+ return (int)res;
56+}
57+
58
59 const char *xs_curl_strerr(int errnum)
60 {