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;
+}