From c8b250e9b0aef8472da48173dadaf6dea0ed0d6b Mon Sep 17 00:00:00 2001 From: Mario Fetka Date: Fri, 24 Jul 2026 11:05:19 +0200 Subject: [PATCH] Implement durable C local mail submission --- cmake/Directories.cmake | 1 + config.h.cmake | 1 + contrib/testing/runtime-path-check.sh | 1 + debian/bongo.postinst | 13 +- debian/tests/installed-smoke | 5 + include/bongomaildrop.h | 47 ++ init/bongo.sysusers.in | 2 + init/bongo.tmpfiles.in | 1 + man/CMakeLists.txt | 4 + man/bongo-sendmail.1 | 31 +- man/bongopostdrop.8 | 31 + src/agents/queue/queue.c | 217 ++++++ src/apps/sendmail/CMakeLists.txt | 39 +- src/apps/sendmail/bongo-sendmail.py.in | 33 - src/apps/sendmail/postdrop.c | 162 +++++ src/apps/sendmail/sendmail.c | 799 ++++++++++++--------- src/apps/sendmail/sendmail.h | 49 -- src/apps/sendmail/tests/sendmail-c-test.py | 110 +++ src/apps/sendmail/tests/test_sendmail.py | 125 ---- src/libs/python/bongo/sendmail.py | 267 ------- src/libs/util/CMakeLists.txt | 5 + src/libs/util/maildrop.c | 257 +++++++ src/libs/util/tests/maildrop-test.c | 67 ++ 23 files changed, 1457 insertions(+), 810 deletions(-) create mode 100644 include/bongomaildrop.h create mode 100644 man/bongopostdrop.8 delete mode 100644 src/apps/sendmail/bongo-sendmail.py.in create mode 100644 src/apps/sendmail/postdrop.c delete mode 100644 src/apps/sendmail/sendmail.h create mode 100644 src/apps/sendmail/tests/sendmail-c-test.py delete mode 100644 src/apps/sendmail/tests/test_sendmail.py delete mode 100644 src/libs/python/bongo/sendmail.py create mode 100644 src/libs/util/maildrop.c create mode 100644 src/libs/util/tests/maildrop-test.c diff --git a/cmake/Directories.cmake b/cmake/Directories.cmake index 2c9eb20..26c491c 100644 --- a/cmake/Directories.cmake +++ b/cmake/Directories.cmake @@ -63,6 +63,7 @@ set(XPL_DEFAULT_WORK_DIR ${XPL_DEFAULT_STATE_DIR}/work) set(XPL_DEFAULT_CACHE_DIR ${XPL_DEFAULT_STATE_DIR}/cache) set(XPL_DEFAULT_SCMS_DIR ${XPL_DEFAULT_STATE_DIR}/scms) set(XPL_DEFAULT_SPOOL_DIR ${XPL_DEFAULT_STATE_DIR}/spool) +set(XPL_DEFAULT_MAILDROP_DIR ${XPL_DEFAULT_STATE_DIR}/maildrop) set(XPL_DEFAULT_STORE_SYSTEM_DIR ${XPL_DEFAULT_STATE_DIR}/system) set(XPL_DEFAULT_SYSTEM_DIR ${XPL_DEFAULT_STATE_DIR}/system) set(XPL_DEFAULT_MAIL_DIR ${XPL_DEFAULT_STATE_DIR}/users) diff --git a/config.h.cmake b/config.h.cmake index f136b07..9792d67 100644 --- a/config.h.cmake +++ b/config.h.cmake @@ -52,6 +52,7 @@ #cmakedefine XPL_DEFAULT_LIB_DIR "@XPL_DEFAULT_LIB_DIR@" #cmakedefine XPL_DEFAULT_CONF_DIR "@XPL_DEFAULT_CONF_DIR@" #cmakedefine XPL_DEFAULT_CONFIG_DIR "@XPL_DEFAULT_CONFIG_DIR@" +#cmakedefine XPL_DEFAULT_MAILDROP_DIR "@XPL_DEFAULT_MAILDROP_DIR@" #cmakedefine XPL_DEFAULT_ALIASES_DIR "@XPL_DEFAULT_ALIASES_DIR@" #cmakedefine XPL_DEFAULT_SSL_DIR "@XPL_DEFAULT_SSL_DIR@" #cmakedefine XPL_DEFAULT_DKIM_DIR "@XPL_DEFAULT_DKIM_DIR@" diff --git a/contrib/testing/runtime-path-check.sh b/contrib/testing/runtime-path-check.sh index 155dab7..e86a668 100755 --- a/contrib/testing/runtime-path-check.sh +++ b/contrib/testing/runtime-path-check.sh @@ -95,6 +95,7 @@ assert_path "$RUNTIME_DIR" 750 bongo:bongo assert_path "$STATE_DIR" 750 bongo:bongo assert_path "$STATE_DIR/dbf" 750 bongo:bongo assert_path "$STATE_DIR/dbf/cookies" 700 bongo:bongo +assert_path "$STATE_DIR/maildrop" 1730 bongo:bongopostdrop assert_path /etc/bongo/ssl.d 750 root:bongo echo "PASS tmpfiles recreated volatile and persistent directory ownership" diff --git a/debian/bongo.postinst b/debian/bongo.postinst index 4b7a63f..36c8b60 100755 --- a/debian/bongo.postinst +++ b/debian/bongo.postinst @@ -4,9 +4,18 @@ set -e if [ "$1" = configure ]; then if command -v systemd-sysusers >/dev/null 2>&1; then systemd-sysusers bongo-user.conf - elif ! getent passwd bongo >/dev/null 2>&1; then - adduser --system --group --home /var/lib/bongo --no-create-home bongo + else + if ! getent group bongopostdrop >/dev/null 2>&1; then + addgroup --system bongopostdrop + fi + if ! getent passwd bongo >/dev/null 2>&1; then + adduser --system --group --home /var/lib/bongo \ + --no-create-home bongo + fi + adduser bongo bongopostdrop fi + chown root:bongopostdrop /usr/sbin/bongopostdrop + chmod 2755 /usr/sbin/bongopostdrop fi #DEBHELPER# diff --git a/debian/tests/installed-smoke b/debian/tests/installed-smoke index b993c68..79cbc9a 100755 --- a/debian/tests/installed-smoke +++ b/debian/tests/installed-smoke @@ -4,6 +4,11 @@ set -eu test -x /usr/sbin/bongo-manager test -x /usr/sbin/bongo-admin test -x /usr/sbin/bongo-config +test -x /usr/sbin/bongo-sendmail +test -x /usr/sbin/bongopostdrop +[ "$(stat -c '%a %U:%G' /usr/sbin/bongopostdrop)" = \ + "2755 root:bongopostdrop" ] +getent group bongopostdrop >/dev/null python3 -c 'import bongo, bongo_web, libbongo' test -f /lib/systemd/system/bongo.service || test -f /usr/lib/systemd/system/bongo.service diff --git a/include/bongomaildrop.h b/include/bongomaildrop.h new file mode 100644 index 0000000..b795d97 --- /dev/null +++ b/include/bongomaildrop.h @@ -0,0 +1,47 @@ +/**************************************************************************** + * + * Copyright (c) 2001 Novell, Inc. All Rights Reserved. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of version 2 of the GNU General Public License + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, contact Novell, Inc. + * + ****************************************************************************/ + +#ifndef BONGO_MAILDROP_H +#define BONGO_MAILDROP_H + +#include +#include +#include + +#include + +#define BONGO_MAILDROP_ADDRESS_LIMIT 998U +#define BONGO_MAILDROP_RECIPIENT_LIMIT 1000U +#define BONGO_MAILDROP_MESSAGE_LIMIT (256U * 1024U * 1024U) + +typedef struct { + uid_t uid; + char *sender; + GPtrArray *recipients; + unsigned char *message; + size_t message_length; +} BongoMaildropSubmission; + +int BongoMaildropAddressValid(const char *address); +int BongoMaildropWrite(FILE *stream, const BongoMaildropSubmission *submission); +int BongoMaildropRead(FILE *stream, size_t message_limit, + unsigned int recipient_limit, + BongoMaildropSubmission *submission); +void BongoMaildropSubmissionClear(BongoMaildropSubmission *submission); + +#endif diff --git a/init/bongo.sysusers.in b/init/bongo.sysusers.in index f6007d0..2c111f2 100644 --- a/init/bongo.sysusers.in +++ b/init/bongo.sysusers.in @@ -19,4 +19,6 @@ # * # ****************************************************************************/ +g bongopostdrop - - u @BONGO_USER@ - "Bongo mail and calendar server" @XPL_DEFAULT_STATE_DIR@ - +m @BONGO_USER@ bongopostdrop diff --git a/init/bongo.tmpfiles.in b/init/bongo.tmpfiles.in index dbf5aba..fce8313 100644 --- a/init/bongo.tmpfiles.in +++ b/init/bongo.tmpfiles.in @@ -27,6 +27,7 @@ d @XPL_DEFAULT_CACHE_DIR@ 0750 @BONGO_USER@ @BONGO_USER@ - d @XPL_DEFAULT_CACHE_DIR@/mta-sts 0750 @BONGO_USER@ @BONGO_USER@ - d @XPL_DEFAULT_SCMS_DIR@ 0750 @BONGO_USER@ @BONGO_USER@ - d @XPL_DEFAULT_SPOOL_DIR@ 0750 @BONGO_USER@ @BONGO_USER@ - +d @XPL_DEFAULT_MAILDROP_DIR@ 1730 @BONGO_USER@ bongopostdrop - d @XPL_DEFAULT_SYSTEM_DIR@ 0750 @BONGO_USER@ @BONGO_USER@ - d @XPL_DEFAULT_MAIL_DIR@ 0750 @BONGO_USER@ @BONGO_USER@ - d @XPL_DEFAULT_COOKIE_DIR@ 0700 @BONGO_USER@ @BONGO_USER@ - diff --git a/man/CMakeLists.txt b/man/CMakeLists.txt index 9291887..e803ef4 100644 --- a/man/CMakeLists.txt +++ b/man/CMakeLists.txt @@ -32,11 +32,15 @@ foreach(page IN LISTS BONGO_MAN1_PAGES) endforeach() configure_file(bongoagents.8 "${CMAKE_CURRENT_BINARY_DIR}/bongoagents.8" @ONLY) +configure_file(bongopostdrop.8 + "${CMAKE_CURRENT_BINARY_DIR}/bongopostdrop.8" @ONLY) install(FILES ${BONGO_CONFIGURED_MAN1_PAGES} DESTINATION ${MAN_INSTALL_DIR}/man1) install(FILES "${CMAKE_CURRENT_BINARY_DIR}/bongoagents.8" DESTINATION ${MAN_INSTALL_DIR}/man8) +install(FILES "${CMAKE_CURRENT_BINARY_DIR}/bongopostdrop.8" + DESTINATION ${MAN_INSTALL_DIR}/man8) foreach(agent IN LISTS BONGO_AGENT_MAN8_NAMES) install(FILES "${CMAKE_CURRENT_BINARY_DIR}/bongoagents.8" DESTINATION ${MAN_INSTALL_DIR}/man8 diff --git a/man/bongo-sendmail.1 b/man/bongo-sendmail.1 index 61388aa..57049a4 100644 --- a/man/bongo-sendmail.1 +++ b/man/bongo-sendmail.1 @@ -9,7 +9,12 @@ bongo-sendmail \(em submit a message to the local Bongo Queue .PP The .B bongo-sendmail -command attempts to implement an LSB-compliant sendmail interface direct to the Bongo Queue agent. Most useful options are implemented; some are not. +command provides the common sendmail-compatible local submission interface. +It passes a bounded and validated request to +.BR bongopostdrop (8), +which commits it atomically to Bongo's protected local maildrop. The Queue +agent then imports the message through its normal incoming path. No Bongo +system credential and no SMTP listener are required. .PP .B bongo-sendmail will continue to read input from standard input until it sees either a dot alone on a line, or reaches the end of file. @@ -17,12 +22,17 @@ will continue to read input from standard input until it sees either a dot alone .PP A summary of options is included below. .TP -.BI \-r " mail_from" -Set the From: header of the mail being sent. +.BI \-r " envelope_from" +Set the sender address on the mail envelope. This is an alias for +.BR \-f . .TP -.BI \-F " env_from" +.BI \-f " envelope_from" Set the sender address on the mail envelope. .TP +.BI \-F " full_name" +Supply the display name used for a generated From header when the message has +no From header. +.TP .BR \-i Read the entire standard input until the end of the file. .TP @@ -33,7 +43,18 @@ Read from standard input until a dot appears alone on a line. -oi is equivalent to -i; other arguments are ignored. .TP .BR \-t -Extract recipient addresses from the mail body. +Extract recipient addresses from the To, Cc, and Bcc headers. Bcc headers +and their continuations are removed from the stored message. +.SH "SECURITY" +The frontend never reads Bongo's Queue credential. The installed +.B bongopostdrop +helper is set-group-ID to the dedicated +.B bongopostdrop +group and has access only to the local maildrop directory. Maildrop records +are treated as hostile input and validated again by the Queue agent. +.SH "SEE ALSO" +.BR bongopostdrop (8), +.BR bongoqueue (8) .SH "AUTHOR" .PP This manual page was originally written by Alex Hudson for the diff --git a/man/bongopostdrop.8 b/man/bongopostdrop.8 new file mode 100644 index 0000000..f43dff2 --- /dev/null +++ b/man/bongopostdrop.8 @@ -0,0 +1,31 @@ +.TH BONGOPOSTDROP 8 "July 2026" "Bongo @BONGO_V_STR@" "Bongo local maildrop" +.SH NAME +bongopostdrop \(em atomically commit a local Bongo submission +.SH SYNOPSIS +.B bongopostdrop +.SH DESCRIPTION +.B bongopostdrop +is the restricted privilege-separation helper used by +.BR bongo-sendmail (1). +It accepts only Bongo's length-delimited local-submission protocol on standard +input, validates all envelope fields and bounds, and atomically commits the +record to the protected Bongo maildrop. +.PP +This command is not a general-purpose Queue client. It does not read the +Bongo system credential, contact SMTP, or accept command-line paths. The +Bongo Queue agent claims and revalidates each record before creating its +private Queue control and data files. +.SH SECURITY +The installed executable must be owned by +.B root:bongopostdrop +and have mode 2755. The maildrop directory is owned by the Bongo service +account and the +.B bongopostdrop +group with mode 1730. Do not grant users direct membership in that group. +.SH FILES +.TP +.I @XPL_DEFAULT_MAILDROP_DIR@ +Protected local maildrop. +.SH SEE ALSO +.BR bongo-sendmail (1), +.BR bongoqueue (8) diff --git a/src/agents/queue/queue.c b/src/agents/queue/queue.c index 46390d8..97d7529 100644 --- a/src/agents/queue/queue.c +++ b/src/agents/queue/queue.c @@ -23,8 +23,12 @@ #include #include +#include +#include #include #include +#include +#include #include "queue.h" #include "collected-count.h" @@ -36,6 +40,7 @@ #include "spool-file.h" #include "store-delivery-format.h" #include "delivery-policy.h" +#include static BOOL QueueFormatBuffer(char *destination, size_t destinationSize, @@ -105,6 +110,216 @@ uint32_t IntHash(const void *key); int IntCmp(const void *key1, const void *key2); static int HandleDSN(FILE *data, FILE *control); +static BOOL ProcessQueueEntry(char *entryIn); + +static void +StartImportedQueueEntry(unsigned long id) +{ + char entry[16]; + int result; + + if (!QueueFormat(entry, "%03d%07lx", Q_INCOMING, id)) return; + if (XplSafeRead(Queue.activeWorkers) < Conf.maxConcurrentWorkers) { + XplThreadID thread_id; + XplBeginCountedThread(&thread_id, ProcessQueueEntry, STACKSPACE_Q, + MemStrdup(entry), result, + Queue.activeWorkers); + } else { + Queue.restartNeeded = TRUE; + } +} + +/* + * Convert one hostile, user-owned maildrop record into private Queue files. + * The record has already been renamed to an unpredictable pickup name, so a + * submitter cannot reach it through the maildrop directory while it is read. + */ +static int +ImportMaildropFile(const char *path) +{ + BongoMaildropSubmission submission = { 0 }; + struct stat details; + char control_work[XPL_MAX_PATH + 1]; + char control_path[XPL_MAX_PATH + 1]; + char data_work[XPL_MAX_PATH + 1]; + char data_path[XPL_MAX_PATH + 1]; + char date[80]; + FILE *drop = NULL; + FILE *control = NULL; + FILE *data = NULL; + unsigned long id; + unsigned int index; + int invalid = 0; + int saved_error = 0; + + drop = fopen(path, "rb"); + if (drop == NULL) return -1; + if (fstat(fileno(drop), &details) != 0 || !S_ISREG(details.st_mode) || + details.st_nlink != 1 || + details.st_size <= 0 || + (uintmax_t)details.st_size > + (uintmax_t)BONGO_MAILDROP_MESSAGE_LIMIT + + ((uintmax_t)BONGO_MAILDROP_RECIPIENT_LIMIT * + (BONGO_MAILDROP_ADDRESS_LIMIT + 4U)) + 4096U || + BongoMaildropRead(drop, BONGO_MAILDROP_MESSAGE_LIMIT, + BONGO_MAILDROP_RECIPIENT_LIMIT, + &submission) != 0 || + details.st_uid != submission.uid) { + invalid = errno == EIO || errno == ENOMEM ? 0 : 1; + saved_error = errno != 0 ? errno : EINVAL; + fclose(drop); + BongoMaildropSubmissionClear(&submission); + errno = saved_error; + return invalid ? 1 : -1; + } + fclose(drop); + drop = NULL; + + XplMutexLock(Queue.queueIDLock); + id = Queue.queueID++ & ((1UL << 28) - 1UL); + XplMutexUnlock(Queue.queueIDLock); + if (!QueueFormat(control_work, "%s/c%07lx.local", + Conf.spoolPath, id) || + !QueueFormat(control_path, "%s/c%07lx.%03d", + Conf.spoolPath, id, Q_INCOMING) || + !QueueFormat(data_work, "%s/d%07lx.local", + Conf.spoolPath, id) || + !QueueFormat(data_path, "%s/d%07lx.msg", + Conf.spoolPath, id)) { + saved_error = ENAMETOOLONG; + goto fail; + } + control = fdopen(open(control_work, + O_WRONLY | O_CREAT | O_EXCL | O_NOFOLLOW, + 0600), "wb"); + if (control == NULL) { + saved_error = errno; + goto fail; + } + data = fdopen(open(data_work, + O_WRONLY | O_CREAT | O_EXCL | O_NOFOLLOW, + 0600), "wb"); + if (data == NULL) { + saved_error = errno; + goto fail; + } + MsgGetRFC822Date(-1, 0, date); + if (BongoQueueFilePrintf( + control, QUEUES_DATE"%" PRIdMAX "\r\n", + (intmax_t)time(NULL)) != 0 || + BongoQueueFilePrintf(control, QUEUES_FROM"%s -\r\n", + submission.sender[0] != '\0' ? + submission.sender : "-") != 0) { + saved_error = errno; + goto fail; + } + for (index = 0U; index < submission.recipients->len; index++) { + const char *recipient = + g_ptr_array_index(submission.recipients, index); + if (BongoQueueFilePrintf(control, QUEUES_RECIP_REMOTE"%s %s 2\r\n", + recipient, recipient) != 0) { + saved_error = errno; + goto fail; + } + } + if (BongoQueueFilePrintf( + data, + "Received: from local (uid %lu) by %s\r\n" + "\twith Bongo local submission; %s\r\n", + (unsigned long)submission.uid, BongoGlobals.hostname, date) != 0 || + BongoQueueFileWrite(data, submission.message, + submission.message_length) != 0 || + fsync(fileno(data)) != 0 || fsync(fileno(control)) != 0 || + BongoQueueFileFinish(&data) != 0 || + BongoQueueFileFinish(&control) != 0) { + saved_error = errno != 0 ? errno : EIO; + goto fail; + } + if (rename(data_work, data_path) != 0) { + saved_error = errno; + goto fail; + } + if (rename(control_work, control_path) != 0) { + saved_error = errno; + unlink(data_path); + goto fail; + } + XplSafeIncrement(Queue.queuedLocal); + StartImportedQueueEntry(id); + BongoMaildropSubmissionClear(&submission); + return 0; + +fail: + if (data != NULL) fclose(data); + if (control != NULL) fclose(control); + unlink(data_work); + unlink(control_work); + BongoMaildropSubmissionClear(&submission); + errno = saved_error != 0 ? saved_error : EIO; + return -1; +} + +static void +CheckMaildrop(void *unused) +{ + UNUSED_PARAMETER(unused); + XplRenameThread(XplGetThreadID(), "Local-maildrop Monitor"); + + while (BongoAgentGetState(&Agent.agent) < + BONGO_AGENT_STATE_STOPPING) { + DIR *directory = opendir(XPL_DEFAULT_MAILDROP_DIR); + struct dirent *entry; + + if (directory == NULL) { + Log(LOG_ERROR, "Cannot open local maildrop %s: %s", + XPL_DEFAULT_MAILDROP_DIR, strerror(errno)); + XplDelay(5000); + continue; + } + while ((entry = readdir(directory)) != NULL) { + char source[XPL_MAX_PATH + 1]; + char pickup[XPL_MAX_PATH + 1]; + const char *name = entry->d_name; + int result; + + if (strncmp(name, "drop-", 5U) == 0) { + if (!QueueFormat(source, "%s/%s", + XPL_DEFAULT_MAILDROP_DIR, name) || + !QueueFormat(pickup, "%s/.pickup-%s", + XPL_DEFAULT_MAILDROP_DIR, name + 5U)) + continue; + if (rename(source, pickup) != 0) { + if (errno != ENOENT) + Log(LOG_WARNING, + "Cannot claim local maildrop %s: %s", + source, strerror(errno)); + continue; + } + } else if (strncmp(name, ".pickup-", 8U) != 0) { + continue; + } else if (!QueueFormat(pickup, "%s/%s", + XPL_DEFAULT_MAILDROP_DIR, name)) { + continue; + } + result = ImportMaildropFile(pickup); + if (result == 0) { + if (unlink(pickup) != 0 && errno != ENOENT) + Log(LOG_WARNING, "Cannot remove local maildrop %s: %s", + pickup, strerror(errno)); + } else if (result > 0) { + Log(LOG_ERROR, "Discarding invalid local maildrop %s", + pickup); + unlink(pickup); + } else { + Log(LOG_WARNING, + "Deferring local maildrop %s: %s", + pickup, strerror(errno)); + } + } + closedir(directory); + XplDelay(1000); + } +} #define MAX_CHARS_IN_PDBSEARCH 512 @@ -2956,6 +3171,8 @@ CreateQueueThreads(BOOL failed) if (!Conf.debug) { XplBeginCountedThread(&id, CheckQueue, STACKSPACE_Q, NULL, i, Agent.activeThreads); } + XplBeginCountedThread(&id, CheckMaildrop, STACKSPACE_Q, NULL, i, + Agent.activeThreads); return(0); } diff --git a/src/apps/sendmail/CMakeLists.txt b/src/apps/sendmail/CMakeLists.txt index 3a899fe..2f21c2e 100644 --- a/src/apps/sendmail/CMakeLists.txt +++ b/src/apps/sendmail/CMakeLists.txt @@ -1,13 +1,36 @@ -configure_file(bongo-sendmail.py.in bongo-sendmail @ONLY) +StrictCompile() -install(PROGRAMS ${CMAKE_CURRENT_BINARY_DIR}/bongo-sendmail +add_executable(bongo-sendmail sendmail.c) +target_compile_definitions(bongo-sendmail PRIVATE + BONGO_POSTDROP_PATH="${SBIN_INSTALL_DIR}/bongopostdrop") +target_link_libraries(bongo-sendmail PRIVATE bongoutil GLib2::GLib2) + +add_executable(bongopostdrop postdrop.c) +target_compile_definitions(bongopostdrop PRIVATE + BONGO_MAILDROP_DIR="${XPL_DEFAULT_MAILDROP_DIR}") +target_link_libraries(bongopostdrop PRIVATE bongoutil GLib2::GLib2) + +install(TARGETS bongo-sendmail bongopostdrop DESTINATION ${SBIN_INSTALL_DIR}) if(BUILD_TESTING) - add_test(NAME sendmail-local-submission - COMMAND ${CMAKE_COMMAND} -E env - "PYTHONPATH=${CMAKE_SOURCE_DIR}/src/libs/python" - "PYTHONPYCACHEPREFIX=${CMAKE_CURRENT_BINARY_DIR}/pycache" - ${Python3_EXECUTABLE} -m unittest discover - -s ${CMAKE_CURRENT_SOURCE_DIR}/tests -v) + set(SENDMAIL_TEST_MAILDROP + "${CMAKE_CURRENT_BINARY_DIR}/maildrop-test") + add_executable(bongopostdrop-test-helper postdrop.c) + target_compile_definitions(bongopostdrop-test-helper PRIVATE + BONGO_MAILDROP_DIR="${SENDMAIL_TEST_MAILDROP}") + target_link_libraries(bongopostdrop-test-helper PRIVATE + bongoutil GLib2::GLib2) + + add_executable(bongo-sendmail-test-bin sendmail.c) + target_compile_definitions(bongo-sendmail-test-bin PRIVATE + BONGO_POSTDROP_PATH="${CMAKE_CURRENT_BINARY_DIR}/bongopostdrop-test-helper") + target_link_libraries(bongo-sendmail-test-bin PRIVATE + bongoutil GLib2::GLib2) + + add_test(NAME sendmail-c-local-submission + COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/tests/sendmail-c-test.py + $ + ${SENDMAIL_TEST_MAILDROP}) endif() diff --git a/src/apps/sendmail/bongo-sendmail.py.in b/src/apps/sendmail/bongo-sendmail.py.in deleted file mode 100644 index 402cd06..0000000 --- a/src/apps/sendmail/bongo-sendmail.py.in +++ /dev/null @@ -1,33 +0,0 @@ -#!/usr/bin/env python3 - -# /**************************************************************************** -# * -# * Copyright (c) 2001 Novell, Inc. All Rights Reserved. -# * -# * This program is free software; you can redistribute it and/or -# * modify it under the terms of version 2 of the GNU General Public License -# * as published by the Free Software Foundation. -# * -# * This program is distributed in the hope that it will be useful, -# * but WITHOUT ANY WARRANTY; without even the implied warranty of -# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# * GNU General Public License for more details. -# * -# * You should have received a copy of the GNU General Public License -# * along with this program; if not, contact Novell, Inc. -# * -# * To contact Novell about this file by physical or electronic mail, you -# * may find current contact information at www.novell.com. -# * -# ****************************************************************************/ - -import sys - -sys.path.insert(0, "@PYTHON_SITEPACKAGES_PATH@") -sys.path.insert(0, "@PYTHON_SITELIB_PATH@") - -from bongo.sendmail import main - - -if __name__ == "__main__": - raise SystemExit(main(config_dir="@XPL_DEFAULT_CONFIG_DIR@")) diff --git a/src/apps/sendmail/postdrop.c b/src/apps/sendmail/postdrop.c new file mode 100644 index 0000000..01d151e --- /dev/null +++ b/src/apps/sendmail/postdrop.c @@ -0,0 +1,162 @@ +/**************************************************************************** + * + * Copyright (c) 2001 Novell, Inc. All Rights Reserved. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of version 2 of the GNU General Public License + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, contact Novell, Inc. + * + ****************************************************************************/ + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifndef BONGO_MAILDROP_DIR +#define BONGO_MAILDROP_DIR "/var/lib/bongo/maildrop" +#endif + +static int MaildropDirectory = -1; +static char TemporaryName[96]; + +static void +Cleanup(int signal_number) +{ + if (MaildropDirectory >= 0 && TemporaryName[0] != '\0') + unlinkat(MaildropDirectory, TemporaryName, 0); + if (signal_number != 0) _exit(128 + signal_number); +} + +static int +InstallSignalHandlers(void) +{ + struct sigaction action; + int signals[] = { SIGHUP, SIGINT, SIGQUIT, SIGTERM }; + unsigned int index; + + memset(&action, 0, sizeof(action)); + action.sa_handler = Cleanup; + sigemptyset(&action.sa_mask); + for (index = 0U; index < sizeof(signals) / sizeof(signals[0]); index++) { + if (sigaction(signals[index], &action, NULL) != 0) return -1; + } + return 0; +} + +static int +RandomBytes(unsigned char *data, size_t length) +{ + size_t offset = 0U; + + while (offset < length) { + ssize_t count = getrandom(data + offset, length - offset, 0); + if (count > 0) { + offset += (size_t)count; + continue; + } + if (count < 0 && errno == EINTR) continue; + return -1; + } + return 0; +} + +static int +MakeNames(char *final_name, size_t final_size) +{ + unsigned char random[16]; + char encoded[sizeof(random) * 2U + 1U]; + unsigned int index; + int length; + + if (RandomBytes(random, sizeof(random)) != 0) return -1; + for (index = 0U; index < sizeof(random); index++) + snprintf(encoded + index * 2U, 3U, "%02x", random[index]); + length = snprintf(TemporaryName, sizeof(TemporaryName), ".tmp-%s-%ld", + encoded, (long)getpid()); + if (length < 0 || (size_t)length >= sizeof(TemporaryName)) { + errno = ENAMETOOLONG; + return -1; + } + length = snprintf(final_name, final_size, "drop-%s", encoded); + if (length < 0 || (size_t)length >= final_size) { + errno = ENAMETOOLONG; + return -1; + } + return 0; +} + +int +main(void) +{ + BongoMaildropSubmission submission = { 0 }; + char final_name[64]; + FILE *output = NULL; + int output_fd = -1; + int saved_error; + + umask(0027); + if (InstallSignalHandlers() != 0 || + BongoMaildropRead(stdin, BONGO_MAILDROP_MESSAGE_LIMIT, + BONGO_MAILDROP_RECIPIENT_LIMIT, + &submission) != 0) { + fprintf(stderr, "bongopostdrop: invalid submission: %s\n", + strerror(errno)); + return 64; + } + submission.uid = getuid(); + MaildropDirectory = + open(BONGO_MAILDROP_DIR, O_PATH | O_DIRECTORY | O_CLOEXEC); + if (MaildropDirectory < 0 || MakeNames(final_name, sizeof(final_name)) != 0) + goto fail; + output_fd = openat(MaildropDirectory, TemporaryName, + O_WRONLY | O_CREAT | O_EXCL | O_NOFOLLOW | O_CLOEXEC, + 0640); + if (output_fd < 0 || fchmod(output_fd, 0640) != 0) goto fail; + output = fdopen(output_fd, "wb"); + if (output == NULL) goto fail; + output_fd = -1; + if (BongoMaildropWrite(output, &submission) != 0 || + fflush(output) != 0 || fsync(fileno(output)) != 0 || + fclose(output) != 0) { + output = NULL; + goto fail; + } + output = NULL; + if (renameat(MaildropDirectory, TemporaryName, + MaildropDirectory, final_name) != 0) + goto fail; + TemporaryName[0] = '\0'; + close(MaildropDirectory); + MaildropDirectory = -1; + BongoMaildropSubmissionClear(&submission); + return 0; + +fail: + saved_error = errno != 0 ? errno : EIO; + if (output != NULL) fclose(output); + if (output_fd >= 0) close(output_fd); + Cleanup(0); + if (MaildropDirectory >= 0) close(MaildropDirectory); + BongoMaildropSubmissionClear(&submission); + fprintf(stderr, "bongopostdrop: cannot commit local message: %s\n", + strerror(saved_error)); + return 75; +} diff --git a/src/apps/sendmail/sendmail.c b/src/apps/sendmail/sendmail.c index 021e4db..1e4922f 100644 --- a/src/apps/sendmail/sendmail.c +++ b/src/apps/sendmail/sendmail.c @@ -1,355 +1,512 @@ /**************************************************************************** * * Copyright (c) 2001 Novell, Inc. All Rights Reserved. - * + * * This program is free software; you can redistribute it and/or * modify it under the terms of version 2 of the GNU General Public License * as published by the Free Software Foundation. - * + * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. - * + * * You should have received a copy of the GNU General Public License * along with this program; if not, contact Novell, Inc. - * - * To contact Novell about this file by physical or electronic mail, you - * may find current contact information at www.novell.com. * ****************************************************************************/ -/* This is an implementation of sendmail designed to eventually conform to LSB: - * http://refspecs.freestandards.org/LSB_2.1.0/LSB-Core-generic/LSB-Core-generic/baselib-sendmail-1.html - * Other platforms may or may not find it useful. - */ +#include -#include -#include +#include #include -#include -#include -#include +#include +#include #include #include +#include +#include +#include #include -#include -#include -#include "sendmail.h" - -static char* -LookupUser(uid_t uid) -{ - char *ret; - char *buf; - int buflen; - struct passwd pwbuf; - struct passwd *pw; - - buflen = sysconf(_SC_GETPW_R_SIZE_MAX); - buf = MemMalloc(buflen + 1); -#if defined(__SVR4) && defined(__sun) && !defined(_POSIX_PTHREAD_SEMANTICS) - pw = getpwuid_r(uid, &pwbuf, buf, buflen); -#else - getpwuid_r(uid, &pwbuf, buf, buflen, &pw); +#ifndef BONGO_POSTDROP_PATH +#define BONGO_POSTDROP_PATH "/usr/sbin/bongopostdrop" #endif - - if (!pw) { - MemFree(buf); - return NULL; - } - ret = MemStrdup(pw->pw_name); - MemFree(buf); - return ret; +typedef struct { + char *sender; + char *from_name; + int extract_recipients; + int ignore_dots; + GPtrArray *recipients; +} SendmailOptions; + +static void +Usage(const char *program) +{ + fprintf(stderr, + "Usage: %s [-i] [-t] [-f sender] [-F name] [recipient ...]\n", + program); } - -int -main (int argc, char *argv[]) +static int +AddRecipient(GPtrArray *recipients, const char *value) { - Connection *nmap; - ssize_t size = 0; - char *line = NULL; - char *response; - char command[200]; - int i; - - ConnStartup((15*60)); - - MsgInit(); - NMAPInitialize(); - CONN_TRACE_INIT(XPL_DEFAULT_WORK_DIR, "sendmail"); - // CONN_TRACE_SET_FLAGS(CONN_TRACE_ALL); /* uncomment this line and pass '--enable-conntrace' to autogen to get the agent to trace all connections */ + char *copy; + unsigned int index; - /* TODO: we only allow localhost at the moment, because that's where we're - * getting our credentials. In the future, being able to remove the MDB - * stuff and connect to an arbitrary NMAP server would be nice. - */ - nmap = NMAPConnectQueue("127.0.0.1", NULL); - if (nmap == NULL) { - FatalError(1, "cannot connect to NMAP server."); + if (value == NULL) return -1; + copy = g_strdup(value); + g_strstrip(copy); + if (copy[0] == '<' && copy[strlen(copy) - 1U] == '>') { + memmove(copy, copy + 1, strlen(copy)); + copy[strlen(copy) - 1U] = '\0'; } - - response = MemMalloc(sizeof(char) * 1000); - if (! NMAPAuthenticateToQueue(nmap, response, 1000)) { - FatalError(1, "cannot authenticate with NMAP."); - } - MemFree(response); - - /* we're now all setup, and can start doing things */ - ParseArgs(argc, argv); - - /* start sending the email */ - NMAPSimple(nmap, "QCREA"); - - /* send the mail body */ - NMAPSendCommand(nmap, "QSTOR MESSAGE\r\n", 15); - - line = MemMalloc(sizeof(char) * 1000); - while ((size = GetLine(line, sizeof(char) * 1000, stdin)) && (size != -1)) { - if ((line[0] == '.') && (size == 3)) { - if (BongoSendmailConfig.inputmode == UNTIL_DOT) { - /* this means end of mail */ - break; - } else { - /* this is mail content we need to pad to not confuse NMAP */ - NMAPSendCommand(nmap, "..\r\n", - sizeof("..\r\n") - 1U); - } - } else { - /* pass the contents through to NMAP; this is the mail */ - char *text = line; - if (! strncmp(line, "From:", 5) && BongoSendmailConfig.from_header) { - NMAPSendCommand(nmap, "From: ", 6); - text = BongoSendmailConfig.from_header; - } else if ((! strncmp(line, "To:", 3)) || (! strncmp(line, "Cc:", 3))) { - if (BongoSendmailConfig.extract) { - AddAddressees(line); - } - } else if (! strncmp(line, "Bcc:", 4)) { - if (BongoSendmailConfig.extract) { - AddAddressees(line); - } - /* TODO: should only remove Bcc: when To: or Cc: is present, RFC 2822 */ - text = NULL; - } - if (text) { - NMAPSendCommand(nmap, text, strlen(text)); - } - } - } - NMAPSimple(nmap, "."); - MemFree(line); - - /* send the mail envelope */ - for (i = 0; i < BongoSendmailConfig.addressees; i++) { - char *address = BongoSendmailConfig.deliver_to[i]; - - sprintf(command, "QSTOR TO %s %s 2", address, address); - NMAPSimple(nmap, command); - } - - if (! BongoSendmailConfig.from_envelope) { - char *host; - char *username = getenv("USER"); - if (! username) { - username = getenv("LOGNAME"); - } - - if (! username) { - username = LookupUser(getuid()); - } - host = MemMalloc(256); - gethostname(host, 256); - BongoSendmailConfig.from_envelope = MemMalloc(1000); - snprintf(BongoSendmailConfig.from_envelope, 1000,"%s@%s", username, host); - MemFree(host); - } - - sprintf(command, "QSTOR FROM %s -", BongoSendmailConfig.from_envelope); - NMAPSimple(nmap, command); - NMAPSimple(nmap, "QRUN"); - - NMAPQuit(nmap); - CONN_TRACE_SHUTDOWN(); - - return(0); -} - -void -ParseArgs (int argc, char *argv[]) -{ - int arg = 1; - int i; - - BongoSendmailConfig.inputmode = UNTIL_DOT; - BongoSendmailConfig.extract = FALSE; - - while ((arg < argc) && (argv[arg][0] == '-')) { - switch(argv[arg][1]) { - case 'b': - switch (argv[arg][2]) { - case 'p': - FatalError(50, "listing mail queue jobs not supported."); - PrintUsage(); - break; - case 's': - FatalError(50, "running in smtpd mode is not supported."); - PrintUsage(); - break; - case 'm': - BongoSendmailConfig.inputmode = UNTIL_DOT; - break; - default: - XplConsolePrintf("Unknown argument: %s\n", argv[arg]); - break; - } - break; - case 'i': - BongoSendmailConfig.inputmode = UNTIL_EOF; - break; - case 't': - /* TODO: we don't remove addresses passed in argv when in -t i - * mode, which we should for LSB compliance. Horrible - * behaviour tho'. - */ - BongoSendmailConfig.extract = TRUE; - break; - case 'F': - arg++; - BongoSendmailConfig.from_header = argv[arg]; - break; - case 'f': - case 'r': /* postfix sendmail accepts -r ... */ - arg++; - BongoSendmailConfig.from_envelope = argv[arg]; - break; - case 'e': - /* ignore these arguments */ - break; - case 'o': - if(argv[arg][2] == 'i') { - BongoSendmailConfig.inputmode = UNTIL_EOF; - } - /* ignore other -o arguments */ - break; - default: - XplConsolePrintf("Unknown argument: %s\n", argv[arg]); - break; - } - arg++; - } - - for (i = 0; i < (argc - arg); i++) { - AddAddressee(argv[arg]); - } -} - -void -AddAddressees (char *addressees) -{ - /* This is supposed to extract individual addresses from a header. It has - * to deal with lines such as (from RFC 2822): - * To: Mary Smith , jdoe@example.org, Who? - * Cc: , "Giant; \"Big\" Box" - * In this simple scenario, it's just pulling out things which look like - * actual addresses. - */ - int start, i; - int has_at = 0; - int addressees_length; - const char *invalid_email_chars = "<>, \t\r\n"; - char *email; - - addressees_length = strlen(addressees); - start = -1; - - for (i = 0; i < addressees_length; i++) { - int is_invalid_mail_char = 0; - unsigned int j; - - for (j = 0; j < strlen(invalid_email_chars); j++) { - if (addressees[i] == invalid_email_chars[j]) { - is_invalid_mail_char = 1; - } - } - if (is_invalid_mail_char || (i == addressees_length)) { - if ((start != -1) && has_at && ((i - start)<1000)) { - email = MemMalloc ((sizeof(char) * (i - start)) + 1); - strncpy (email, &addressees[start], i - start); - email[i - start] = 0; - AddAddressee(email); - } - start = -1; - has_at = 0; - } else if (addressees[i] == '@') { - has_at = 1; - } else { - if (start == -1) start = i; - } - } -} - -void -AddAddressee (char *addressee) -{ - /* this is someone we want to send the mail to (ie., address envelope to) */ - int size; - - BongoSendmailConfig.addressees++; - size = (sizeof(char *) * BongoSendmailConfig.addressees); - if (BongoSendmailConfig.deliver_to) { - BongoSendmailConfig.deliver_to = MemRealloc(BongoSendmailConfig.deliver_to, size); - } else { - BongoSendmailConfig.deliver_to = MemMalloc(size); - } - BongoSendmailConfig.deliver_to[BongoSendmailConfig.addressees - 1] = addressee; -} - -void -FatalError (const int exit_code, const char *message) -{ - fprintf(stderr, "bongosendmail: %s\n", message); - exit(exit_code); -} - -void -PrintUsage () -{ - XplConsolePrintf("bongosendmail: fatal: usage: [options] [addresses]\n"); - exit(0); -} - -int -GetLine (char *buf, const unsigned int bufsize, FILE *fh) -{ - /* read a line from the file handle, make sure it's \r\n terminated */ - if (fgets (buf, bufsize - 1, fh ) == NULL) { + if (!BongoMaildropAddressValid(copy)) { + g_free(copy); return -1; - } else { - unsigned int strsize = strlen(buf); - buf[strsize-1] = '\r'; - buf[strsize] = '\n'; - buf[strsize+1] = 0; - return strsize + 1; } + for (index = 0U; index < recipients->len; index++) { + if (strcmp(g_ptr_array_index(recipients, index), copy) == 0) { + g_free(copy); + return 0; + } + } + g_ptr_array_add(recipients, copy); + return 0; } -void -NMAPSimple (Connection *nmap, char *command) +static const char * +OptionValue(int argc, char **argv, int *index) { - /* send NMAP a command and burp if it doesn't succeed */ - int repcode; - char response[1024]; - - NMAPSendCommand(nmap, command, strlen(command)); - NMAPSendCommand(nmap, "\r\n", 2); - repcode = NMAPReadAnswer(nmap, response, sizeof(response), FALSE); - if (repcode != 1000) { - XplConsolePrintf("ERROR: Sent command %s\n", command); - XplConsolePrintf("ERROR: NMAP server returned %d: %s\n", repcode, response); - exit(-2); - } + if (argv[*index][2] != '\0') return argv[*index] + 2; + if (*index + 1 >= argc) return NULL; + (*index)++; + return argv[*index]; +} + +static int +ParseOptions(int argc, char **argv, SendmailOptions *options) +{ + int index; + + memset(options, 0, sizeof(*options)); + options->recipients = g_ptr_array_new_with_free_func(g_free); + for (index = 1; index < argc; index++) { + const char *argument = argv[index]; + const char *value; + if (strcmp(argument, "--") == 0) { + for (index++; index < argc; index++) { + if (AddRecipient(options->recipients, argv[index]) != 0) + return -1; + } + break; + } + if (argument[0] != '-' || strcmp(argument, "-") == 0) { + if (AddRecipient(options->recipients, argument) != 0) return -1; + } else if (strcmp(argument, "-i") == 0 || + strcmp(argument, "-oi") == 0) { + options->ignore_dots = 1; + } else if (strcmp(argument, "-t") == 0) { + options->extract_recipients = 1; + } else if (strcmp(argument, "-bm") == 0 || + strcmp(argument, "-om") == 0 || + strcmp(argument, "-Am") == 0 || + strcmp(argument, "-Ac") == 0) { + continue; + } else if (argument[1] == 'f' || argument[1] == 'r') { + value = OptionValue(argc, argv, &index); + if (value == NULL || + (strcmp(value, "<>") != 0 && + !BongoMaildropAddressValid(value))) + return -1; + g_free(options->sender); + options->sender = g_strdup(value); + g_strstrip(options->sender); + if (options->sender[0] == '<' && + options->sender[strlen(options->sender) - 1U] == '>') { + memmove(options->sender, options->sender + 1, + strlen(options->sender)); + options->sender[strlen(options->sender) - 1U] = '\0'; + } + if (options->sender[0] != '\0' && + !BongoMaildropAddressValid(options->sender)) + return -1; + } else if (argument[1] == 'F') { + value = OptionValue(argc, argv, &index); + if (value == NULL || strchr(value, '\r') != NULL || + strchr(value, '\n') != NULL) + return -1; + g_free(options->from_name); + options->from_name = g_strdup(value); + } else if (strcmp(argument, "-o") == 0 || + strcmp(argument, "-O") == 0 || + strcmp(argument, "-B") == 0 || + strcmp(argument, "-N") == 0 || + strcmp(argument, "-R") == 0 || + strcmp(argument, "-V") == 0) { + if (index + 1 >= argc) return -1; + index++; + } else if ((argument[1] == 'o' || argument[1] == 'O') && + argument[2] != '\0') { + continue; + } else if (strcmp(argument, "--help") == 0 || + strcmp(argument, "-h") == 0) { + return 1; + } else { + return -1; + } + } + return 0; +} + +static int +ReadMessage(int ignore_dots, GByteArray **message) +{ + GByteArray *output = g_byte_array_new(); + unsigned char buffer[16384]; + + if (ignore_dots) { + while (!feof(stdin)) { + size_t count = fread(buffer, 1U, sizeof(buffer), stdin); + if (count != 0U) g_byte_array_append(output, buffer, count); + if (ferror(stdin) || + output->len > BONGO_MAILDROP_MESSAGE_LIMIT) { + g_byte_array_free(output, TRUE); + errno = ferror(stdin) ? EIO : EFBIG; + return -1; + } + } + } else { + char *line = NULL; + size_t capacity = 0U; + ssize_t count; + while ((count = getline(&line, &capacity, stdin)) >= 0) { + size_t content = (size_t)count; + while (content > 0U && + (line[content - 1U] == '\n' || + line[content - 1U] == '\r')) + content--; + if (content == 1U && line[0] == '.') break; + if (content >= 2U && line[0] == '.' && line[1] == '.') { + line++; + count--; + capacity--; + g_byte_array_append(output, (unsigned char *)line, + (size_t)count); + line--; + capacity++; + } else { + g_byte_array_append(output, (unsigned char *)line, + (size_t)count); + } + if (output->len > BONGO_MAILDROP_MESSAGE_LIMIT) { + free(line); + g_byte_array_free(output, TRUE); + errno = EFBIG; + return -1; + } + } + free(line); + if (ferror(stdin)) { + g_byte_array_free(output, TRUE); + errno = EIO; + return -1; + } + } + if (output->len == 0U) { + g_byte_array_free(output, TRUE); + errno = EINVAL; + return -1; + } + *message = output; + return 0; +} + +static void +ExtractAddressToken(GPtrArray *recipients, char *token) +{ + char *left; + char *right; + char *colon; + char *semicolon; + + g_strstrip(token); + left = strrchr(token, '<'); + right = left != NULL ? strchr(left + 1, '>') : NULL; + if (left != NULL && right != NULL) { + *right = '\0'; + AddRecipient(recipients, left + 1); + return; + } + colon = strchr(token, ':'); + if (colon != NULL) token = colon + 1; + semicolon = strrchr(token, ';'); + if (semicolon != NULL) *semicolon = '\0'; + g_strstrip(token); + if (strchr(token, ' ') == NULL && strchr(token, '\t') == NULL && + token[0] != '\0') + AddRecipient(recipients, token); +} + +static void +ExtractAddresses(GPtrArray *recipients, const char *value) +{ + char *copy = g_strdup(value); + char *start = copy; + char *cursor; + int quoted = 0; + int escaped = 0; + int angle = 0; + int comment = 0; + + for (cursor = copy; ; cursor++) { + char current = *cursor; + if (escaped) { + escaped = 0; + } else if (current == '\\' && quoted) { + escaped = 1; + } else if (current == '"' && comment == 0) { + quoted = !quoted; + } else if (!quoted && current == '(') { + comment++; + } else if (!quoted && current == ')' && comment > 0) { + comment--; + } else if (!quoted && comment == 0 && current == '<') { + angle++; + } else if (!quoted && comment == 0 && current == '>' && angle > 0) { + angle--; + } else if ((current == ',' && !quoted && comment == 0 && + angle == 0) || current == '\0') { + *cursor = '\0'; + ExtractAddressToken(recipients, start); + start = cursor + 1; + } + if (current == '\0') break; + } + g_free(copy); +} + +static int +HeaderName(const unsigned char *line, size_t length, const char *name) +{ + size_t name_length = strlen(name); + return length > name_length && line[name_length] == ':' && + g_ascii_strncasecmp((const char *)line, name, name_length) == 0; +} + +static int +PrepareMessage(GByteArray *raw, const SendmailOptions *options, + GByteArray **prepared) +{ + GByteArray *output = g_byte_array_new(); + GString *address_header = g_string_new(NULL); + size_t offset = 0U; + int skip_header = 0; + int recipient_header = 0; + int has_from = 0; + int in_headers = 1; + + while (offset < raw->len) { + size_t end = offset; + size_t content_end; + const unsigned char *line; + size_t length; + int continuation; + while (end < raw->len && raw->data[end] != '\n') end++; + if (end < raw->len) end++; + line = raw->data + offset; + length = end - offset; + content_end = length; + while (content_end > 0U && + (line[content_end - 1U] == '\n' || + line[content_end - 1U] == '\r')) + content_end--; + continuation = content_end > 0U && + (line[0] == ' ' || line[0] == '\t'); + if (in_headers && !continuation && recipient_header && + address_header->len != 0U) { + ExtractAddresses(options->recipients, address_header->str); + g_string_truncate(address_header, 0U); + } + if (in_headers && content_end == 0U) { + if (options->from_name != NULL && !has_from) { + char hostname[256]; + struct passwd *password = getpwuid(getuid()); + const char *user = + password != NULL ? password->pw_name : "unknown"; + if (gethostname(hostname, sizeof(hostname)) != 0) + strcpy(hostname, "localhost"); + hostname[sizeof(hostname) - 1U] = '\0'; + g_byte_array_append(output, (const unsigned char *)"From: ", + sizeof("From: ") - 1U); + g_byte_array_append(output, + (const unsigned char *)options->from_name, + strlen(options->from_name)); + g_byte_array_append(output, (const unsigned char *)" <", + sizeof(" <") - 1U); + g_byte_array_append(output, (const unsigned char *)user, + strlen(user)); + g_byte_array_append(output, (const unsigned char *)"@", + 1U); + g_byte_array_append(output, (const unsigned char *)hostname, + strlen(hostname)); + g_byte_array_append(output, + (const unsigned char *)">\r\n", 3U); + } + in_headers = 0; + skip_header = 0; + recipient_header = 0; + } else if (in_headers && !continuation) { + skip_header = HeaderName(line, content_end, "Bcc"); + recipient_header = + options->extract_recipients && + (HeaderName(line, content_end, "To") || + HeaderName(line, content_end, "Cc") || + skip_header); + if (HeaderName(line, content_end, "From")) has_from = 1; + if (recipient_header) { + const unsigned char *colon = + memchr(line, ':', content_end); + if (colon != NULL) + g_string_append_len(address_header, + (const char *)colon + 1, + content_end - + (size_t)(colon + 1 - line)); + } + } else if (in_headers && continuation && recipient_header) { + g_string_append_c(address_header, ' '); + g_string_append_len(address_header, (const char *)line, + content_end); + } + if (!in_headers || !skip_header) + g_byte_array_append(output, line, length); + offset = end; + } + if (in_headers && recipient_header && address_header->len != 0U) + ExtractAddresses(options->recipients, address_header->str); + g_string_free(address_header, TRUE); + if (options->recipients->len == 0U) { + g_byte_array_free(output, TRUE); + errno = EINVAL; + return -1; + } + *prepared = output; + return 0; +} + +static char * +DefaultSender(void) +{ + struct passwd *password = getpwuid(getuid()); + const char *user = password != NULL ? password->pw_name : "unknown"; + char hostname[256]; + + if (gethostname(hostname, sizeof(hostname)) != 0) + strcpy(hostname, "localhost"); + hostname[sizeof(hostname) - 1U] = '\0'; + return g_strdup_printf("%s@%s", user, hostname); +} + +static int +Submit(const BongoMaildropSubmission *submission) +{ + int descriptors[2]; + pid_t child; + FILE *stream; + int status; + struct sigaction ignore; + struct sigaction previous; + + if (pipe(descriptors) != 0) return -1; + child = fork(); + if (child < 0) { + close(descriptors[0]); + close(descriptors[1]); + return -1; + } + if (child == 0) { + if (dup2(descriptors[0], STDIN_FILENO) < 0) _exit(75); + close(descriptors[0]); + close(descriptors[1]); + execl(BONGO_POSTDROP_PATH, "bongopostdrop", (char *)NULL); + _exit(75); + } + close(descriptors[0]); + memset(&ignore, 0, sizeof(ignore)); + ignore.sa_handler = SIG_IGN; + sigemptyset(&ignore.sa_mask); + sigaction(SIGPIPE, &ignore, &previous); + stream = fdopen(descriptors[1], "wb"); + if (stream == NULL || BongoMaildropWrite(stream, submission) != 0 || + fflush(stream) != 0 || fclose(stream) != 0) { + if (stream == NULL) close(descriptors[1]); + sigaction(SIGPIPE, &previous, NULL); + waitpid(child, &status, 0); + return -1; + } + sigaction(SIGPIPE, &previous, NULL); + while (waitpid(child, &status, 0) < 0) { + if (errno != EINTR) return -1; + } + if (!WIFEXITED(status)) { + errno = EIO; + return -1; + } + if (WEXITSTATUS(status) != 0) { + errno = WEXITSTATUS(status) == 64 ? EINVAL : EAGAIN; + return -1; + } + return 0; +} + +int +main(int argc, char **argv) +{ + SendmailOptions options; + BongoMaildropSubmission submission = { 0 }; + GByteArray *raw = NULL; + GByteArray *prepared = NULL; + int result; + + result = ParseOptions(argc, argv, &options); + if (result != 0) { + Usage(argv[0]); + if (options.recipients != NULL) + g_ptr_array_free(options.recipients, TRUE); + g_free(options.sender); + g_free(options.from_name); + return result > 0 ? 0 : 64; + } + if (ReadMessage(options.ignore_dots, &raw) != 0 || + PrepareMessage(raw, &options, &prepared) != 0) { + fprintf(stderr, "bongo-sendmail: invalid message or recipients: %s\n", + strerror(errno)); + result = 64; + goto cleanup; + } + submission.uid = getuid(); + submission.sender = + options.sender != NULL ? g_strdup(options.sender) : DefaultSender(); + submission.recipients = options.recipients; + options.recipients = NULL; + submission.message = g_memdup2(prepared->data, prepared->len); + submission.message_length = prepared->len; + if (Submit(&submission) != 0) { + fprintf(stderr, "bongo-sendmail: local submission failed: %s\n", + strerror(errno)); + result = errno == EINVAL ? 64 : 75; + } else { + result = 0; + } + +cleanup: + if (raw != NULL) g_byte_array_free(raw, TRUE); + if (prepared != NULL) g_byte_array_free(prepared, TRUE); + BongoMaildropSubmissionClear(&submission); + if (options.recipients != NULL) + g_ptr_array_free(options.recipients, TRUE); + g_free(options.sender); + g_free(options.from_name); + return result; } diff --git a/src/apps/sendmail/sendmail.h b/src/apps/sendmail/sendmail.h deleted file mode 100644 index 421435b..0000000 --- a/src/apps/sendmail/sendmail.h +++ /dev/null @@ -1,49 +0,0 @@ -/**************************************************************************** - * - * Copyright (c) 2001 Novell, Inc. All Rights Reserved. - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of version 2 of the GNU General Public License - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, contact Novell, Inc. - * - * To contact Novell about this file by physical or electronic mail, you - * may find current contact information at www.novell.com. - * - ****************************************************************************/ - -#ifndef _BONGO_SENDMAIL -#define _BONGO_SENDMAIL - -enum InputMode { - UNTIL_DOT, UNTIL_EOF, -}; - -struct config { - char *from_header; - char *from_envelope; - - enum InputMode inputmode; - int extract; - - int addressees; - char **deliver_to; -} BongoSendmailConfig; - -/* bongosendmail.c */ -void FatalError(const int error_code, const char *string); -void PrintUsage(void); -void NMAPSimple(Connection *nmap, char *command); -void ParseArgs(int argc, char *argv[]); -int GetLine(char *buffer, const unsigned int bufsize, FILE *handle); -void AddAddressees (char *addressees); -void AddAddressee (char *addressee); - -#endif /* _BONGO_SENDMAIL */ diff --git a/src/apps/sendmail/tests/sendmail-c-test.py b/src/apps/sendmail/tests/sendmail-c-test.py new file mode 100644 index 0000000..397388c --- /dev/null +++ b/src/apps/sendmail/tests/sendmail-c-test.py @@ -0,0 +1,110 @@ +#!/usr/bin/env python3 +# /**************************************************************************** +# * +# * Copyright (c) 2001 Novell, Inc. All Rights Reserved. +# * +# * This program is free software; you can redistribute it and/or +# * modify it under the terms of version 2 of the GNU General Public License +# * as published by the Free Software Foundation. +# * +# * This program is distributed in the hope that it will be useful, +# * but WITHOUT ANY WARRANTY; without even the implied warranty of +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# * GNU General Public License for more details. +# * +# ****************************************************************************/ + +"""Black-box checks for the C sendmail/postdrop privilege boundary.""" + +from __future__ import annotations + +import shutil +import struct +import subprocess +import sys +from pathlib import Path + + +def decode(path: Path) -> tuple[int, str, list[str], bytes]: + data = memoryview(path.read_bytes()) + offset = 0 + + def take(length: int) -> bytes: + nonlocal offset + if offset + length > len(data): + raise AssertionError("truncated maildrop") + value = bytes(data[offset:offset + length]) + offset += length + return value + + def u32() -> int: + return struct.unpack(">I", take(4))[0] + + def u64() -> int: + return struct.unpack(">Q", take(8))[0] + + assert take(8) == b"BONGOMD1" + uid = u64() + sender_length = u32() + recipient_count = u32() + message_length = u64() + sender = take(sender_length).decode("utf-8") + recipients = [ + take(u32()).decode("utf-8") for _ in range(recipient_count) + ] + message = take(message_length) + assert offset == len(data) + return uid, sender, recipients, message + + +def main() -> int: + if len(sys.argv) != 3: + raise SystemExit("usage: sendmail-c-test.py SENDMAIL MAILDROP") + sendmail = Path(sys.argv[1]) + maildrop = Path(sys.argv[2]) + shutil.rmtree(maildrop, ignore_errors=True) + maildrop.mkdir(mode=0o700) + raw = ( + b"From: Sender \r\n" + b"To: One \r\n" + b"Cc: two@example.test\r\n" + b"Bcc: Hidden \r\n" + b"\t, folded@example.test\r\n" + b"Subject: C frontend\r\n" + b"\r\n" + b"one\r\n" + b"..two\r\n" + b".\r\n" + b"ignored\r\n" + ) + completed = subprocess.run( + [ + str(sendmail), "-t", "-f", "sender@example.test", + "explicit@example.test", + ], + input=raw, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + check=False, + ) + assert completed.returncode == 0, completed.stderr.decode() + drops = list(maildrop.glob("drop-*")) + assert len(drops) == 1 + _, sender, recipients, message = decode(drops[0]) + assert sender == "sender@example.test" + assert recipients == [ + "explicit@example.test", + "one@example.test", + "two@example.test", + "hidden@example.test", + "folded@example.test", + ] + assert b"\r\nBcc:" not in b"\r\n" + message + assert b"\r\n\t, folded@example.test" not in message + assert message.endswith(b"one\r\n.two\r\n") + shutil.rmtree(maildrop) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/src/apps/sendmail/tests/test_sendmail.py b/src/apps/sendmail/tests/test_sendmail.py deleted file mode 100644 index f87ed42..0000000 --- a/src/apps/sendmail/tests/test_sendmail.py +++ /dev/null @@ -1,125 +0,0 @@ -# /**************************************************************************** -# * -# * Copyright (c) 2001 Novell, Inc. All Rights Reserved. -# * -# * This program is free software; you can redistribute it and/or -# * modify it under the terms of version 2 of the GNU General Public License -# * as published by the Free Software Foundation. -# * -# * This program is distributed in the hope that it will be useful, -# * but WITHOUT ANY WARRANTY; without even the implied warranty of -# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# * GNU General Public License for more details. -# * -# * You should have received a copy of the GNU General Public License -# * along with this program; if not, contact Novell, Inc. -# * -# * To contact Novell about this file by physical or electronic mail, you -# * may find current contact information at www.novell.com. -# * -# ****************************************************************************/ - -import io -import json -import tempfile -import unittest -from pathlib import Path -from unittest import mock - -from bongo import sendmail - - -class SendmailTest(unittest.TestCase): - def test_parses_combined_options_and_attached_sender(self): - options = sendmail.parse_options( - ["-oi", "-t", "-froot@example.test", "extra@example.test"]) - self.assertTrue(options.ignore_dots) - self.assertTrue(options.extract_recipients) - self.assertEqual(options.envelope_from, "root@example.test") - self.assertEqual(options.recipients, ("extra@example.test",)) - - def test_dot_terminated_input_is_unstuffed(self): - source = io.BytesIO(b"Subject: test\r\n\r\none\r\n..two\r\n.\r\nignored\r\n") - self.assertEqual( - sendmail.read_message(source, False), - b"Subject: test\r\n\r\none\r\n.two\r\n") - - def test_extracts_all_recipients_and_removes_bcc(self): - options = sendmail.parse_options(["-t", "extra@example.test"]) - raw = ( - b"From: sender@example.test\r\n" - b"To: One \r\n" - b"Cc: two@example.test\r\n" - b"Bcc: Hidden \r\n" - b"\r\nbody\r\n" - ) - rendered, recipients = sendmail.prepare_message(raw, options) - self.assertEqual( - recipients, - ["extra@example.test", "one@example.test", "two@example.test", - "hidden@example.test"]) - self.assertNotIn(b"Bcc:", rendered) - self.assertIn(b"body", rendered) - - def test_prefers_enabled_internal_relay(self): - with tempfile.TemporaryDirectory() as directory: - Path(directory, "smtp").write_text(json.dumps({ - "internal_relay_enabled": True, - "internal_relay_bind_address": "0.0.0.0", - "internal_relay_port": 2026, - "port": 2025, - })) - self.assertEqual( - sendmail.select_endpoint(directory), - sendmail.SubmissionEndpoint("127.0.0.1", 2026)) - - def test_falls_back_to_normal_local_smtp(self): - with tempfile.TemporaryDirectory() as directory: - Path(directory, "smtp").write_text(json.dumps({ - "internal_relay_enabled": False, - "port": 2525, - })) - self.assertEqual( - sendmail.select_endpoint(directory), - sendmail.SubmissionEndpoint("127.0.0.1", 2525)) - - @mock.patch("bongo.sendmail.smtplib.SMTP") - def test_submission_upgrades_to_starttls(self, smtp_class): - client = smtp_class.return_value.__enter__.return_value - client.has_extn.return_value = True - client.sendmail.return_value = {} - endpoint = sendmail.SubmissionEndpoint("127.0.0.1", 26) - - sendmail.submit_message( - endpoint, "sender@example.test", ["to@example.test"], - b"Subject: test\r\n\r\nbody\r\n", 5) - - smtp_class.assert_called_once_with("127.0.0.1", 26, timeout=5) - client.starttls.assert_called_once() - client.sendmail.assert_called_once_with( - "sender@example.test", ["to@example.test"], - b"Subject: test\r\n\r\nbody\r\n") - - @mock.patch("bongo.sendmail.submit_message") - def test_main_submits_without_system_credentials(self, submit): - with tempfile.TemporaryDirectory() as directory: - Path(directory, "smtp").write_text(json.dumps({ - "internal_relay_enabled": True, - "internal_relay_bind_address": "127.0.0.1", - "internal_relay_port": 26, - })) - result = sendmail.main( - ["-oi", "-t", "-f", "sender@example.test"], - config_dir=directory, - stdin=io.BytesIO( - b"To: receiver@example.test\r\nSubject: test\r\n\r\nbody\r\n"), - stderr=io.StringIO()) - self.assertEqual(result, sendmail.EX_OK) - submit.assert_called_once() - self.assertEqual(submit.call_args.args[0].port, 26) - self.assertEqual(submit.call_args.args[1], "sender@example.test") - self.assertEqual(submit.call_args.args[2], ["receiver@example.test"]) - - -if __name__ == "__main__": - unittest.main() diff --git a/src/libs/python/bongo/sendmail.py b/src/libs/python/bongo/sendmail.py deleted file mode 100644 index c55632c..0000000 --- a/src/libs/python/bongo/sendmail.py +++ /dev/null @@ -1,267 +0,0 @@ -# /**************************************************************************** -# * -# * Copyright (c) 2001 Novell, Inc. All Rights Reserved. -# * -# * This program is free software; you can redistribute it and/or -# * modify it under the terms of version 2 of the GNU General Public License -# * as published by the Free Software Foundation. -# * -# * This program is distributed in the hope that it will be useful, -# * but WITHOUT ANY WARRANTY; without even the implied warranty of -# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# * GNU General Public License for more details. -# * -# * You should have received a copy of the GNU General Public License -# * along with this program; if not, contact Novell, Inc. -# * -# * To contact Novell about this file by physical or electronic mail, you -# * may find current contact information at www.novell.com. -# * -# ****************************************************************************/ - -"""Restricted sendmail-compatible local submission frontend. - -This frontend deliberately does not read Bongo's system credential. It -submits through the rate-limited internal SMTP listener, or through the normal -local SMTP listener when the internal listener is disabled. -""" - -from __future__ import annotations - -import email.policy -import getpass -import json -import os -import smtplib -import socket -import ssl -import sys -from dataclasses import dataclass -from email.generator import BytesGenerator -from email.parser import BytesParser -from email.utils import getaddresses -from io import BytesIO -from pathlib import Path -from typing import BinaryIO, Sequence, TextIO - -EX_OK = 0 -EX_USAGE = 64 -EX_UNAVAILABLE = 69 -EX_TEMPFAIL = 75 - - -class SubmissionError(RuntimeError): - """The message could not be submitted.""" - - -@dataclass(frozen=True) -class SendmailOptions: - envelope_from: str | None - from_name: str | None - extract_recipients: bool - ignore_dots: bool - recipients: tuple[str, ...] - - -@dataclass(frozen=True) -class SubmissionEndpoint: - host: str - port: int - opportunistic_tls: bool = True - - -def _option_value(arguments: Sequence[str], index: int, option: str) -> tuple[str, int]: - argument = arguments[index] - if len(argument) > 2: - return argument[2:], index - if index + 1 >= len(arguments): - raise SubmissionError(f"{option} requires an argument") - return arguments[index + 1], index + 1 - - -def parse_options(arguments: Sequence[str]) -> SendmailOptions: - envelope_from = None - from_name = None - extract_recipients = False - ignore_dots = False - recipients: list[str] = [] - index = 0 - - while index < len(arguments): - argument = arguments[index] - if argument == "--": - recipients.extend(arguments[index + 1:]) - break - if not argument.startswith("-") or argument == "-": - recipients.append(argument) - index += 1 - continue - if argument in ("-i", "-oi"): - ignore_dots = True - elif argument == "-t": - extract_recipients = True - elif argument in ("-bm", "-om"): - pass - elif argument.startswith("-f") or argument.startswith("-r"): - envelope_from, index = _option_value(arguments, index, argument[:2]) - elif argument.startswith("-F"): - from_name, index = _option_value(arguments, index, "-F") - elif argument in ("-o", "-O"): - _, index = _option_value(arguments, index, argument) - elif argument.startswith("-o") or argument.startswith("-O"): - # Common sendmail compatibility controls are advisory for this - # restricted local frontend. -oi was handled above. - pass - elif argument in ("-Am", "-Ac"): - pass - elif argument in ("-B", "-N", "-R", "-V"): - _, index = _option_value(arguments, index, argument) - elif argument in ("--help", "-h"): - raise SubmissionError("usage") - else: - raise SubmissionError(f"unsupported sendmail option {argument}") - index += 1 - - return SendmailOptions( - envelope_from=envelope_from, - from_name=from_name, - extract_recipients=extract_recipients, - ignore_dots=ignore_dots, - recipients=tuple(recipients), - ) - - -def read_message(stream: BinaryIO, ignore_dots: bool) -> bytes: - if ignore_dots: - return stream.read() - - lines: list[bytes] = [] - for line in stream: - if line.rstrip(b"\r\n") == b".": - break - if line.startswith(b".."): - line = line[1:] - lines.append(line) - return b"".join(lines) - - -def prepare_message(raw: bytes, options: SendmailOptions) -> tuple[bytes, list[str]]: - try: - message = BytesParser(policy=email.policy.SMTP).parsebytes(raw) - except (TypeError, ValueError) as error: - raise SubmissionError(f"cannot parse message: {error}") from error - - recipients = list(options.recipients) - if options.extract_recipients: - headers: list[str] = [] - for name in ("to", "cc", "bcc"): - headers.extend(str(value) for value in message.get_all(name, [])) - recipients.extend(address for _, address in getaddresses(headers) if address) - - while "bcc" in message: - del message["bcc"] - - if options.from_name and "from" not in message: - username = getpass.getuser() - message["From"] = f"{options.from_name} <{username}@{socket.getfqdn()}>" - - recipients = list(dict.fromkeys(address.strip() for address in recipients - if address.strip())) - if not recipients: - raise SubmissionError("no recipients were specified") - - output = BytesIO() - BytesGenerator(output, policy=email.policy.SMTP).flatten(message) - return output.getvalue(), recipients - - -def default_envelope_sender(options: SendmailOptions) -> str: - if options.envelope_from is not None: - return options.envelope_from.strip("<>") - return f"{getpass.getuser()}@{socket.getfqdn()}" - - -def _read_configuration(config_dir: str | os.PathLike[str], name: str) -> dict: - path = Path(config_dir) / name - try: - value = json.loads(path.read_text(encoding="utf-8")) - except (OSError, UnicodeError, json.JSONDecodeError) as error: - raise SubmissionError(f"cannot read Bongo {name} configuration: {error}") from error - if not isinstance(value, dict): - raise SubmissionError(f"Bongo {name} configuration is not an object") - return value - - -def select_endpoint(config_dir: str | os.PathLike[str]) -> SubmissionEndpoint: - override_host = os.environ.get("BONGO_SENDMAIL_HOST") - override_port = os.environ.get("BONGO_SENDMAIL_PORT") - if override_host or override_port: - try: - port = int(override_port or "25") - except ValueError as error: - raise SubmissionError("BONGO_SENDMAIL_PORT is not an integer") from error - return SubmissionEndpoint(override_host or "127.0.0.1", port) - - smtp = _read_configuration(config_dir, "smtp") - if smtp.get("internal_relay_enabled"): - host = str(smtp.get("internal_relay_bind_address") or "127.0.0.1") - if host in ("0.0.0.0", "::", "[::]"): - host = "127.0.0.1" - return SubmissionEndpoint(host, int(smtp.get("internal_relay_port", 26))) - return SubmissionEndpoint("127.0.0.1", int(smtp.get("port", 25))) - - -def submit_message(endpoint: SubmissionEndpoint, envelope_from: str, - recipients: Sequence[str], message: bytes, - timeout: float = 30.0) -> None: - try: - with smtplib.SMTP(endpoint.host, endpoint.port, timeout=timeout) as client: - client.ehlo_or_helo_if_needed() - if endpoint.opportunistic_tls and client.has_extn("starttls"): - # This is a same-host submission hop. Public PKI identity is - # verified on Bongo's external SMTP delivery path. - context = ssl._create_unverified_context() - client.starttls(context=context) - client.ehlo() - refused = client.sendmail(envelope_from, list(recipients), message) - if refused: - details = ", ".join(sorted(refused)) - raise SubmissionError(f"recipients refused: {details}") - except SubmissionError: - raise - except (OSError, smtplib.SMTPException) as error: - raise SubmissionError(f"local SMTP submission failed: {error}") from error - - -def _usage(program: str, stream: TextIO) -> None: - stream.write( - f"Usage: {program} [-i] [-t] [-f sender] [-F name] [recipient ...]\n" - ) - - -def main(arguments: Sequence[str] | None = None, *, - config_dir: str | os.PathLike[str] = "/etc/bongo/config.d", - stdin: BinaryIO | None = None, stderr: TextIO | None = None) -> int: - arguments = list(sys.argv[1:] if arguments is None else arguments) - stdin = sys.stdin.buffer if stdin is None else stdin - stderr = sys.stderr if stderr is None else stderr - - try: - options = parse_options(arguments) - raw = read_message(stdin, options.ignore_dots) - message, recipients = prepare_message(raw, options) - endpoint = select_endpoint(config_dir) - timeout = float(os.environ.get("BONGO_SENDMAIL_TIMEOUT", "30")) - submit_message(endpoint, default_envelope_sender(options), recipients, - message, timeout) - return EX_OK - except SubmissionError as error: - if str(error) == "usage": - _usage(Path(sys.argv[0]).name, stderr) - return EX_OK - stderr.write(f"bongo-sendmail: {error}\n") - return EX_USAGE if "option" in str(error) or "recipients" in str(error) \ - else EX_TEMPFAIL - except (ValueError, OverflowError) as error: - stderr.write(f"bongo-sendmail: invalid configuration: {error}\n") - return EX_UNAVAILABLE diff --git a/src/libs/util/CMakeLists.txt b/src/libs/util/CMakeLists.txt index d55486b..8de49ea 100644 --- a/src/libs/util/CMakeLists.txt +++ b/src/libs/util/CMakeLists.txt @@ -12,6 +12,7 @@ add_library(bongoutil SHARED quicksort.c utf7mod.c logging.c + maildrop.c domain.c bongoutil.c) @@ -37,4 +38,8 @@ if(BUILD_TESTING) add_executable(util-domain-test tests/domain-test.c domain.c) target_link_libraries(util-domain-test PRIVATE LibIDN2::LibIDN2) add_test(NAME util-domain COMMAND util-domain-test) + + add_executable(util-maildrop-test tests/maildrop-test.c maildrop.c) + target_link_libraries(util-maildrop-test PRIVATE GLib2::GLib2) + add_test(NAME util-maildrop COMMAND util-maildrop-test) endif() diff --git a/src/libs/util/maildrop.c b/src/libs/util/maildrop.c new file mode 100644 index 0000000..2e629dd --- /dev/null +++ b/src/libs/util/maildrop.c @@ -0,0 +1,257 @@ +/**************************************************************************** + * + * Copyright (c) 2001 Novell, Inc. All Rights Reserved. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of version 2 of the GNU General Public License + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, contact Novell, Inc. + * + ****************************************************************************/ + +#include + +#include +#include +#include + +#define MAILDROP_MAGIC "BONGOMD1" +#define MAILDROP_MAGIC_LENGTH 8U + +static int +WriteBytes(FILE *stream, const void *value, size_t length) +{ + if (length != 0U && fwrite(value, 1U, length, stream) != length) { + if (errno == 0) errno = EIO; + return -1; + } + return 0; +} + +static int +ReadBytes(FILE *stream, void *value, size_t length) +{ + if (length != 0U && fread(value, 1U, length, stream) != length) { + errno = ferror(stream) ? (errno != 0 ? errno : EIO) : EINVAL; + return -1; + } + return 0; +} + +static int +WriteU32(FILE *stream, uint32_t value) +{ + unsigned char data[4]; + + data[0] = (unsigned char)(value >> 24); + data[1] = (unsigned char)(value >> 16); + data[2] = (unsigned char)(value >> 8); + data[3] = (unsigned char)value; + return WriteBytes(stream, data, sizeof(data)); +} + +static int +ReadU32(FILE *stream, uint32_t *value) +{ + unsigned char data[4]; + + if (ReadBytes(stream, data, sizeof(data)) != 0) return -1; + *value = ((uint32_t)data[0] << 24) | + ((uint32_t)data[1] << 16) | + ((uint32_t)data[2] << 8) | + (uint32_t)data[3]; + return 0; +} + +static int +WriteU64(FILE *stream, uint64_t value) +{ + unsigned char data[8]; + unsigned int index; + + for (index = 0U; index < sizeof(data); index++) + data[index] = (unsigned char)(value >> ((7U - index) * 8U)); + return WriteBytes(stream, data, sizeof(data)); +} + +static int +ReadU64(FILE *stream, uint64_t *value) +{ + unsigned char data[8]; + unsigned int index; + + if (ReadBytes(stream, data, sizeof(data)) != 0) return -1; + *value = 0U; + for (index = 0U; index < sizeof(data); index++) + *value = (*value << 8) | data[index]; + return 0; +} + +int +BongoMaildropAddressValid(const char *address) +{ + const unsigned char *cursor = (const unsigned char *)address; + size_t length; + + if (address == NULL) return 0; + length = strlen(address); + if (length == 0U || length > BONGO_MAILDROP_ADDRESS_LIMIT) return 0; + while (*cursor != '\0') { + if (*cursor == '\r' || *cursor == '\n' || *cursor == '\0' || + *cursor < 0x20U || *cursor == 0x7fU) + return 0; + cursor++; + } + return 1; +} + +int +BongoMaildropWrite(FILE *stream, const BongoMaildropSubmission *submission) +{ + unsigned int index; + size_t sender_length; + + if (stream == NULL || submission == NULL || + (submission->sender == NULL || + (submission->sender[0] != '\0' && + !BongoMaildropAddressValid(submission->sender))) || + submission->recipients == NULL || + submission->recipients->len == 0U || + submission->recipients->len > BONGO_MAILDROP_RECIPIENT_LIMIT || + submission->message == NULL || submission->message_length == 0U || + submission->message_length > BONGO_MAILDROP_MESSAGE_LIMIT) { + errno = EINVAL; + return -1; + } + sender_length = strlen(submission->sender); + for (index = 0U; index < submission->recipients->len; index++) { + if (!BongoMaildropAddressValid( + g_ptr_array_index(submission->recipients, index))) { + errno = EINVAL; + return -1; + } + } + if (WriteBytes(stream, MAILDROP_MAGIC, MAILDROP_MAGIC_LENGTH) != 0 || + WriteU64(stream, (uint64_t)submission->uid) != 0 || + WriteU32(stream, (uint32_t)sender_length) != 0 || + WriteU32(stream, submission->recipients->len) != 0 || + WriteU64(stream, submission->message_length) != 0 || + WriteBytes(stream, submission->sender, sender_length) != 0) + return -1; + for (index = 0U; index < submission->recipients->len; index++) { + const char *recipient = + g_ptr_array_index(submission->recipients, index); + size_t length = strlen(recipient); + if (WriteU32(stream, (uint32_t)length) != 0 || + WriteBytes(stream, recipient, length) != 0) + return -1; + } + return WriteBytes(stream, submission->message, + submission->message_length); +} + +int +BongoMaildropRead(FILE *stream, size_t message_limit, + unsigned int recipient_limit, + BongoMaildropSubmission *submission) +{ + unsigned char magic[MAILDROP_MAGIC_LENGTH]; + uint64_t uid; + uint64_t message_length; + uint32_t sender_length; + uint32_t recipient_count; + uint32_t length; + uint32_t index; + int trailing; + + if (stream == NULL || submission == NULL || message_limit == 0U || + recipient_limit == 0U) { + errno = EINVAL; + return -1; + } + memset(submission, 0, sizeof(*submission)); + if (ReadBytes(stream, magic, sizeof(magic)) != 0 || + memcmp(magic, MAILDROP_MAGIC, sizeof(magic)) != 0 || + ReadU64(stream, &uid) != 0 || + ReadU32(stream, &sender_length) != 0 || + ReadU32(stream, &recipient_count) != 0 || + ReadU64(stream, &message_length) != 0 || + sender_length > BONGO_MAILDROP_ADDRESS_LIMIT || + recipient_count == 0U || recipient_count > recipient_limit || + recipient_count > BONGO_MAILDROP_RECIPIENT_LIMIT || + message_length == 0U || message_length > message_limit || + message_length > BONGO_MAILDROP_MESSAGE_LIMIT || + message_length > SIZE_MAX) { + errno = EINVAL; + return -1; + } + submission->uid = (uid_t)uid; + if ((uint64_t)submission->uid != uid) { + errno = EINVAL; + return -1; + } + submission->sender = g_malloc((size_t)sender_length + 1U); + if (ReadBytes(stream, submission->sender, sender_length) != 0) + goto fail; + submission->sender[sender_length] = '\0'; + if (submission->sender[0] != '\0' && + !BongoMaildropAddressValid(submission->sender)) { + errno = EINVAL; + goto fail; + } + submission->recipients = + g_ptr_array_new_with_free_func(g_free); + for (index = 0U; index < recipient_count; index++) { + char *recipient; + if (ReadU32(stream, &length) != 0 || length == 0U || + length > BONGO_MAILDROP_ADDRESS_LIMIT) { + errno = EINVAL; + goto fail; + } + recipient = g_malloc((size_t)length + 1U); + if (ReadBytes(stream, recipient, length) != 0) { + g_free(recipient); + goto fail; + } + recipient[length] = '\0'; + if (!BongoMaildropAddressValid(recipient)) { + g_free(recipient); + errno = EINVAL; + goto fail; + } + g_ptr_array_add(submission->recipients, recipient); + } + submission->message = g_malloc((size_t)message_length); + submission->message_length = (size_t)message_length; + if (ReadBytes(stream, submission->message, + submission->message_length) != 0) + goto fail; + trailing = fgetc(stream); + if (trailing != EOF || ferror(stream)) { + errno = EINVAL; + goto fail; + } + return 0; + +fail: + BongoMaildropSubmissionClear(submission); + return -1; +} + +void +BongoMaildropSubmissionClear(BongoMaildropSubmission *submission) +{ + if (submission == NULL) return; + g_free(submission->sender); + if (submission->recipients != NULL) + g_ptr_array_free(submission->recipients, TRUE); + g_free(submission->message); + memset(submission, 0, sizeof(*submission)); +} diff --git a/src/libs/util/tests/maildrop-test.c b/src/libs/util/tests/maildrop-test.c new file mode 100644 index 0000000..ec5e19b --- /dev/null +++ b/src/libs/util/tests/maildrop-test.c @@ -0,0 +1,67 @@ +/**************************************************************************** + * + * Copyright (c) 2001 Novell, Inc. All Rights Reserved. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of version 2 of the GNU General Public License + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + ****************************************************************************/ + +#include +#include +#include +#include + +#include + +int +main(void) +{ + static const unsigned char message[] = + "Subject: local submission\r\n\r\nbody\r\n"; + BongoMaildropSubmission input = { 0 }; + BongoMaildropSubmission output = { 0 }; + FILE *stream = tmpfile(); + + assert(stream != NULL); + input.uid = 1234; + input.sender = g_strdup("sender@example.test"); + input.recipients = g_ptr_array_new_with_free_func(g_free); + g_ptr_array_add(input.recipients, g_strdup("one@example.test")); + g_ptr_array_add(input.recipients, g_strdup("two@example.test")); + input.message = g_memdup2(message, sizeof(message) - 1U); + input.message_length = sizeof(message) - 1U; + + assert(BongoMaildropWrite(stream, &input) == 0); + assert(fflush(stream) == 0); + rewind(stream); + assert(BongoMaildropRead(stream, 1024U, 10U, &output) == 0); + assert(output.uid == input.uid); + assert(strcmp(output.sender, input.sender) == 0); + assert(output.recipients->len == 2U); + assert(strcmp(g_ptr_array_index(output.recipients, 1), + "two@example.test") == 0); + assert(output.message_length == input.message_length); + assert(memcmp(output.message, input.message, input.message_length) == 0); + BongoMaildropSubmissionClear(&output); + BongoMaildropSubmissionClear(&input); + fclose(stream); + + assert(!BongoMaildropAddressValid("bad\naddress@example.test")); + assert(!BongoMaildropAddressValid("")); + stream = tmpfile(); + assert(stream != NULL); + assert(fwrite("broken", 1U, 6U, stream) == 6U); + rewind(stream); + errno = 0; + assert(BongoMaildropRead(stream, 1024U, 10U, &output) == -1); + assert(errno == EINVAL); + fclose(stream); + return 0; +}