Переглянути джерело

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
grunfink 1 рік тому
батько
коміт
169c5c8c07
9 змінених файлів з 175 додано та 50 видалено
  1. 2 1
      .gitignore
  2. 8 2
      Makefile
  3. 21 30
      activitypub.c
  4. 1 1
      data.c
  5. 15 16
      sandbox.c
  6. 1 0
      snac.h
  7. 24 0
      tests/smtp.c
  8. 57 0
      utils.c
  9. 46 0
      xs_curl.h

+ 2 - 1
.gitignore

@@ -1,2 +1,3 @@
-*.o
+**/*.o
+tests/smtp
 snac

+ 8 - 2
Makefile

@@ -8,11 +8,16 @@ snac: snac.o main.o sandbox.o data.o http.o httpd.o webfinger.o \
     activitypub.o html.o utils.o format.o upgrade.o mastoapi.o
 	$(CC) $(CFLAGS) -L$(PREFIX)/lib *.o -lcurl -lcrypto $(LDFLAGS) -pthread -o $@
 
+test: tests/smtp
+
+tests/smtp: tests/smtp.o
+	$(CC) $(CFLAGS) -L$(PREFIX)/lib $< -lcurl $(LDFLAGS) -o $@
+
 .c.o:
-	$(CC) $(CFLAGS) $(CPPFLAGS) -I$(PREFIX)/include -c $<
+	$(CC) $(CFLAGS) $(CPPFLAGS) -I$(PREFIX)/include -c $< -o $@
 
 clean:
-	rm -rf *.o *.core snac makefile.depend
+	rm -rf *.o tests/*.o tests/smtp *.core snac makefile.depend
 
 dep:
 	$(CC) -I$(PREFIX)/include -MM *.c > makefile.depend
@@ -72,3 +77,4 @@ utils.o: utils.c xs.h xs_io.h xs_json.h xs_time.h xs_openssl.h \
  xs_random.h xs_glob.h xs_curl.h xs_regex.h snac.h http_codes.h
 webfinger.o: webfinger.c xs.h xs_json.h xs_curl.h xs_mime.h snac.h \
  http_codes.h
+tests/smtp.o: tests/smtp.c xs.h xs_curl.h

+ 21 - 30
activitypub.c

@@ -1044,17 +1044,20 @@ void notify(snac *snac, const char *type, const char *utype, const char *actor,
 
         xs *subject = xs_fmt("snac notify for @%s@%s",
                     xs_dict_get(snac->config, "uid"), xs_dict_get(srv_config, "host"));
-        xs *from    = xs_fmt("snac-daemon <snac-daemon@%s>", xs_dict_get(srv_config, "host"));
+        xs *from    = xs_fmt("<snac-daemon@%s>", xs_dict_get(srv_config, "host"));
         xs *header  = xs_fmt(
-                    "From: %s\n"
+                    "From: snac-daemon %s\n"
                     "To: %s\n"
                     "Subject: %s\n"
                     "\n",
                     from, email, subject);
 
-        xs *email_body = xs_fmt("%s%s", header, body);
+        xs *mailinfo = xs_dict_new();
+        mailinfo = xs_dict_append(mailinfo, "from", from);
+        mailinfo = xs_dict_append(mailinfo, "to", email);
+        mailinfo = xs_dict_append(mailinfo, "body", xs_fmt("%s%s", header, body));
 
-        enqueue_email(email_body, 0);
+        enqueue_email(mailinfo, 0);
     }
 
     /* telegram */
@@ -2563,32 +2566,20 @@ int process_input_message(snac *snac, const xs_dict *msg, const xs_dict *req)
 }
 
 
-int send_email(const char *msg)
-/* invoke sendmail with email headers and body in msg */
+int send_email(const xs_dict *mailinfo)
+/* invoke curl */
 {
-    FILE *f;
-    int status;
-    int fds[2];
-    pid_t pid;
-    if (pipe(fds) == -1) return -1;
-    pid = vfork();
-    if (pid == -1) return -1;
-    else if (pid == 0) {
-        dup2(fds[0], 0);
-        close(fds[0]);
-        close(fds[1]);
-        execl("/usr/sbin/sendmail", "sendmail", "-t", (char *) NULL);
-        _exit(1);
-    }
-    close(fds[0]);
-    if ((f = fdopen(fds[1], "w")) == NULL) {
-        close(fds[1]);
-        return -1;
-    }
-    fprintf(f, "%s\n", msg);
-    fclose(f);
-    if (waitpid(pid, &status, 0) == -1) return -1;
-    return status;
+    const xs_dict *smtp_cfg = xs_dict_get(srv_config, "email_notifications");
+    const char 
+        *url  = xs_dict_get_def(smtp_cfg, "url", "smtp://localhost"),
+        *user = xs_dict_get(smtp_cfg, "username"),
+        *pass = xs_dict_get(smtp_cfg, "password"),
+        *from = xs_dict_get(mailinfo, "from"),
+        *to   = xs_dict_get(mailinfo, "to"),
+        *body = xs_dict_get(mailinfo, "body");
+    int smtp_port = parse_port(url, NULL);
+
+    return xs_smtp_request(url, user, pass, from, to, body, smtp_port == 465 || smtp_port == 587);
 }
 
 
@@ -2861,7 +2852,7 @@ void process_queue_item(xs_dict *q_item)
     else
     if (strcmp(type, "email") == 0) {
         /* send this email */
-        const xs_str *msg = xs_dict_get(q_item, "message");
+        const xs_dict *msg = xs_dict_get(q_item, "message");
         int retries = xs_number_get(xs_dict_get(q_item, "retries"));
 
         if (!send_email(msg))

+ 1 - 1
data.c

@@ -3357,7 +3357,7 @@ void enqueue_output_by_actor(snac *snac, const xs_dict *msg,
 }
 
 
-void enqueue_email(const xs_str *msg, int retries)
+void enqueue_email(const xs_dict *msg, int retries)
 /* enqueues an email message to be sent */
 {
     xs *qmsg   = _new_qmsg("email", msg, retries);

+ 15 - 16
sandbox.c

@@ -8,8 +8,6 @@ void sbox_enter(const char *basedir)
 {
     const char *address = xs_dict_get(srv_config, "address");
 
-    int smail = !xs_is_true(xs_dict_get(srv_config, "disable_email_notifications"));
-
     if (xs_is_true(xs_dict_get(srv_config, "disable_openbsd_security"))) {
         srv_log(xs_dup("OpenBSD security disabled by admin"));
         return;
@@ -24,9 +22,6 @@ void sbox_enter(const char *basedir)
     unveil("/etc/ssl/cert.pem",    "r");
     unveil("/usr/share/zoneinfo",  "r");
 
-    if (smail)
-        unveil("/usr/sbin/sendmail",   "x");
-
     if (*address == '/')
         unveil(address, "rwc");
 
@@ -36,9 +31,6 @@ void sbox_enter(const char *basedir)
 
     xs *p = xs_str_new("stdio rpath wpath cpath flock inet proc dns fattr");
 
-    if (smail)
-        p = xs_str_cat(p, " exec");
-
     if (*address == '/')
         p = xs_str_cat(p, " unix");
 
@@ -55,7 +47,7 @@ void sbox_enter(const char *basedir)
 #include "landloc.h"
 
 static
-LL_BEGIN(sbox_enter_linux_, const char* basedir, const char *address, int smail) {
+LL_BEGIN(sbox_enter_linux_, const char* basedir, const char *address, int smtp_port) {
 
     const unsigned long long
         rd = LANDLOCK_ACCESS_FS_READ_DIR,
@@ -101,9 +93,6 @@ LL_BEGIN(sbox_enter_linux_, const char* basedir, const char *address, int smail)
         LL_PATH(sdir, s);
     }
 
-    if (smail && mtime("/usr/sbin/sendmail") > 0)
-        LL_PATH("/usr/sbin/sendmail", x);
-
     if (*address != '/') {
         unsigned short listen_port = xs_number_get(xs_dict_get(srv_config, "port"));
         LL_PORT(listen_port, LANDLOCK_ACCESS_NET_BIND_TCP_COMPAT);
@@ -111,24 +100,34 @@ LL_BEGIN(sbox_enter_linux_, const char* basedir, const char *address, int smail)
 
     LL_PORT(80,  LANDLOCK_ACCESS_NET_CONNECT_TCP_COMPAT);
     LL_PORT(443, LANDLOCK_ACCESS_NET_CONNECT_TCP_COMPAT);
+    if (smtp_port > 0)
+        LL_PORT((unsigned short)smtp_port, LANDLOCK_ACCESS_NET_CONNECT_TCP_COMPAT);
 
 } LL_END
 
 void sbox_enter(const char *basedir)
 {
+    const xs_val *v;
+    const char *errstr;
     const char *address = xs_dict_get(srv_config, "address");
-
-    int smail = !xs_is_true(xs_dict_get(srv_config, "disable_email_notifications"));
+    int smtp_port = -1;
 
     if (xs_is_true(xs_dict_get(srv_config, "disable_sandbox"))) {
         srv_debug(1, xs_dup("Linux sandbox disabled by admin"));
         return;
     }
 
-    if (sbox_enter_linux_(basedir, address, smail) == 0)
+    if ((v = xs_dict_get(srv_config, "email_notifications")) && 
+        (v = xs_dict_get(v, "url"))) {
+        smtp_port = parse_port((const char *)v, &errstr);
+        if (errstr)
+            srv_debug(0, xs_fmt("Couldn't determine port from '%s': %s", (const char *)v, errstr));
+    }
+
+    if (sbox_enter_linux_(basedir, address, smtp_port) == 0)
         srv_debug(1, xs_dup("Linux sandbox enabled"));
     else
-        srv_debug(1, xs_dup("Linux sandbox failed"));
+        srv_debug(0, xs_dup("Linux sandbox failed"));
 }
 
 #else /* defined(WITH_LINUX_SANDBOX) */

+ 1 - 0
snac.h

@@ -434,6 +434,7 @@ void import_blocked_accounts_csv(snac *user, const char *fn);
 void import_following_accounts_csv(snac *user, const char *fn);
 void import_list_csv(snac *user, const char *fn);
 void import_csv(snac *user);
+int parse_port(const char *url, const char **errstr);
 
 typedef enum {
 #define HTTP_STATUS(code, name, text) HTTP_STATUS_ ## name = code,

+ 24 - 0
tests/smtp.c

@@ -0,0 +1,24 @@
+/* snac - A simple, minimalistic ActivityPub instance */
+/* copyright (c) 2022 - 2025 grunfink et al. / MIT license */
+
+#define XS_IMPLEMENTATION
+#include "../xs.h"
+#include "../xs_curl.h"
+
+#define FROM "<snac-smtp-test@locahost>"
+
+int main(void) {
+    xs *to   = xs_fmt("<%s@localhost>", getenv("USER")),
+       *body = xs_fmt(""
+        "To: %s \r\n"
+        "From: " FROM "\r\n"
+        "Subject: snac smtp test\r\n"
+        "\r\n"
+        "If you read this as an email, it probably worked!\r\n",
+        to);
+
+    return xs_smtp_request("smtp://localhost", NULL, NULL, 
+        FROM,
+        to,
+        body, 0);
+}

+ 57 - 0
utils.c

@@ -912,3 +912,60 @@ void import_csv(snac *user)
     else
         snac_log(user, xs_fmt("Cannot open file %s", fn));
 }
+
+static const struct {
+	const char *proto;
+	unsigned short default_port;
+} FALLBACK_PORTS[] = {
+	/* caution: https > http, smpts > smtp */
+	{"https", 443},
+	{"http",  80},
+    {"smtps", 465},
+	{"smtp", 25}
+};
+
+int parse_port(const char *url, const char **errstr)
+{
+	const char *col, *rcol;
+	int tmp, ret = -1;
+
+    if (errstr)
+	    *errstr = NULL;
+
+	if (!(col = strchr(url, ':'))) {
+        if (errstr)
+		    *errstr = "bad url";
+
+		return -1;
+	}
+	
+	for (size_t i = 0; i < sizeof(FALLBACK_PORTS) / sizeof(*FALLBACK_PORTS); ++i) {
+		if (memcmp(url, FALLBACK_PORTS[i].proto, strlen(FALLBACK_PORTS[i].proto)) == 0) {
+			ret = FALLBACK_PORTS[i].default_port;
+			break;
+		}
+	}
+
+	if (!(rcol = strchr(col + 1, ':')))
+		rcol = col;
+
+	if (rcol) {
+		tmp = atoi(rcol + 1);
+        if (tmp == 0) {
+            if (ret != -1) 
+                return ret;
+            
+            if (errstr)
+                *errstr = strerror(errno);
+
+            return -1;
+        }
+		
+		return tmp;
+	}
+
+    if (errstr)
+	    *errstr = "unknown protocol";
+
+	return -1;
+}

+ 46 - 0
xs_curl.h

@@ -9,6 +9,10 @@ xs_dict *xs_http_request(const char *method, const char *url,
                         const xs_str *body, int b_size, int *status,
                         xs_str **payload, int *p_size, int timeout);
 
+int xs_smtp_request(const char *url, const char *user, const char *pass,
+                   const char *from, const char *to, const xs_str *body,
+                   int use_ssl);
+
 const char *xs_curl_strerr(int errnum);
 
 #ifdef XS_IMPLEMENTATION
@@ -196,6 +200,48 @@ xs_dict *xs_http_request(const char *method, const char *url,
     return response;
 }
 
+int xs_smtp_request(const char *url, const char *user, const char *pass,
+                   const char *from, const char *to, const xs_str *body,
+                   int use_ssl)
+{
+    CURL *curl;
+    CURLcode res = CURLE_OK;
+    struct curl_slist *rcpt = NULL;
+    struct _payload_data pd = {
+        .data = (char *)body,
+        .size = xs_size(body),
+        .offset = 0
+    };
+
+    curl = curl_easy_init();
+
+    curl_easy_setopt(curl, CURLOPT_URL, url);
+    if (user && pass) {
+        /* allow authless connections, to, e.g. localhost */
+        curl_easy_setopt(curl, CURLOPT_USERNAME, user);
+        curl_easy_setopt(curl, CURLOPT_PASSWORD, pass);
+    }
+
+    if (use_ssl)
+        curl_easy_setopt(curl, CURLOPT_USE_SSL, (long)CURLUSESSL_ALL);
+    
+    curl_easy_setopt(curl, CURLOPT_MAIL_FROM, from);
+
+    rcpt = curl_slist_append(rcpt, to);
+    curl_easy_setopt(curl, CURLOPT_MAIL_RCPT, rcpt);
+
+    curl_easy_setopt(curl, CURLOPT_READDATA, &pd);
+    curl_easy_setopt(curl, CURLOPT_READFUNCTION, _post_callback);
+    curl_easy_setopt(curl, CURLOPT_UPLOAD, 1L);
+
+    res = curl_easy_perform(curl);
+    
+    curl_easy_cleanup(curl);
+    curl_slist_free_all(rcpt);
+
+    return (int)res;
+}
+
 
 const char *xs_curl_strerr(int errnum)
 {