Implement durable C local mail submission
Debian Trixie package bundle / packages (push) Successful in 22m11s
Debian Trixie package bundle / packages (push) Successful in 22m11s
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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@"
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
Vendored
+11
-2
@@ -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#
|
||||
|
||||
Vendored
+5
@@ -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
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
/****************************************************************************
|
||||
* <Novell-copyright>
|
||||
* 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.
|
||||
* </Novell-copyright>
|
||||
****************************************************************************/
|
||||
|
||||
#ifndef BONGO_MAILDROP_H
|
||||
#define BONGO_MAILDROP_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <sys/types.h>
|
||||
|
||||
#include <glib.h>
|
||||
|
||||
#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
|
||||
@@ -19,4 +19,6 @@
|
||||
# * </Novell-copyright>
|
||||
# ****************************************************************************/
|
||||
|
||||
g bongopostdrop - -
|
||||
u @BONGO_USER@ - "Bongo mail and calendar server" @XPL_DEFAULT_STATE_DIR@ -
|
||||
m @BONGO_USER@ bongopostdrop
|
||||
|
||||
@@ -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@ -
|
||||
|
||||
@@ -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
|
||||
|
||||
+26
-5
@@ -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 <home@alexhudson.com> for the
|
||||
|
||||
@@ -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)
|
||||
@@ -23,8 +23,12 @@
|
||||
#include <config.h>
|
||||
|
||||
#include <errno.h>
|
||||
#include <dirent.h>
|
||||
#include <fcntl.h>
|
||||
#include <inttypes.h>
|
||||
#include <stdint.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#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 <bongomaildrop.h>
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
$<TARGET_FILE:bongo-sendmail-test-bin>
|
||||
${SENDMAIL_TEST_MAILDROP})
|
||||
endif()
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# /****************************************************************************
|
||||
# * <Novell-copyright>
|
||||
# * 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.
|
||||
# * </Novell-copyright>
|
||||
# ****************************************************************************/
|
||||
|
||||
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@"))
|
||||
@@ -0,0 +1,162 @@
|
||||
/****************************************************************************
|
||||
* <Novell-copyright>
|
||||
* 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.
|
||||
* </Novell-copyright>
|
||||
****************************************************************************/
|
||||
|
||||
#include <bongomaildrop.h>
|
||||
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <signal.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/random.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/types.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#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;
|
||||
}
|
||||
+478
-321
@@ -1,355 +1,512 @@
|
||||
/****************************************************************************
|
||||
* <Novell-copyright>
|
||||
* 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.
|
||||
* </Novell-copyright>
|
||||
****************************************************************************/
|
||||
|
||||
/* 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 <bongomaildrop.h>
|
||||
|
||||
#include <config.h>
|
||||
#include <xpl.h>
|
||||
#include <ctype.h>
|
||||
#include <errno.h>
|
||||
#include <bongoutil.h>
|
||||
#include <nmap.h>
|
||||
#include <nmlib.h>
|
||||
#include <pwd.h>
|
||||
#include <signal.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/wait.h>
|
||||
#include <unistd.h>
|
||||
#include <msgapi.h>
|
||||
#include <pwd.h>
|
||||
|
||||
#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 <mary@x.test>, jdoe@example.org, Who? <one@y.test>
|
||||
* Cc: <boss@nil.test>, "Giant; \"Big\" Box" <sysservices@example.net>
|
||||
* 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;
|
||||
}
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
/****************************************************************************
|
||||
* <Novell-copyright>
|
||||
* 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.
|
||||
* </Novell-copyright>
|
||||
****************************************************************************/
|
||||
|
||||
#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 */
|
||||
@@ -0,0 +1,110 @@
|
||||
#!/usr/bin/env python3
|
||||
# /****************************************************************************
|
||||
# * <Novell-copyright>
|
||||
# * 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.
|
||||
# * </Novell-copyright>
|
||||
# ****************************************************************************/
|
||||
|
||||
"""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 <sender@example.test>\r\n"
|
||||
b"To: One <one@example.test>\r\n"
|
||||
b"Cc: two@example.test\r\n"
|
||||
b"Bcc: Hidden <hidden@example.test>\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())
|
||||
@@ -1,125 +0,0 @@
|
||||
# /****************************************************************************
|
||||
# * <Novell-copyright>
|
||||
# * 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.
|
||||
# * </Novell-copyright>
|
||||
# ****************************************************************************/
|
||||
|
||||
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 <one@example.test>\r\n"
|
||||
b"Cc: two@example.test\r\n"
|
||||
b"Bcc: Hidden <hidden@example.test>\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()
|
||||
@@ -1,267 +0,0 @@
|
||||
# /****************************************************************************
|
||||
# * <Novell-copyright>
|
||||
# * 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.
|
||||
# * </Novell-copyright>
|
||||
# ****************************************************************************/
|
||||
|
||||
"""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
|
||||
@@ -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()
|
||||
|
||||
@@ -0,0 +1,257 @@
|
||||
/****************************************************************************
|
||||
* <Novell-copyright>
|
||||
* 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.
|
||||
* </Novell-copyright>
|
||||
****************************************************************************/
|
||||
|
||||
#include <bongomaildrop.h>
|
||||
|
||||
#include <errno.h>
|
||||
#include <limits.h>
|
||||
#include <string.h>
|
||||
|
||||
#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));
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
/****************************************************************************
|
||||
* <Novell-copyright>
|
||||
* 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.
|
||||
* </Novell-copyright>
|
||||
****************************************************************************/
|
||||
|
||||
#include <assert.h>
|
||||
#include <errno.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#include <bongomaildrop.h>
|
||||
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user