Route external identities through provider SMTP

This commit is contained in:
Mario Fetka
2026-07-16 18:52:00 +02:00
parent 7a2f562c1c
commit eafdaea491
29 changed files with 649 additions and 63 deletions
+3 -3
View File
@@ -13,8 +13,8 @@ target_link_libraries(bongocollector
bongostreamio
bongomsgapi
bongocollectoraccounts
${CURL_LIBRARIES}
${SQLITE_LIBRARIES}
CURL::libcurl
SQLite3::SQLite3
)
install(TARGETS bongocollector DESTINATION ${SBIN_INSTALL_DIR})
@@ -27,6 +27,6 @@ if(BUILD_TESTING)
target_include_directories(collector-transport-test PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}
)
target_link_libraries(collector-transport-test ${CURL_LIBRARIES})
target_link_libraries(collector-transport-test CURL::libcurl)
add_test(NAME collector-external-transport COMMAND collector-transport-test)
endif()
+6
View File
@@ -73,6 +73,12 @@ static int CollectExternalAccount(sqlite3 *database,
CollectorAccountsMarkResult(database, account->id, now, 0, error);
goto done;
}
/* A successful mailbox listing proves the stored credentials even when a
later individual message cannot be imported. */
if (CollectorAccountsMarkVerified(database, account->id, now) != 0) {
snprintf(error, sizeof(error), "cannot verify external sender identity");
goto failed;
}
for (index = 0; index < ids.count && processed < EXTERNAL_MESSAGES_PER_POLL; index++) {
FILE *message;
size_t message_size;
+93
View File
@@ -322,6 +322,99 @@ CollectorAccountsMarkResult(sqlite3 *database, sqlite3_int64 id, long now,
return result ? 0 : -1;
}
int
CollectorAccountsMarkVerified(sqlite3 *database, sqlite3_int64 id, long now)
{
sqlite3_stmt *statement = NULL;
int result = -1;
if (database == NULL || id <= 0) return -1;
if (sqlite3_exec(database, "BEGIN IMMEDIATE", NULL, NULL, NULL) != SQLITE_OK) return -1;
if (sqlite3_prepare_v2(database,
"UPDATE external_accounts SET verified_at = COALESCE(verified_at, ?), "
"updated_at = ? WHERE id = ?", -1, &statement, NULL) != SQLITE_OK) goto done;
sqlite3_bind_int64(statement, 1, (sqlite3_int64) now);
sqlite3_bind_int64(statement, 2, (sqlite3_int64) now);
sqlite3_bind_int64(statement, 3, id);
if (sqlite3_step(statement) != SQLITE_DONE || sqlite3_changes(database) != 1) goto done;
sqlite3_finalize(statement);
statement = NULL;
/* Successful mailbox authentication proves only the address coupled to
this account. Additional sender identities require their own proof. */
if (sqlite3_prepare_v2(database,
"UPDATE external_account_identities SET verified_at = COALESCE(verified_at, ?) "
"WHERE account_id = ? AND email_address = "
"(SELECT email_address FROM external_accounts WHERE id = ?)",
-1, &statement, NULL) != SQLITE_OK) goto done;
sqlite3_bind_int64(statement, 1, (sqlite3_int64) now);
sqlite3_bind_int64(statement, 2, id);
sqlite3_bind_int64(statement, 3, id);
if (sqlite3_step(statement) != SQLITE_DONE || sqlite3_changes(database) != 1) goto done;
sqlite3_finalize(statement);
statement = NULL;
if (sqlite3_exec(database, "COMMIT", NULL, NULL, NULL) != SQLITE_OK) goto done;
result = 0;
done:
if (statement != NULL) sqlite3_finalize(statement);
if (result != 0) sqlite3_exec(database, "ROLLBACK", NULL, NULL, NULL);
return result;
}
int
CollectorAccountsOutboundForSender(sqlite3 *database, const char *owner,
const char *sender,
CollectorOutboundAccount *account)
{
sqlite3_stmt *statement = NULL;
const char query[] =
"SELECT a.id, i.email_address, a.outbound_mode, "
"COALESCE(a.outbound_host, ''), COALESCE(a.outbound_port, 0), "
"a.outbound_tls, COALESCE(a.outbound_username, ''), "
"COALESCE(a.outbound_secret_id, '') "
"FROM external_account_identities i "
"JOIN external_accounts a ON a.id = i.account_id "
"WHERE a.owner = ? COLLATE NOCASE "
"AND i.email_address = ? COLLATE NOCASE "
"AND a.enabled = 1 AND i.verified_at IS NOT NULL "
"ORDER BY i.is_default DESC, a.id LIMIT 1";
int status;
if (database == NULL || owner == NULL || *owner == '\0' ||
sender == NULL || *sender == '\0' || account == NULL ||
sqlite3_prepare_v2(database, query, -1, &statement, NULL) != SQLITE_OK) {
return -1;
}
memset(account, 0, sizeof(*account));
sqlite3_bind_text(statement, 1, owner, -1, SQLITE_TRANSIENT);
sqlite3_bind_text(statement, 2, sender, -1, SQLITE_TRANSIENT);
status = sqlite3_step(statement);
if (status == SQLITE_DONE) {
sqlite3_finalize(statement);
return 0;
}
if (status != SQLITE_ROW) {
sqlite3_finalize(statement);
return -1;
}
account->account_id = sqlite3_column_int64(statement, 0);
#define COPY_OUTBOUND_FIELD(column, field) \
snprintf(account->field, sizeof(account->field), "%s", \
sqlite3_column_text(statement, (column)) != NULL ? \
(const char *) sqlite3_column_text(statement, (column)) : "")
COPY_OUTBOUND_FIELD(1, email_address);
COPY_OUTBOUND_FIELD(2, mode);
COPY_OUTBOUND_FIELD(3, host);
account->port = sqlite3_column_int(statement, 4);
COPY_OUTBOUND_FIELD(5, tls);
COPY_OUTBOUND_FIELD(6, username);
COPY_OUTBOUND_FIELD(7, secret_id);
#undef COPY_OUTBOUND_FIELD
sqlite3_finalize(statement);
return 1;
}
int
CollectorAccountsSecretSet(sqlite3 *database, const char *id,
const char *owner, const char *secret, long now)
+16
View File
@@ -21,6 +21,17 @@ typedef struct {
int delete_after_days;
} CollectorExternalAccount;
typedef struct {
sqlite3_int64 account_id;
char email_address[320];
char mode[16];
char host[256];
int port;
char tls[16];
char username[256];
char secret_id[128];
} CollectorOutboundAccount;
sqlite3 *CollectorAccountsOpen(void);
void CollectorAccountsClose(sqlite3 *database);
int CollectorAccountsDueCount(sqlite3 *database, long now);
@@ -28,6 +39,11 @@ int CollectorAccountsNextDue(sqlite3 *database, long now,
CollectorExternalAccount *account);
int CollectorAccountsMarkResult(sqlite3 *database, sqlite3_int64 id,
long now, int success, const char *error);
int CollectorAccountsMarkVerified(sqlite3 *database, sqlite3_int64 id,
long now);
int CollectorAccountsOutboundForSender(sqlite3 *database, const char *owner,
const char *sender,
CollectorOutboundAccount *account);
int CollectorAccountsSecretSet(sqlite3 *database, const char *id,
const char *owner, const char *secret, long now);
int CollectorAccountsSecretGet(sqlite3 *database, const char *id,
+3
View File
@@ -11,6 +11,7 @@ target_link_libraries(bongosmtp
bongojson
bongomsgapi
bongomailauth
bongocollectoraccounts
)
add_executable(bongosmtpc
@@ -24,6 +25,8 @@ target_link_libraries(bongosmtpc
bongojson
bongomsgapi
bongomailauth
bongocollectoraccounts
CURL::libcurl
)
install(TARGETS bongosmtp DESTINATION ${SBIN_INSTALL_DIR})
+195 -21
View File
@@ -23,6 +23,8 @@
#include <config.h>
#include <curl/curl.h>
#include <xpl.h>
#include <memmgr.h>
#include <logger.h>
@@ -125,7 +127,8 @@ GenerateDKIMSignature(SMTPClient *queue)
BongoMailAuthStatus status;
queue->hasDKIMSignature = FALSE;
if (!MailAuth.dkim_sign_outgoing) {
/* External submission providers apply their own aligned DKIM signature. */
if (!MailAuth.dkim_sign_outgoing || queue->hasExternalRelay) {
return BONGO_MAILAUTH_OK;
}
/* Do not add a new author signature to unauthenticated forwarded mail. */
@@ -442,6 +445,172 @@ ConnectLMTP(RecipStruct *recipient, const char *host, int targetPort)
return connection;
}
static int
ResolveExternalRelay(SMTPClient *queue)
{
sqlite3 *database;
int found;
memset(&queue->externalRelay, 0, sizeof(queue->externalRelay));
queue->hasExternalRelay = FALSE;
if (queue->authSender == NULL || queue->authSender[0] == '-' ||
queue->sender == NULL || queue->sender[0] == '-' ||
queue->sender[0] == '\0') return 0;
database = CollectorAccountsOpen();
if (database == NULL) return -1;
found = CollectorAccountsOutboundForSender(
database, queue->authSender, queue->sender, &queue->externalRelay);
CollectorAccountsClose(database);
if (found < 0) return -1;
queue->hasExternalRelay = found == 1 &&
strcmp(queue->externalRelay.mode, "smtp") == 0;
return 0;
}
static void
EraseSecret(char *secret, size_t length)
{
volatile unsigned char *cursor = (volatile unsigned char *) secret;
while (length-- > 0) *cursor++ = 0;
}
static int
QueueMessageToFile(SMTPClient *queue, FILE *message, curl_off_t *messageSize)
{
char timeBuffer[80];
char *line = NULL;
unsigned long lineSize = 0;
long long remaining;
int result = -1;
MsgGetRFC822Date(-1, 0, timeBuffer);
if (fprintf(message,
"Received: from %s (%d.%d.%d.%d) by %s\r\n"
"\twith NMAP (bongosmtpc Agent); %s\r\n",
BongoGlobals.hostname,
queue->conn->socketAddress.sin_addr.s_net,
queue->conn->socketAddress.sin_addr.s_host,
queue->conn->socketAddress.sin_addr.s_lh,
queue->conn->socketAddress.sin_addr.s_impno,
BongoGlobals.hostname, timeBuffer) < 0) goto done;
if (queue->hasDKIMSignature &&
fprintf(message, "DKIM-Signature: %s\r\n", queue->dkimSignature) < 0) goto done;
ConnWriteF(queue->conn, "QRETR %s MESSAGE\r\n", queue->qID);
ConnFlush(queue->conn);
ConnReadAnswer(queue->conn, queue->line, CONN_BUFSIZE);
if (atoi(queue->line) != 2023) goto done;
remaining = atoll(&queue->line[5]);
while (remaining > 0) {
size_t length;
size_t consumed;
ConnReadToAllocatedBuffer(queue->conn, &line, &lineSize);
if (line == NULL) goto done;
length = strlen(line);
if (fwrite(line, 1, length, message) != length ||
fwrite("\r\n", 1, 2, message) != 2) goto done;
consumed = length + 2U;
remaining -= remaining > (long long) consumed
? (long long) consumed : remaining;
}
ConnReadAnswer(queue->conn, queue->line, CONN_BUFSIZE);
if (atoi(queue->line) != 1000 || fflush(message) != 0) goto done;
{
off_t end = ftello(message);
if (end < 0 || fseeko(message, 0, SEEK_SET) != 0) goto done;
*messageSize = (curl_off_t) end;
}
result = 0;
done:
if (line != NULL) MemFree(line);
return result;
}
static BOOL
DeliverExternalRelay(SMTPClient *queue, RecipStruct *recipient)
{
sqlite3 *database = NULL;
CURL *curl = NULL;
struct curl_slist *recipients = NULL;
FILE *message = NULL;
char secret[4097] = {0};
char url[560];
char curlError[CURL_ERROR_SIZE] = {0};
char rewrittenSender[1024];
const char *envelopeSender = NULL;
curl_off_t messageSize = 0;
CURLcode code = CURLE_FAILED_INIT;
long response = 0;
BOOL success = FALSE;
recipient->Result = DELIVER_TRY_LATER;
if (!queue->hasExternalRelay || queue->externalRelay.host[0] == '\0' ||
queue->externalRelay.port < 1 || queue->externalRelay.port > 65535 ||
queue->externalRelay.username[0] == '\0' ||
queue->externalRelay.secret_id[0] == '\0' ||
(strcmp(queue->externalRelay.tls, "implicit") != 0 &&
strcmp(queue->externalRelay.tls, "starttls") != 0) ||
strpbrk(queue->externalRelay.host, "/?#@\r\n") != NULL ||
GetEnvelopeSender(queue, rewrittenSender, sizeof(rewrittenSender),
&envelopeSender) != BONGO_MAILAUTH_OK) goto done;
if (snprintf(url, sizeof(url), "%s://%s:%d",
strcmp(queue->externalRelay.tls, "implicit") == 0
? "smtps" : "smtp",
queue->externalRelay.host,
queue->externalRelay.port) >= (int) sizeof(url)) goto done;
database = CollectorAccountsOpen();
if (database == NULL || CollectorAccountsSecretGet(
database, queue->externalRelay.secret_id,
secret, sizeof(secret)) != 1) goto done;
CollectorAccountsClose(database);
database = NULL;
message = tmpfile();
if (message == NULL || QueueMessageToFile(queue, message, &messageSize) != 0) goto done;
curl = curl_easy_init();
recipients = curl_slist_append(recipients, recipient->To);
if (curl == NULL || recipients == NULL) goto done;
curl_easy_setopt(curl, CURLOPT_URL, url);
curl_easy_setopt(curl, CURLOPT_USERNAME, queue->externalRelay.username);
curl_easy_setopt(curl, CURLOPT_PASSWORD, secret);
curl_easy_setopt(curl, CURLOPT_MAIL_FROM, envelopeSender);
curl_easy_setopt(curl, CURLOPT_MAIL_RCPT, recipients);
curl_easy_setopt(curl, CURLOPT_UPLOAD, 1L);
curl_easy_setopt(curl, CURLOPT_READDATA, message);
curl_easy_setopt(curl, CURLOPT_INFILESIZE_LARGE, messageSize);
curl_easy_setopt(curl, CURLOPT_USE_SSL, CURLUSESSL_ALL);
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L);
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 2L);
curl_easy_setopt(curl, CURLOPT_NETRC, CURL_NETRC_IGNORED);
curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1L);
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 30L);
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 300L);
curl_easy_setopt(curl, CURLOPT_ERRORBUFFER, curlError);
code = curl_easy_perform(curl);
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &response);
if (code == CURLE_OK && response >= 200 && response < 300) {
recipient->Result = DELIVER_SUCCESS;
success = TRUE;
} else if (response >= 500 && code != CURLE_LOGIN_DENIED) {
recipient->Result = DELIVER_FAILURE;
}
Log(success ? LOG_INFO : LOG_WARN,
"External SMTP relay %s returned %ld (%s) for message %s to %s",
queue->externalRelay.host, response,
code == CURLE_OK ? "SMTP response" :
(curlError[0] != '\0' ? curlError : curl_easy_strerror(code)),
queue->qID, recipient->To);
done:
if (database != NULL) CollectorAccountsClose(database);
if (curl != NULL) curl_easy_cleanup(curl);
if (recipients != NULL) curl_slist_free_all(recipients);
if (message != NULL) fclose(message);
EraseSecret(secret, sizeof(secret));
return success;
}
BOOL
DeliverMessage(SMTPClient *Queue, SMTPClient *Remote, RecipStruct *Recip) {
int Extensions;
@@ -878,6 +1047,12 @@ ProcessEntry(void *clientp, Connection *conn)
}
/* now that we've got all recipients and locations, let's sort them to use connections more efficiently */
if (ResolveExternalRelay(Queue) != 0) {
Log(LOG_ERROR, "Cannot resolve external sender identity for queue %s",
Queue->qID);
g_array_free(Recipients, TRUE);
return -1;
}
{
BongoMailAuthStatus dkimStatus = GenerateDKIMSignature(Queue);
if (dkimStatus != BONGO_MAILAUTH_OK) {
@@ -912,27 +1087,26 @@ ProcessEntry(void *clientp, Connection *conn)
/* by the time i get here, CurrentRecip should be the last recip
* of any string of duplicates */
Remote.isLMTP = FindLMTPTransport(
&CurrentRecip, Remote.transportHost,
sizeof(Remote.transportHost), &Remote.transportPort);
Remote.conn = Remote.isLMTP
? ConnectLMTP(&CurrentRecip, Remote.transportHost,
Remote.transportPort)
: LookupRemoteMX(&CurrentRecip);
if (!Remote.conn) {
/* there was an error looking up or connecting to the remote server
* if i get an error in this stage of the process i need to just skip
* the rest of the email addresses on this domain */
/* FIXME: */
continue;
}
if (Queue->hasExternalRelay) {
DeliverExternalRelay(Queue, &CurrentRecip);
} else {
Remote.isLMTP = FindLMTPTransport(
&CurrentRecip, Remote.transportHost,
sizeof(Remote.transportHost), &Remote.transportPort);
Remote.conn = Remote.isLMTP
? ConnectLMTP(&CurrentRecip, Remote.transportHost,
Remote.transportPort)
: LookupRemoteMX(&CurrentRecip);
if (!Remote.conn) {
/* There was an error looking up or connecting to the remote server. */
continue;
}
DeliverMessage(Queue, &Remote, &CurrentRecip);
// we're responsible for closing this connection
ConnClose(Remote.conn);
ConnFree(Remote.conn);
Remote.conn = NULL;
DeliverMessage(Queue, &Remote, &CurrentRecip);
ConnClose(Remote.conn);
ConnFree(Remote.conn);
Remote.conn = NULL;
}
/* now handle delivery result codes */
switch(CurrentRecip.Result) {
+4
View File
@@ -30,6 +30,8 @@
#include <bongoagent.h>
#include <xpldns.h>
#include "external_accounts.h"
#include "smtp.h"
#define AGENT_NAME "smtpd_c"
@@ -48,6 +50,8 @@ typedef struct {
char qID[16]; /* holds the queueid pulled during handshake */
BOOL hasDKIMSignature;
char dkimSignature[8192];
BOOL hasExternalRelay;
CollectorOutboundAccount externalRelay;
BOOL isLMTP;
char transportHost[256];
int transportPort;
+16 -4
View File
@@ -45,6 +45,7 @@
#include <bongoutil.h>
#include "mailauth.h"
#include "smtpd.h"
#include "external_accounts.h"
struct {
int port;
@@ -516,13 +517,24 @@ SubmissionSenderAllowed(const char *user, const char *sender)
{
const char *at;
size_t localLength;
sqlite3 *database;
CollectorOutboundAccount account;
int found;
if (SMTP.submission_allow_sender_override || user == NULL || sender == NULL ||
*sender == '\0') return TRUE;
if (strcasecmp(user, sender) == 0) return TRUE;
if (strchr(user, '@') != NULL) return FALSE;
at = strrchr(sender, '@');
localLength = at != NULL ? (size_t) (at - sender) : strlen(sender);
return strlen(user) == localLength && strncasecmp(user, sender, localLength) == 0;
if (strchr(user, '@') == NULL) {
at = strrchr(sender, '@');
localLength = at != NULL ? (size_t) (at - sender) : strlen(sender);
if (strlen(user) == localLength &&
strncasecmp(user, sender, localLength) == 0) return TRUE;
}
database = CollectorAccountsOpen();
if (database == NULL) return FALSE;
found = CollectorAccountsOutboundForSender(database, user, sender, &account);
CollectorAccountsClose(database);
return found == 1;
}
static BOOL
+3 -3
View File
@@ -39,11 +39,11 @@ target_link_libraries(bongostore
bongoutil
bongojson
bongomsgapi
${SQLITE_LIBRARIES}
${GLIB2_LIBRARIES}
SQLite3::SQLite3
GLib2::GLib2
${GCRYPT_LIBRARIES}
${ICAL_LIBRARIES}
${GMIME2_LIBRARIES}
GMime::GMime
)
install(TARGETS bongostore DESTINATION ${SBIN_INSTALL_DIR})
-1
View File
@@ -15,7 +15,6 @@ add_library(bongocal SHARED
#target_link_libraries(bongo-import-tz
# bongomemmgr
# bongocal
# ${GLIB2_LIBRARIES}
# ${ICAL_LIBRARIES}
# )
+12 -1
View File
@@ -23,7 +23,18 @@ target_include_directories(bongocollectoraccounts PUBLIC
target_link_libraries(bongocollectoraccounts
bongoxpl
${GCRYPT_LIBRARIES}
${SQLITE_LIBRARIES}
SQLite3::SQLite3
)
install(TARGETS bongocollectoraccounts DESTINATION ${LIB_INSTALL_DIR})
if(BUILD_TESTING)
add_executable(collector-accounts-test
tests/external-accounts-test.c
)
target_link_libraries(collector-accounts-test
bongocollectoraccounts
SQLite3::SQLite3
)
add_test(NAME collector-external-accounts COMMAND collector-accounts-test)
endif()
@@ -0,0 +1,76 @@
#include <sqlite3.h>
#include <stdio.h>
#include <string.h>
#include "external_accounts.h"
static int
Check(int condition, const char *message)
{
if (condition) return 0;
fprintf(stderr, "%s\n", message);
return 1;
}
int
main(void)
{
sqlite3 *database = NULL;
CollectorOutboundAccount account;
sqlite3_stmt *statement = NULL;
int primaryVerified = 0;
int aliasVerified = 1;
int failed = 0;
const char schema[] =
"CREATE TABLE external_accounts ("
"id INTEGER PRIMARY KEY, owner TEXT, email_address TEXT, enabled INTEGER, "
"verified_at INTEGER, updated_at INTEGER, outbound_mode TEXT, outbound_host TEXT, "
"outbound_port INTEGER, outbound_tls TEXT, outbound_username TEXT, outbound_secret_id TEXT);"
"CREATE TABLE external_account_identities ("
"account_id INTEGER, email_address TEXT, verified_at INTEGER, is_default INTEGER);"
"INSERT INTO external_accounts VALUES "
"(1, 'alice', 'alice@example.test', 1, NULL, 1, 'smtp', "
"'smtp.example.test', 587, 'starttls', 'alice@example.test', 'secret-1');"
"INSERT INTO external_account_identities VALUES "
"(1, 'alice@example.test', NULL, 1), (1, 'alias@example.test', NULL, 0);";
failed |= Check(sqlite3_open(":memory:", &database) == SQLITE_OK,
"cannot open in-memory database");
failed |= Check(!failed && sqlite3_exec(database, schema, NULL, NULL, NULL) == SQLITE_OK,
"cannot initialize identity test schema");
failed |= Check(!failed && CollectorAccountsOutboundForSender(
database, "alice", "alice@example.test", &account) == 0,
"unverified identity was accepted");
failed |= Check(!failed && CollectorAccountsMarkVerified(database, 1, 42) == 0,
"cannot verify collected account identity");
failed |= Check(!failed && sqlite3_prepare_v2(database,
"SELECT verified_at IS NOT NULL FROM external_account_identities "
"WHERE account_id=1 ORDER BY is_default DESC", -1, &statement, NULL) == SQLITE_OK,
"cannot query verification state");
if (!failed && sqlite3_step(statement) == SQLITE_ROW) {
primaryVerified = sqlite3_column_int(statement, 0);
}
if (!failed && sqlite3_step(statement) == SQLITE_ROW) {
aliasVerified = sqlite3_column_int(statement, 0);
}
sqlite3_finalize(statement);
statement = NULL;
failed |= Check(primaryVerified == 1, "primary identity was not verified");
failed |= Check(aliasVerified == 0, "additional identity was verified implicitly");
failed |= Check(!failed && CollectorAccountsOutboundForSender(
database, "ALICE", "ALICE@EXAMPLE.TEST", &account) == 1,
"verified identity was not resolved case-insensitively");
failed |= Check(!failed && account.account_id == 1 && account.port == 587 &&
strcmp(account.host, "smtp.example.test") == 0,
"resolved SMTP account is incorrect");
failed |= Check(!failed && CollectorAccountsOutboundForSender(
database, "alice", "alias@example.test", &account) == 0,
"unverified additional identity was accepted");
failed |= Check(!failed && CollectorAccountsOutboundForSender(
database, "mallory", "alice@example.test", &account) == 0,
"another user's identity was accepted");
sqlite3_close(database);
return failed ? 1 : 0;
}
+1 -1
View File
@@ -10,7 +10,7 @@ add_library(bongoconnio SHARED
)
target_link_libraries(bongoconnio
${GNUTLS_LIBRARIES}
GnuTLS::GnuTLS
)
install(TARGETS bongoconnio DESTINATION ${LIB_INSTALL_DIR})
+1 -2
View File
@@ -9,7 +9,6 @@ add_library(bongomailauth
target_include_directories(bongomailauth
PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}
${PSL_INCLUDE_DIRS}
)
target_link_libraries(bongomailauth
@@ -18,7 +17,7 @@ target_link_libraries(bongomailauth
OpenDKIM::OpenDKIM
OpenDMARC::OpenDMARC
SRS2::SRS2
${PSL_LIBRARIES}
LibPSL::LibPSL
)
install(TARGETS bongomailauth DESTINATION ${LIB_INSTALL_DIR})
+2 -2
View File
@@ -14,9 +14,9 @@ target_link_libraries(bongomsgapi
bongoxpl
bongocal
bongojson
${SQLITE_LIBRARIES}
SQLite3::SQLite3
${LIBICAL_LIBRARIES}
${CURL_LIBRARIES}
CURL::libcurl
)
install(TARGETS bongomsgapi DESTINATION ${LIB_INSTALL_DIR})
+1 -1
View File
@@ -62,7 +62,7 @@ target_link_libraries(_calendar
target_link_libraries(_external_accounts
bongocollectoraccounts
bongoxpl
${GLIB2_LIBRARIES}
GLib2::GLib2
${GCRYPT_LIBRARIES}
${PYTHON_LIBRARIES}
)
@@ -95,37 +95,48 @@ AccountCreate(PyObject *self, PyObject *args)
{
const char *owner, *secret, *label, *email, *protocol, *host, *tls;
const char *username, *mailbox, *folder, *delete_policy;
const char *outbound_mode, *outbound_host, *outbound_tls, *outbound_username;
PyObject *settings;
sqlite3 *database = NULL;
sqlite3_stmt *statement = NULL;
char secret_id[33];
long port, poll, delete_days;
long port, poll, delete_days, outbound_port;
long now = (long) time(NULL);
sqlite3_int64 account_id;
int ok = 0;
const char insert[] = "INSERT INTO external_accounts "
"(owner, label, email_address, inbound_protocol, inbound_host, inbound_port, "
"inbound_tls, inbound_username, inbound_secret_id, inbound_mailbox, poll_interval, "
"destination_folder, delete_policy, delete_after_days, outbound_mode, created_at, updated_at) "
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'direct', ?, ?)";
"destination_folder, delete_policy, delete_after_days, outbound_mode, outbound_host, "
"outbound_port, outbound_tls, outbound_username, outbound_secret_id, created_at, updated_at) "
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
(void) self;
if (!PyArg_ParseTuple(args, "sO!s", &owner, &PyDict_Type, &settings, &secret)) return NULL;
port = GetLong(settings, "inbound_port");
poll = GetLong(settings, "poll_interval");
delete_days = GetLong(settings, "delete_after_days");
outbound_port = GetLong(settings, "outbound_port");
if (!ValidText(owner, 255) || !ValidText(secret, 4096) ||
GetText(settings, "label", 255, &label) || GetText(settings, "email_address", 319, &email) ||
GetText(settings, "inbound_protocol", 7, &protocol) || GetText(settings, "inbound_host", 255, &host) ||
GetText(settings, "inbound_tls", 15, &tls) || GetText(settings, "inbound_username", 255, &username) ||
GetText(settings, "inbound_mailbox", 255, &mailbox) || GetText(settings, "destination_folder", 255, &folder) ||
GetText(settings, "delete_policy", 15, &delete_policy) ||
GetText(settings, "outbound_mode", 15, &outbound_mode) ||
GetText(settings, "outbound_host", 255, &outbound_host) ||
GetText(settings, "outbound_tls", 15, &outbound_tls) ||
GetText(settings, "outbound_username", 255, &outbound_username) ||
(strcmp(protocol, "imap") && strcmp(protocol, "pop3")) ||
(strcmp(tls, "implicit") && strcmp(tls, "starttls")) ||
(strcmp(delete_policy, "never") && strcmp(delete_policy, "after_import") && strcmp(delete_policy, "after_days")) ||
port < 1 || port > 65535 || poll < 60 || poll > 86400 ||
delete_days < 0 || delete_days > 3650 ||
(!strcmp(delete_policy, "after_days") && delete_days == 0) ||
strpbrk(host, "/?#@") != NULL || strpbrk(mailbox, "\"\\") != NULL || strpbrk(folder, "\"\\") != NULL) {
(strcmp(outbound_mode, "direct") && strcmp(outbound_mode, "smtp")) ||
(strcmp(outbound_tls, "implicit") && strcmp(outbound_tls, "starttls")) ||
outbound_port < 1 || outbound_port > 65535 ||
strpbrk(host, "/?#@") != NULL || strpbrk(outbound_host, "/?#@") != NULL ||
strpbrk(mailbox, "\"\\") != NULL || strpbrk(folder, "\"\\") != NULL) {
return PyErr_Format(PyExc_ValueError, "invalid external account settings");
}
database = CollectorAccountsOpen();
@@ -150,11 +161,31 @@ AccountCreate(PyObject *self, PyObject *args)
sqlite3_bind_text(statement, 12, folder, -1, SQLITE_TRANSIENT);
sqlite3_bind_text(statement, 13, delete_policy, -1, SQLITE_TRANSIENT);
if (delete_days > 0) sqlite3_bind_int(statement, 14, (int) delete_days); else sqlite3_bind_null(statement, 14);
sqlite3_bind_int64(statement, 15, now);
sqlite3_bind_int64(statement, 16, now);
sqlite3_bind_text(statement, 15, outbound_mode, -1, SQLITE_TRANSIENT);
if (!strcmp(outbound_mode, "smtp")) {
sqlite3_bind_text(statement, 16, outbound_host, -1, SQLITE_TRANSIENT);
sqlite3_bind_int(statement, 17, (int) outbound_port);
sqlite3_bind_text(statement, 19, outbound_username, -1, SQLITE_TRANSIENT);
sqlite3_bind_text(statement, 20, secret_id, -1, SQLITE_TRANSIENT);
} else {
sqlite3_bind_null(statement, 16);
sqlite3_bind_null(statement, 17);
sqlite3_bind_null(statement, 19);
sqlite3_bind_null(statement, 20);
}
sqlite3_bind_text(statement, 18, outbound_tls, -1, SQLITE_TRANSIENT);
sqlite3_bind_int64(statement, 21, now);
sqlite3_bind_int64(statement, 22, now);
if (sqlite3_step(statement) != SQLITE_DONE) goto done;
account_id = sqlite3_last_insert_rowid(database);
sqlite3_finalize(statement); statement = NULL;
if (sqlite3_prepare_v2(database,
"INSERT INTO external_account_identities (account_id, email_address, verified_at, is_default) "
"VALUES (?, ?, NULL, 1)", -1, &statement, NULL) != SQLITE_OK) goto done;
sqlite3_bind_int64(statement, 1, account_id);
sqlite3_bind_text(statement, 2, email, -1, SQLITE_TRANSIENT);
if (sqlite3_step(statement) != SQLITE_DONE) goto done;
sqlite3_finalize(statement); statement = NULL;
if (sqlite3_exec(database, "COMMIT", NULL, NULL, NULL) != SQLITE_OK) goto done;
ok = 1;
done:
+1 -2
View File
@@ -1,6 +1,5 @@
add_library(bongosasl sasl.c)
target_include_directories(bongosasl PRIVATE ${SASL2_INCLUDE_DIRS})
target_link_libraries(bongosasl PRIVATE ${SASL2_LIBRARIES})
target_link_libraries(bongosasl PRIVATE CyrusSASL::CyrusSASL)
install(TARGETS bongosasl DESTINATION ${LIB_INSTALL_DIR})
if(BUILD_TESTING)
+1 -2
View File
@@ -1,8 +1,7 @@
add_library(libbongosieve sieve.c store.c managesieve.c)
set_target_properties(libbongosieve PROPERTIES OUTPUT_NAME bongosieve)
target_include_directories(libbongosieve PRIVATE ${MAILUTILS_INCLUDE_DIR})
target_include_directories(libbongosieve PRIVATE ${SQLITE_INCLUDE_DIRS})
target_link_libraries(libbongosieve PRIVATE ${MAILUTILS_LIBRARIES} ${SQLITE_LIBRARIES})
target_link_libraries(libbongosieve PRIVATE ${MAILUTILS_LIBRARIES} SQLite3::SQLite3)
install(TARGETS libbongosieve DESTINATION ${LIB_INSTALL_DIR})
+1 -1
View File
@@ -16,7 +16,7 @@ add_library(bongoutil SHARED
bongoutil.c)
target_link_libraries(bongoutil
${GLIB2_LIBRARIES}
GLib2::GLib2
bongoconnio
bongostreamio
)
+1
View File
@@ -16,6 +16,7 @@ target_link_libraries(bongoxpl
resolv
pthread
${GCRYPT_LIBRARIES}
GLib2::GLib2
)
install(TARGETS bongoxpl DESTINATION ${LIB_INSTALL_DIR})
+6
View File
@@ -40,6 +40,7 @@ def create_account(owner: str, payload: dict, presets: dict) -> int:
raise ValueError("invalid external account fields")
delete_policy = str(payload.get("delete_policy", "never"))
delete_days = int(payload.get("delete_after_days", 0))
smtp = provider.get("smtp")
settings = {
"label": str(payload.get("label", provider["name"])).strip(),
"email_address": email,
@@ -53,6 +54,11 @@ def create_account(owner: str, payload: dict, presets: dict) -> int:
"poll_interval": int(payload.get("poll_interval", 300)),
"delete_policy": delete_policy,
"delete_after_days": delete_days,
"outbound_mode": "smtp" if smtp else "direct",
"outbound_host": smtp["host"] if smtp else "direct.invalid",
"outbound_port": smtp["port"] if smtp else 1,
"outbound_tls": smtp["tls"] if smtp else "starttls",
"outbound_username": username,
}
return native.create_account(owner, settings, secret)
+9 -1
View File
@@ -31,7 +31,9 @@ class ExternalAccountWebTests(unittest.TestCase):
"id": "example", "name": "Example",
"authentication": ["app_password"],
"imap": {"host": "imap.example.test", "port": 993,
"tls": "implicit"}}]}
"tls": "implicit"},
"smtp": {"host": "smtp.example.test", "port": 587,
"tls": "starttls"}}]}
def tearDown(self):
sys.modules.pop("bongo_web.external_accounts", None)
@@ -54,6 +56,12 @@ class ExternalAccountWebTests(unittest.TestCase):
self.assertEqual(settings["destination_folder"], "INBOX")
self.assertEqual(settings["inbound_host"], "imap.example.test")
self.assertEqual(settings["inbound_port"], 993)
self.assertEqual(settings["email_address"], "external@example.test")
self.assertEqual(settings["outbound_mode"], "smtp")
self.assertEqual(settings["outbound_host"], "smtp.example.test")
self.assertEqual(settings["outbound_port"], 587)
self.assertEqual(settings["outbound_username"],
"external@example.test")
def test_oauth_only_provider_is_rejected_until_supported(self):
presets = {"providers": [{"id": "oauth", "name": "OAuth",