From eafdaea491f2cb4e41d33c10db1b6efe41de635f Mon Sep 17 00:00:00 2001 From: Mario Fetka Date: Thu, 16 Jul 2026 18:52:00 +0200 Subject: [PATCH] Route external identities through provider SMTP --- CMakeLists.txt | 2 +- cmake/FindCyrusSASL.cmake | 34 +++ cmake/FindGLib2.cmake | 55 +++++ cmake/FindGMime.cmake | 34 +++ cmake/FindLibPSL.cmake | 24 ++ cmake/Requirements.cmake | 24 +- src/agents/collector/CMakeLists.txt | 6 +- src/agents/collector/collector.c | 6 + src/agents/collector/external_accounts.c | 93 ++++++++ src/agents/collector/external_accounts.h | 16 ++ src/agents/smtp/CMakeLists.txt | 3 + src/agents/smtp/smtpc.c | 216 ++++++++++++++++-- src/agents/smtp/smtpc.h | 4 + src/agents/smtp/smtpd.c | 20 +- src/agents/store/CMakeLists.txt | 6 +- src/libs/cal/CMakeLists.txt | 1 - src/libs/collectoraccounts/CMakeLists.txt | 13 +- .../tests/external-accounts-test.c | 76 ++++++ src/libs/connio/CMakeLists.txt | 2 +- src/libs/mailauth/CMakeLists.txt | 3 +- src/libs/msgapi/CMakeLists.txt | 4 +- src/libs/python/libbongo/CMakeLists.txt | 2 +- .../libbongo/external-accounts-module.c | 43 +++- src/libs/sasl/CMakeLists.txt | 3 +- src/libs/sieve/CMakeLists.txt | 3 +- src/libs/util/CMakeLists.txt | 2 +- src/libs/xpl/CMakeLists.txt | 1 + src/www/bongo_web/external_accounts.py | 6 + src/www/tests/test_external_accounts.py | 10 +- 29 files changed, 649 insertions(+), 63 deletions(-) create mode 100644 cmake/FindCyrusSASL.cmake create mode 100644 cmake/FindGLib2.cmake create mode 100644 cmake/FindGMime.cmake create mode 100644 cmake/FindLibPSL.cmake create mode 100644 src/libs/collectoraccounts/tests/external-accounts-test.c diff --git a/CMakeLists.txt b/CMakeLists.txt index 9037b07..fb79ae7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ # Global build file for Bongo -cmake_minimum_required(VERSION 3.10) +cmake_minimum_required(VERSION 3.16) project(bongo VERSION 0.7.0 LANGUAGES C) if(POLICY CMP0177) cmake_policy(SET CMP0177 NEW) diff --git a/cmake/FindCyrusSASL.cmake b/cmake/FindCyrusSASL.cmake new file mode 100644 index 0000000..2f74147 --- /dev/null +++ b/cmake/FindCyrusSASL.cmake @@ -0,0 +1,34 @@ +find_path(CyrusSASL_INCLUDE_DIR NAMES sasl/sasl.h) +find_library(CyrusSASL_LIBRARY NAMES sasl2) + +if(CyrusSASL_INCLUDE_DIR) + file(STRINGS "${CyrusSASL_INCLUDE_DIR}/sasl/sasl.h" + _CyrusSASL_VERSION_LINES + REGEX "^#define SASL_VERSION_(MAJOR|MINOR|STEP)[ \t]+[0-9]+") + foreach(_component MAJOR MINOR STEP) + string(REGEX MATCH + "SASL_VERSION_${_component}[ \t]+([0-9]+)" + _CyrusSASL_VERSION_MATCH "${_CyrusSASL_VERSION_LINES}") + set(_CyrusSASL_VERSION_${_component} "${CMAKE_MATCH_1}") + endforeach() + if(DEFINED _CyrusSASL_VERSION_MAJOR AND + DEFINED _CyrusSASL_VERSION_MINOR AND + DEFINED _CyrusSASL_VERSION_STEP) + set(CyrusSASL_VERSION + "${_CyrusSASL_VERSION_MAJOR}.${_CyrusSASL_VERSION_MINOR}.${_CyrusSASL_VERSION_STEP}") + endif() +endif() + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(CyrusSASL + REQUIRED_VARS CyrusSASL_LIBRARY CyrusSASL_INCLUDE_DIR + VERSION_VAR CyrusSASL_VERSION) + +if(CyrusSASL_FOUND AND NOT TARGET CyrusSASL::CyrusSASL) + add_library(CyrusSASL::CyrusSASL UNKNOWN IMPORTED) + set_target_properties(CyrusSASL::CyrusSASL PROPERTIES + IMPORTED_LOCATION "${CyrusSASL_LIBRARY}" + INTERFACE_INCLUDE_DIRECTORIES "${CyrusSASL_INCLUDE_DIR}") +endif() + +mark_as_advanced(CyrusSASL_INCLUDE_DIR CyrusSASL_LIBRARY) diff --git a/cmake/FindGLib2.cmake b/cmake/FindGLib2.cmake new file mode 100644 index 0000000..d680ca2 --- /dev/null +++ b/cmake/FindGLib2.cmake @@ -0,0 +1,55 @@ +find_path(GLib2_INCLUDE_DIR NAMES glib.h PATH_SUFFIXES glib-2.0) +find_library(GLib2_LIBRARY NAMES glib-2.0) +find_library(GLib2_GOBJECT_LIBRARY NAMES gobject-2.0) + +if(GLib2_LIBRARY) + get_filename_component(_GLib2_LIBRARY_DIR "${GLib2_LIBRARY}" DIRECTORY) +endif() +find_path(GLib2_CONFIG_INCLUDE_DIR NAMES glibconfig.h + HINTS "${_GLib2_LIBRARY_DIR}/glib-2.0/include" + PATH_SUFFIXES + glib-2.0/include + lib/glib-2.0/include + lib64/glib-2.0/include + "lib/${CMAKE_LIBRARY_ARCHITECTURE}/glib-2.0/include") + +if(GLib2_CONFIG_INCLUDE_DIR) + file(STRINGS "${GLib2_CONFIG_INCLUDE_DIR}/glibconfig.h" + _GLib2_VERSION_LINES + REGEX "^#define GLIB_(MAJOR|MINOR|MICRO)_VERSION[ \t]+[0-9]+") + foreach(_component MAJOR MINOR MICRO) + string(REGEX MATCH "GLIB_${_component}_VERSION[ \t]+([0-9]+)" + _GLib2_VERSION_MATCH "${_GLib2_VERSION_LINES}") + set(_GLib2_VERSION_${_component} "${CMAKE_MATCH_1}") + endforeach() + if(DEFINED _GLib2_VERSION_MAJOR AND DEFINED _GLib2_VERSION_MINOR AND + DEFINED _GLib2_VERSION_MICRO) + set(GLib2_VERSION + "${_GLib2_VERSION_MAJOR}.${_GLib2_VERSION_MINOR}.${_GLib2_VERSION_MICRO}") + endif() +endif() + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(GLib2 + REQUIRED_VARS GLib2_LIBRARY GLib2_GOBJECT_LIBRARY GLib2_INCLUDE_DIR + GLib2_CONFIG_INCLUDE_DIR + VERSION_VAR GLib2_VERSION) + +if(GLib2_FOUND AND NOT TARGET GLib2::GLib2) + add_library(GLib2::GLib2 UNKNOWN IMPORTED) + set_target_properties(GLib2::GLib2 PROPERTIES + IMPORTED_LOCATION "${GLib2_LIBRARY}" + INTERFACE_INCLUDE_DIRECTORIES + "${GLib2_INCLUDE_DIR};${GLib2_CONFIG_INCLUDE_DIR}") +endif() +set(GLib2_INCLUDE_DIRS "${GLib2_INCLUDE_DIR};${GLib2_CONFIG_INCLUDE_DIR}") +set(GLib2_LIBRARIES "${GLib2_LIBRARY}") +if(GLib2_FOUND AND NOT TARGET GLib2::GObject) + add_library(GLib2::GObject UNKNOWN IMPORTED) + set_target_properties(GLib2::GObject PROPERTIES + IMPORTED_LOCATION "${GLib2_GOBJECT_LIBRARY}" + INTERFACE_LINK_LIBRARIES GLib2::GLib2) +endif() + +mark_as_advanced(GLib2_INCLUDE_DIR GLib2_CONFIG_INCLUDE_DIR GLib2_LIBRARY + GLib2_GOBJECT_LIBRARY) diff --git a/cmake/FindGMime.cmake b/cmake/FindGMime.cmake new file mode 100644 index 0000000..ed1e689 --- /dev/null +++ b/cmake/FindGMime.cmake @@ -0,0 +1,34 @@ +find_path(GMime_INCLUDE_DIR NAMES gmime/gmime.h + PATH_SUFFIXES gmime-2.6 gmime-2.4 gmime-2.2 gmime-2.0 gmime) +find_library(GMime_LIBRARY NAMES gmime-2.6 gmime-2.4 gmime-2.2 gmime-2.0 gmime) + +if(GMime_INCLUDE_DIR AND EXISTS "${GMime_INCLUDE_DIR}/gmime/gmime-version.h") + file(STRINGS "${GMime_INCLUDE_DIR}/gmime/gmime-version.h" + _GMime_VERSION_LINES + REGEX "^#define GMIME_(MAJOR|MINOR|MICRO)_VERSION[ \t]+\\([0-9]+\\)") + foreach(_component MAJOR MINOR MICRO) + string(REGEX MATCH "GMIME_${_component}_VERSION[ \t]+\\(([0-9]+)\\)" + _GMime_VERSION_MATCH "${_GMime_VERSION_LINES}") + set(_GMime_VERSION_${_component} "${CMAKE_MATCH_1}") + endforeach() + if(DEFINED _GMime_VERSION_MAJOR AND DEFINED _GMime_VERSION_MINOR AND + DEFINED _GMime_VERSION_MICRO) + set(GMime_VERSION + "${_GMime_VERSION_MAJOR}.${_GMime_VERSION_MINOR}.${_GMime_VERSION_MICRO}") + endif() +endif() + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(GMime + REQUIRED_VARS GMime_LIBRARY GMime_INCLUDE_DIR + VERSION_VAR GMime_VERSION) + +if(GMime_FOUND AND NOT TARGET GMime::GMime) + add_library(GMime::GMime UNKNOWN IMPORTED) + set_target_properties(GMime::GMime PROPERTIES + IMPORTED_LOCATION "${GMime_LIBRARY}" + INTERFACE_INCLUDE_DIRECTORIES "${GMime_INCLUDE_DIR}" + INTERFACE_LINK_LIBRARIES "GLib2::GObject") +endif() + +mark_as_advanced(GMime_INCLUDE_DIR GMime_LIBRARY) diff --git a/cmake/FindLibPSL.cmake b/cmake/FindLibPSL.cmake new file mode 100644 index 0000000..77d087b --- /dev/null +++ b/cmake/FindLibPSL.cmake @@ -0,0 +1,24 @@ +find_path(LibPSL_INCLUDE_DIR NAMES libpsl.h) +find_library(LibPSL_LIBRARY NAMES psl) + +if(LibPSL_INCLUDE_DIR) + file(STRINGS "${LibPSL_INCLUDE_DIR}/libpsl.h" _LibPSL_VERSION_LINE + REGEX "^#define PSL_VERSION[ \t]+\"[0-9]+\\.[0-9]+\\.[0-9]+\"") + string(REGEX MATCH "\"([0-9]+\\.[0-9]+\\.[0-9]+)\"" + _LibPSL_VERSION_MATCH "${_LibPSL_VERSION_LINE}") + set(LibPSL_VERSION "${CMAKE_MATCH_1}") +endif() + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(LibPSL + REQUIRED_VARS LibPSL_LIBRARY LibPSL_INCLUDE_DIR + VERSION_VAR LibPSL_VERSION) + +if(LibPSL_FOUND AND NOT TARGET LibPSL::LibPSL) + add_library(LibPSL::LibPSL UNKNOWN IMPORTED) + set_target_properties(LibPSL::LibPSL PROPERTIES + IMPORTED_LOCATION "${LibPSL_LIBRARY}" + INTERFACE_INCLUDE_DIRECTORIES "${LibPSL_INCLUDE_DIR}") +endif() + +mark_as_advanced(LibPSL_INCLUDE_DIR LibPSL_LIBRARY) diff --git a/cmake/Requirements.cmake b/cmake/Requirements.cmake index 5d8e00a..33b5edc 100644 --- a/cmake/Requirements.cmake +++ b/cmake/Requirements.cmake @@ -3,7 +3,6 @@ include(CheckIncludeFile) include(CheckLibraryExists) -include(FindPkgConfig) # look for header files we need first check_include_file(inttypes.h HAVE_INTTYPES_H) @@ -58,12 +57,12 @@ include(cmake/FindLog4C.cmake) include_directories(AFTER ${CMAKE_CURRENT_SOURCE_DIR}/import/log4c/) # check for glib -pkg_check_modules (GLIB2 REQUIRED glib-2.0>=2.10) -include_directories(AFTER ${GLIB2_INCLUDE_DIRS}) +find_package(GLib2 2.10 REQUIRED) +# memmgr.h is a public legacy header and includes glib.h directly. +include_directories(AFTER ${GLib2_INCLUDE_DIRS}) # check for GMime -pkg_search_module (GMIME2 REQUIRED gmime-2.6 gmime-2.4 gmime-2.2 gmime-2.0 gmime) -include_directories(AFTER ${GMIME2_INCLUDE_DIRS}) +find_package(GMime 2.6 REQUIRED) # check for gcrypt check_library_exists(gcrypt gcry_control "" HAVE_GCRYPT) @@ -74,13 +73,16 @@ else (HAVE_GCRYPT) endif (HAVE_GCRYPT) # check for gnutls -pkg_check_modules (GNUTLS REQUIRED gnutls) +find_package(GnuTLS REQUIRED) # check for sqlite3 -pkg_check_modules (SQLITE REQUIRED sqlite3) +find_package(SQLite3 REQUIRED) +if(NOT TARGET SQLite3::SQLite3) + add_library(SQLite3::SQLite3 ALIAS SQLite::SQLite3) +endif() -# check for curl -pkg_check_modules (CURL REQUIRED libcurl) +# Secure IMAP, POP3 and SMTP client transport. +find_package(CURL 7.61 REQUIRED) # RFC 5228 Sieve parser and validator used by the filter UI and ManageSieve. find_path(MAILUTILS_INCLUDE_DIR mailutils/sieve.h REQUIRED) @@ -89,7 +91,7 @@ find_library(MAILUTILS_LIBRARY NAMES mailutils REQUIRED) set(MAILUTILS_LIBRARIES ${MAILUTILS_SIEVE_LIBRARY} ${MAILUTILS_LIBRARY}) # Shared SASL implementation for SMTP, IMAP, POP3 and ManageSieve. -pkg_check_modules(SASL2 REQUIRED libsasl2>=2.1.28) +find_package(CyrusSASL 2.1.28 REQUIRED) # Native mail authentication. Bongo links to the protocol libraries directly; # it does not require the corresponding milter daemons at runtime. @@ -97,7 +99,7 @@ find_package(SPF2 REQUIRED) find_package(OpenDKIM REQUIRED) find_package(OpenDMARC REQUIRED) find_package(SRS2 REQUIRED) -pkg_check_modules(PSL REQUIRED libpsl) +find_package(LibPSL 0.21 REQUIRED) # check for libical check_include_file(libical/ical.h HAVE_ICAL_H) diff --git a/src/agents/collector/CMakeLists.txt b/src/agents/collector/CMakeLists.txt index 65ca8fb..6c91dad 100644 --- a/src/agents/collector/CMakeLists.txt +++ b/src/agents/collector/CMakeLists.txt @@ -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() diff --git a/src/agents/collector/collector.c b/src/agents/collector/collector.c index 79cd492..52ab962 100644 --- a/src/agents/collector/collector.c +++ b/src/agents/collector/collector.c @@ -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; diff --git a/src/agents/collector/external_accounts.c b/src/agents/collector/external_accounts.c index 5213409..dcefa11 100644 --- a/src/agents/collector/external_accounts.c +++ b/src/agents/collector/external_accounts.c @@ -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) diff --git a/src/agents/collector/external_accounts.h b/src/agents/collector/external_accounts.h index 7b18fe6..711ecb7 100644 --- a/src/agents/collector/external_accounts.h +++ b/src/agents/collector/external_accounts.h @@ -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, diff --git a/src/agents/smtp/CMakeLists.txt b/src/agents/smtp/CMakeLists.txt index 305b97e..6d6e3b5 100644 --- a/src/agents/smtp/CMakeLists.txt +++ b/src/agents/smtp/CMakeLists.txt @@ -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}) diff --git a/src/agents/smtp/smtpc.c b/src/agents/smtp/smtpc.c index 057b950..3a24725 100755 --- a/src/agents/smtp/smtpc.c +++ b/src/agents/smtp/smtpc.c @@ -23,6 +23,8 @@ #include +#include + #include #include #include @@ -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) { diff --git a/src/agents/smtp/smtpc.h b/src/agents/smtp/smtpc.h index 9c047c9..9bad79e 100644 --- a/src/agents/smtp/smtpc.h +++ b/src/agents/smtp/smtpc.h @@ -30,6 +30,8 @@ #include #include +#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; diff --git a/src/agents/smtp/smtpd.c b/src/agents/smtp/smtpd.c index b3c13c5..da8a228 100644 --- a/src/agents/smtp/smtpd.c +++ b/src/agents/smtp/smtpd.c @@ -45,6 +45,7 @@ #include #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 diff --git a/src/agents/store/CMakeLists.txt b/src/agents/store/CMakeLists.txt index c0d9fce..fea2964 100644 --- a/src/agents/store/CMakeLists.txt +++ b/src/agents/store/CMakeLists.txt @@ -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}) diff --git a/src/libs/cal/CMakeLists.txt b/src/libs/cal/CMakeLists.txt index 5651652..e559eea 100644 --- a/src/libs/cal/CMakeLists.txt +++ b/src/libs/cal/CMakeLists.txt @@ -15,7 +15,6 @@ add_library(bongocal SHARED #target_link_libraries(bongo-import-tz # bongomemmgr # bongocal -# ${GLIB2_LIBRARIES} # ${ICAL_LIBRARIES} # ) diff --git a/src/libs/collectoraccounts/CMakeLists.txt b/src/libs/collectoraccounts/CMakeLists.txt index c9d2f43..c277c0f 100644 --- a/src/libs/collectoraccounts/CMakeLists.txt +++ b/src/libs/collectoraccounts/CMakeLists.txt @@ -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() diff --git a/src/libs/collectoraccounts/tests/external-accounts-test.c b/src/libs/collectoraccounts/tests/external-accounts-test.c new file mode 100644 index 0000000..00aee1d --- /dev/null +++ b/src/libs/collectoraccounts/tests/external-accounts-test.c @@ -0,0 +1,76 @@ +#include + +#include +#include + +#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; +} diff --git a/src/libs/connio/CMakeLists.txt b/src/libs/connio/CMakeLists.txt index 0b157f6..08652c0 100644 --- a/src/libs/connio/CMakeLists.txt +++ b/src/libs/connio/CMakeLists.txt @@ -10,7 +10,7 @@ add_library(bongoconnio SHARED ) target_link_libraries(bongoconnio - ${GNUTLS_LIBRARIES} + GnuTLS::GnuTLS ) install(TARGETS bongoconnio DESTINATION ${LIB_INSTALL_DIR}) diff --git a/src/libs/mailauth/CMakeLists.txt b/src/libs/mailauth/CMakeLists.txt index 32f815e..9ca8523 100644 --- a/src/libs/mailauth/CMakeLists.txt +++ b/src/libs/mailauth/CMakeLists.txt @@ -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}) diff --git a/src/libs/msgapi/CMakeLists.txt b/src/libs/msgapi/CMakeLists.txt index 9600589..1fc8819 100644 --- a/src/libs/msgapi/CMakeLists.txt +++ b/src/libs/msgapi/CMakeLists.txt @@ -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}) diff --git a/src/libs/python/libbongo/CMakeLists.txt b/src/libs/python/libbongo/CMakeLists.txt index cf70e8a..fb6bb58 100644 --- a/src/libs/python/libbongo/CMakeLists.txt +++ b/src/libs/python/libbongo/CMakeLists.txt @@ -62,7 +62,7 @@ target_link_libraries(_calendar target_link_libraries(_external_accounts bongocollectoraccounts bongoxpl - ${GLIB2_LIBRARIES} + GLib2::GLib2 ${GCRYPT_LIBRARIES} ${PYTHON_LIBRARIES} ) diff --git a/src/libs/python/libbongo/external-accounts-module.c b/src/libs/python/libbongo/external-accounts-module.c index 7005d6e..8d10cf0 100644 --- a/src/libs/python/libbongo/external-accounts-module.c +++ b/src/libs/python/libbongo/external-accounts-module.c @@ -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: diff --git a/src/libs/sasl/CMakeLists.txt b/src/libs/sasl/CMakeLists.txt index 7898558..f040eb6 100644 --- a/src/libs/sasl/CMakeLists.txt +++ b/src/libs/sasl/CMakeLists.txt @@ -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) diff --git a/src/libs/sieve/CMakeLists.txt b/src/libs/sieve/CMakeLists.txt index 366b179..f5498ea 100644 --- a/src/libs/sieve/CMakeLists.txt +++ b/src/libs/sieve/CMakeLists.txt @@ -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}) diff --git a/src/libs/util/CMakeLists.txt b/src/libs/util/CMakeLists.txt index 9d996e9..cb8433c 100644 --- a/src/libs/util/CMakeLists.txt +++ b/src/libs/util/CMakeLists.txt @@ -16,7 +16,7 @@ add_library(bongoutil SHARED bongoutil.c) target_link_libraries(bongoutil - ${GLIB2_LIBRARIES} + GLib2::GLib2 bongoconnio bongostreamio ) diff --git a/src/libs/xpl/CMakeLists.txt b/src/libs/xpl/CMakeLists.txt index 7622fb9..10a38b8 100644 --- a/src/libs/xpl/CMakeLists.txt +++ b/src/libs/xpl/CMakeLists.txt @@ -16,6 +16,7 @@ target_link_libraries(bongoxpl resolv pthread ${GCRYPT_LIBRARIES} + GLib2::GLib2 ) install(TARGETS bongoxpl DESTINATION ${LIB_INSTALL_DIR}) diff --git a/src/www/bongo_web/external_accounts.py b/src/www/bongo_web/external_accounts.py index 7b6b67d..9dadd8f 100644 --- a/src/www/bongo_web/external_accounts.py +++ b/src/www/bongo_web/external_accounts.py @@ -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) diff --git a/src/www/tests/test_external_accounts.py b/src/www/tests/test_external_accounts.py index 6dc3b57..aa56687 100644 --- a/src/www/tests/test_external_accounts.py +++ b/src/www/tests/test_external_accounts.py @@ -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",