snac2

Fork of https://codeberg.org/grunfink/snac2
git clone https://git.inz.fi/snac2
Log | Files | Refs | README | LICENSE

commit 169c5c8c070c527e7462e6ac86b916e94649c1ac
parent 2e39e4e3c36dc23598173f3b6fe64a103574d089
Author: grunfink <grunfink@noreply.codeberg.org>
Date:   Sun, 27 Apr 2025 03:11:57 +0000

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

Diffstat:
M.gitignore | 3++-
MMakefile | 10++++++++--
Mactivitypub.c | 51+++++++++++++++++++++------------------------------
Mdata.c | 2+-
Msandbox.c | 31+++++++++++++++----------------
Msnac.h | 1+
Atests/smtp.c | 25+++++++++++++++++++++++++
Mutils.c | 57+++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mxs_curl.h | 46++++++++++++++++++++++++++++++++++++++++++++++
9 files changed, 176 insertions(+), 50 deletions(-)

diff --git a/.gitignore b/.gitignore @@ -1,2 +1,3 @@ -*.o +**/*.o +tests/smtp snac diff --git a/Makefile b/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 diff --git a/activitypub.c b/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)) diff --git a/data.c b/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); diff --git a/sandbox.c b/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) */ diff --git a/snac.h b/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, diff --git a/tests/smtp.c b/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); +} +\ No newline at end of file diff --git a/utils.c b/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; +} diff --git a/xs_curl.h b/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) {