From b0cd6f35a203224790faa25537745b64f87c8053 Mon Sep 17 00:00:00 2001 From: Mario Fetka Date: Tue, 28 Jul 2026 17:48:55 +0200 Subject: [PATCH] Store Sieve state in user stores --- contrib/testing/protocol-smoke.py | 118 ++++++++ docs/sieve.md | 5 +- docs/storage.md | 10 +- include/bongosieve.h | 9 + src/agents/rules/rules.c | 36 ++- src/agents/rules/rules.h | 1 - src/agents/sieve/managesieved.c | 15 +- src/agents/store/CMakeLists.txt | 2 + src/agents/store/accounts.c | 17 -- src/agents/store/command.c | 6 + src/agents/store/command.h | 1 + src/agents/store/db.c | 14 + src/agents/store/sieve-command.c | 401 +++++++++++++++++++++++++++ src/agents/store/sieve-command.h | 26 ++ src/libs/sieve/CMakeLists.txt | 10 +- src/libs/sieve/sieve-store-private.h | 50 ++++ src/libs/sieve/store-nmap.c | 340 +++++++++++++++++++++++ src/libs/sieve/store.c | 186 ++++++++----- src/libs/sieve/tests/store-test.c | 2 + 19 files changed, 1133 insertions(+), 116 deletions(-) create mode 100644 src/agents/store/sieve-command.c create mode 100644 src/agents/store/sieve-command.h create mode 100644 src/libs/sieve/sieve-store-private.h create mode 100644 src/libs/sieve/store-nmap.c diff --git a/contrib/testing/protocol-smoke.py b/contrib/testing/protocol-smoke.py index f3604cf..1c0166f 100755 --- a/contrib/testing/protocol-smoke.py +++ b/contrib/testing/protocol-smoke.py @@ -8,6 +8,7 @@ from __future__ import annotations import argparse +import base64 from email.message import EmailMessage import imaplib import os @@ -44,6 +45,75 @@ TLS.check_hostname = False TLS.verify_mode = ssl.CERT_NONE +def sieve_quote(value: str) -> str: + return '"' + value.replace("\\", "\\\\").replace('"', '\\"') + '"' + + +class ManageSieveClient: + def __init__(self) -> None: + self.socket = socket.create_connection( + (HOST, PORTS["sieve"]), timeout=15) + self.stream = self.socket.makefile("rb") + self.read_response("greeting") + + def close(self) -> None: + try: + self.stream.close() + finally: + self.socket.close() + + def send(self, data: bytes) -> None: + self.socket.sendall(data) + + def line(self) -> bytes: + line = self.stream.readline(65537) + if not line or len(line) > 65536 or not line.endswith(b"\n"): + raise RuntimeError("ManageSieve connection ended mid-response") + return line + + def read_response(self, operation: str) -> list[str]: + lines: list[str] = [] + while True: + decoded = self.line().decode("utf-8", "strict").rstrip("\r\n") + lines.append(decoded) + upper = decoded.upper() + if upper == "OK" or upper.startswith("OK ") or \ + upper == "NO" or upper.startswith("NO ") or \ + upper == "BYE" or upper.startswith("BYE "): + if not upper.startswith("OK"): + raise RuntimeError( + f"ManageSieve {operation} failed: {decoded}") + return lines + + def command(self, operation: str, command: str) -> list[str]: + self.send(command.encode("utf-8") + b"\r\n") + return self.read_response(operation) + + def starttls(self) -> None: + self.command("STARTTLS", "STARTTLS") + self.stream.close() + self.socket = TLS.wrap_socket(self.socket, server_hostname=HOST) + self.stream = self.socket.makefile("rb") + + def literal_command( + self, operation: str, command: str, payload: bytes) -> list[str]: + self.send( + f"{command} {{{len(payload)}+}}\r\n".encode("utf-8") + payload) + return self.read_response(operation) + + def get_script(self, name: str) -> bytes: + self.send(f"GETSCRIPT {sieve_quote(name)}\r\n".encode("utf-8")) + marker = self.line().decode("ascii", "strict").strip() + if not marker.startswith("{") or not marker.endswith("}"): + raise RuntimeError(f"invalid GETSCRIPT literal marker: {marker}") + length = int(marker[1:-1]) + payload = self.stream.read(length) + if len(payload) != length or self.stream.read(2) != b"\r\n": + raise RuntimeError("truncated GETSCRIPT literal") + self.read_response("GETSCRIPT") + return payload + + def wait_for_listeners(timeout: float = 30.0) -> str: deadline = time.monotonic() + timeout pending = set(PORTS.values()) @@ -97,6 +167,53 @@ def smtp_submit() -> str: return TOKEN +def managesieve() -> str: + script_name = f"{TOKEN[:48]} filter" + renamed = f"{TOKEN[:48]} renamed" + script = ( + 'require ["fileinto"];\r\n' + 'if header :contains "Subject" "Bongo" { keep; }\r\n' + ).encode("utf-8") + auth = base64.b64encode( + b"\0" + USER1.encode("utf-8") + b"\0" + PASSWORD.encode("utf-8") + ).decode("ascii") + client = ManageSieveClient() + try: + client.starttls() + capabilities = client.command("CAPABILITY", "CAPABILITY") + if not any('"SASL"' in line and "PLAIN" in line + for line in capabilities): + raise RuntimeError("SASL PLAIN not advertised after STARTTLS") + client.command( + "AUTHENTICATE", + f'AUTHENTICATE "PLAIN" "{auth}"') + client.literal_command( + "CHECKSCRIPT", "CHECKSCRIPT", script) + client.literal_command( + "PUTSCRIPT", f"PUTSCRIPT {sieve_quote(script_name)}", script) + listing = client.command("LISTSCRIPTS", "LISTSCRIPTS") + if not any(sieve_quote(script_name) in line for line in listing): + raise RuntimeError("uploaded script missing from LISTSCRIPTS") + client.command( + "SETACTIVE", f"SETACTIVE {sieve_quote(script_name)}") + if client.get_script(script_name) != script: + raise RuntimeError("GETSCRIPT returned different script bytes") + client.command( + "RENAMESCRIPT", + f"RENAMESCRIPT {sieve_quote(script_name)} {sieve_quote(renamed)}") + listing = client.command("LISTSCRIPTS", "LISTSCRIPTS") + if not any(sieve_quote(renamed) in line and "ACTIVE" in line + for line in listing): + raise RuntimeError("renamed active script missing") + client.command("SETACTIVE", 'SETACTIVE ""') + client.command( + "DELETESCRIPT", f"DELETESCRIPT {sieve_quote(renamed)}") + client.command("LOGOUT", "LOGOUT") + finally: + client.close() + return "upload, activate, retrieve, rename and delete passed" + + def smtp_implicit() -> str: with smtplib.SMTP_SSL(HOST, PORTS["smtps"], timeout=15, context=TLS) as client: @@ -188,6 +305,7 @@ def main() -> int: parser.error("BONGO_TEST_PASSWORD must be set") checks = [ ("listener readiness", wait_for_listeners), + ("ManageSieve NMAP Store", managesieve), ("SMTP submission STARTTLS", smtp_submit), ("SMTPS submission", smtp_implicit), ("IMAPS delivery", wait_for_imap), diff --git a/docs/sieve.md b/docs/sieve.md index 48c4a05..0ffc6e1 100644 --- a/docs/sieve.md +++ b/docs/sieve.md @@ -43,8 +43,9 @@ The implementation advertises `fileinto`, `envelope`, `body`, `variables`, `vacation`, and `vacation-seconds`. Actions include keep, discard, file into a folder, redirect, reject, and vacation response. Vacation history is durable so repeated delivery does not generate an immediate reply loop. Scripts and -vacation state live in `/var/lib/bongo/dbf/sieve.sqlite`; include that database -and its SQLite sidecars in backups. +vacation state live in the owning user's authoritative Bongo Store and are +accessed by `bongosieve` and `bongorules` through NMAP. Include the user's +complete Store directory in backups. ## Filters in Webmail diff --git a/docs/storage.md b/docs/storage.md index d87700d..33c8340 100644 --- a/docs/storage.md +++ b/docs/storage.md @@ -46,7 +46,6 @@ designs were present in the archived Bongo history. By contrast, standalone databases first added by the 0.7 development work must be moved behind `bongostore` and its NMAP interface: -- `/var/lib/bongo/dbf/sieve.sqlite`; - `/var/lib/bongo/dbf/external-accounts.sqlite`; TLSRPT and the worker scheduler originally joined that list during @@ -54,14 +53,15 @@ development, but have already moved into the `_system` Store and no longer own separate databases. Because these paths existed only in unreleased 0.7 development builds, no production migration is required. -Sieve scripts and vacation state belong to the Store of the user who owns -them. External account definitions, encrypted credentials, destination +Sieve scripts and vacation state now belong to the Store of the user who owns +them; `bongosieve` and `bongorules` access them only through NMAP. External +account definitions, encrypted credentials, destination folder, deletion policy, linked sending identity, remote UID state, and duplicate-detection state likewise belong to that user's Store. The `_system` Store holds only a credential-free due index containing the owner, account ID, and next collection time, so `bongocollector` need not scan every account. -The two remaining standalone paths are transitional until their Store -protocol migrations have landed. +The remaining external-account standalone path is transitional until its +Store protocol move has landed. Web tasks, sender identities, and signatures are ordinary user Store documents below `/preferences`. Login sessions exist only in `bongoweb` diff --git a/include/bongosieve.h b/include/bongosieve.h index 41c490f..5a140e1 100644 --- a/include/bongosieve.h +++ b/include/bongosieve.h @@ -28,6 +28,8 @@ extern "C" { #endif +struct sqlite3; + /* Validate one complete UTF-8 RFC 5228 script without executing it. */ int BongoSieveValidate(const char *script, size_t length); @@ -62,7 +64,14 @@ int BongoSieveEvaluate(const char *script, size_t script_length, typedef struct BongoSieveStore BongoSieveStore; +/* Open a temporary standalone per-user store, primarily for tests and tools. */ int BongoSieveStoreOpen(BongoSieveStore **store, const char *path); +/* Attach Sieve tables to a database owned and locked by bongostore. */ +int BongoSieveStoreAttach(BongoSieveStore **store, struct sqlite3 *database); +/* Borrow an already initialized database owned and locked by bongostore. */ +int BongoSieveStoreBorrow(BongoSieveStore **store, struct sqlite3 *database); +/* Select one user's authoritative Bongo Store through NMAP. */ +int BongoSieveStoreOpenNmap(BongoSieveStore **store, const char *user); void BongoSieveStoreClose(BongoSieveStore **store); int BongoSieveStorePut(BongoSieveStore *store, const char *user, const char *name, const char *script, size_t length); diff --git a/src/agents/rules/rules.c b/src/agents/rules/rules.c index c2e44cf..bf9c58c 100644 --- a/src/agents/rules/rules.c +++ b/src/agents/rules/rules.c @@ -40,6 +40,7 @@ RulesAgentGlobals RulesAgent = {0}; typedef struct { RulesClient *client; + BongoSieveStore *store; const char *message; int result; BOOL written; @@ -162,13 +163,13 @@ SieveSendVacation(SieveExecution *execution, const BongoSieveResult *result) interval = result->has_seconds ? result->seconds : result->days * 86400UL; if (interval < 3600) interval = 3600; SieveVacationHandle(result, handle); - if (!BongoSieveVacationClaim(RulesAgent.SieveStore, client->recipient, + if (!BongoSieveVacationClaim(execution->store, client->recipient, safeSender, handle, interval)) { free(reason); return 1; } replySize = strlen(reason) + strlen(safeSender) + strlen(safeFrom) + strlen(subject) + 320; reply = malloc(replySize); if (!reply) { - BongoSieveVacationRelease(RulesAgent.SieveStore, client->recipient, + BongoSieveVacationRelease(execution->store, client->recipient, safeSender, handle); free(reason); return 0; @@ -183,7 +184,7 @@ SieveSendVacation(SieveExecution *execution, const BongoSieveResult *result) reason, result->mime ? "" : "\r\n"); free(reason); if (code < 0 || (size_t)code >= replySize) { - BongoSieveVacationRelease(RulesAgent.SieveStore, client->recipient, + BongoSieveVacationRelease(execution->store, client->recipient, safeSender, handle); free(reply); return 0; @@ -203,7 +204,7 @@ SieveSendVacation(SieveExecution *execution, const BongoSieveResult *result) ConnWriteF(client->conn, "QABRT\r\n"); ConnFlush(client->conn); NMAPReadAnswer(client->conn, client->line, CONN_BUFSIZE, FALSE); - BongoSieveVacationRelease(RulesAgent.SieveStore, client->recipient, + BongoSieveVacationRelease(execution->store, client->recipient, safeSender, handle); free(reply); return 0; @@ -364,15 +365,20 @@ SieveAction(const BongoSieveResult *action_result, void *data) static int ProcessSieve(RulesClient *client) { + BongoSieveStore *store = NULL; char *script = NULL; char *message = NULL; size_t script_length = 0; int active = 0; long message_length; - SieveExecution execution = { client, NULL, 0, FALSE }; - if (!RulesAgent.SieveStore || - !BongoSieveStoreGet(RulesAgent.SieveStore, client->recipient, NULL, - &script, &script_length, &active) || !active) return 0; + SieveExecution execution = { client, NULL, NULL, 0, FALSE }; + if (!BongoSieveStoreOpenNmap(&store, client->recipient)) goto failed; + execution.store = store; + if (!BongoSieveStoreGet(store, client->recipient, NULL, + &script, &script_length, &active) || !active) { + BongoSieveStoreClose(&store); + return 0; + } if (NMAPSendCommandF(client->conn, "QRETR %s MESSAGE\r\n", client->qID) == -1 || NMAPReadAnswer(client->conn, client->line, CONN_BUFSIZE, TRUE) != 2023) goto failed; message_length = strtol(client->line, NULL, 10); @@ -390,12 +396,14 @@ ProcessSieve(RulesClient *client) } free(message); free(script); + BongoSieveStoreClose(&store); return execution.result == -1 ? -1 : 1; failed: Log(LOG_ERROR, "Sieve processing failed for user %s queue %s; keeping message", client->recipient, client->qID); free(message); free(script); + BongoSieveStoreClose(&store); return ConnWriteF(client->conn, "QMOD RAW %s\r\n", client->envelopeLine) == -1 ? -1 : 1; } @@ -1677,22 +1685,10 @@ XplServiceMain(int argc, char *argv[]) sslconfig.key.type = GNUTLS_X509_FMT_PEM; RulesAgent.agent.sslContext = ConnSSLContextAlloc(&sslconfig); - { - char sieveDatabase[XPL_MAX_PATH + 1]; - snprintf(sieveDatabase, sizeof(sieveDatabase), "%s/sieve.sqlite", - XPL_DEFAULT_DBF_DIR); - if (!BongoSieveStoreOpen(&RulesAgent.SieveStore, sieveDatabase)) { - XplConsolePrintf(AGENT_NAME ": Cannot open Sieve script store\r\n"); - return -1; - } - } - XplSignalHandler(SignalHandler); /* Start the server thread */ XplStartMainThread(AGENT_NAME, &id, RulesAgentServer, 8192, NULL, ccode); - BongoSieveStoreClose(&RulesAgent.SieveStore); - XplUnloadApp(XplGetThreadID()); return 0; diff --git a/src/agents/rules/rules.h b/src/agents/rules/rules.h index 272b995..f3f14e3 100644 --- a/src/agents/rules/rules.h +++ b/src/agents/rules/rules.h @@ -116,7 +116,6 @@ typedef struct _RulesAgentGlobals { void *OutgoingPool; BongoThreadPool *OutgoingThreadPool; bongo_ssl_context *SSL_Context; - BongoSieveStore *SieveStore; } RulesAgentGlobals; extern RulesAgentGlobals RulesAgent; diff --git a/src/agents/sieve/managesieved.c b/src/agents/sieve/managesieved.c index 9b92bef..7a85fe0 100644 --- a/src/agents/sieve/managesieved.c +++ b/src/agents/sieve/managesieved.c @@ -36,7 +36,6 @@ static BongoAgent agent; static Connection *listener; static bongo_ssl_context *tls_context; -static BongoSieveStore *script_store; static int server_port = 4190; static int maximum_script_size = 1024 * 1024; static int maximum_connections = 64; @@ -182,6 +181,7 @@ static void serve(Connection *connection) { char line[LINE_SIZE + 1]; char *user = NULL; + BongoSieveStore *script_store = NULL; BongoSaslSession *sasl = NULL; int tls = 0; const char *mechanisms = NULL; @@ -223,8 +223,12 @@ static void serve(Connection *connection) !BongoSaslMechanisms(sasl, &mechanisms)) break; tls = 1; } else if (request.command == BONGO_MANAGESIEVE_AUTHENTICATE && tls && !user) { - if (authenticate(connection, sasl, &request, &user)) write_ok(connection, NULL); - else { + if (authenticate(connection, sasl, &request, &user) && + BongoSieveStoreOpenNmap(&script_store, user)) { + write_ok(connection, NULL); + } else { + free(user); + user = NULL; Log(LOG_NOTICE, "Bongo authentication failed: service=sieve source=%s", LOGIP(connection->socketAddress)); @@ -276,6 +280,7 @@ static void serve(Connection *connection) free(literal); if (ConnFlush(connection) == -1) break; } + BongoSieveStoreClose(&script_store); free(user); BongoSaslSessionDestroy(&sasl); } @@ -368,7 +373,6 @@ XplServiceCode(signal_handler) int XplServiceMain(int argc, char **argv) { ConnSSLConfiguration tls = {0}; - char database[XPL_MAX_PATH + 1]; int result; int wait; (void)argc; (void)argv; (void)GlobalConfig; @@ -387,8 +391,6 @@ int XplServiceMain(int argc, char **argv) (proxy_protocol_networks == NULL || proxy_protocol_networks->len == 0U)) || !BongoSaslInitialize()) return 1; - snprintf(database, sizeof(database), "%s/sieve.sqlite", XPL_DEFAULT_DBF_DIR); - if (!BongoSieveStoreOpen(&script_store, database)) return 1; tls.certificate.file = XPL_DEFAULT_CERT_PATH; tls.certificate.type = GNUTLS_X509_FMT_PEM; tls.key.file = XPL_DEFAULT_KEY_PATH; @@ -413,7 +415,6 @@ int XplServiceMain(int argc, char **argv) XplSafeRead(active_connections)); return 1; } - BongoSieveStoreClose(&script_store); ConnSSLContextFree(tls_context); BongoSaslShutdown(); return 0; diff --git a/src/agents/store/CMakeLists.txt b/src/agents/store/CMakeLists.txt index 242e88c..70be3a5 100644 --- a/src/agents/store/CMakeLists.txt +++ b/src/agents/store/CMakeLists.txt @@ -32,6 +32,7 @@ add_executable(bongostore ringlog.c search.c scheduler-command.c + sieve-command.c store.c stored.c tlsrpt-command.c @@ -47,6 +48,7 @@ target_link_libraries(bongostore bongocal bongotlsrpt bongoscheduler + libbongosieve SQLite3::SQLite3 GLib2::GLib2 LibIcal::ical diff --git a/src/agents/store/accounts.c b/src/agents/store/accounts.c index 0735c98..3dd58f7 100644 --- a/src/agents/store/accounts.c +++ b/src/agents/store/accounts.c @@ -354,22 +354,16 @@ CCode AccountRename(StoreClient *client, const char *old_username, const char *new_username) { - static const AccountRenameColumn sieve_columns[] = { - { "sieve_scripts", "user" }, - { "sieve_vacation_replies", "user" } - }; static const AccountRenameColumn external_columns[] = { { "external_account_secrets", "owner" }, { "external_accounts", "owner" } }; char old_store[XPL_MAX_PATH + 1]; char new_store[XPL_MAX_PATH + 1]; - char sieve[XPL_MAX_PATH + 1]; char external[XPL_MAX_PATH + 1]; char cookies[XPL_MAX_PATH + 1]; struct stat status; int moved_store = 0; - int moved_sieve = 0; int moved_external = 0; AccountAliasChange *alias_changes = NULL; size_t alias_change_count = 0; @@ -383,8 +377,6 @@ AccountRename(StoreClient *client, const char *old_username, old_username) >= (int)sizeof(old_store) || snprintf(new_store, sizeof(new_store), "%s/%s", StoreAgent.store.rootDir, new_username) >= (int)sizeof(new_store) || - snprintf(sieve, sizeof(sieve), "%s/sieve.sqlite", - XPL_DEFAULT_DBF_DIR) >= (int)sizeof(sieve) || snprintf(external, sizeof(external), "%s/external-accounts.sqlite", XPL_DEFAULT_DBF_DIR) >= (int)sizeof(external) || snprintf(cookies, sizeof(cookies), "%s/cookies.db", @@ -405,11 +397,6 @@ AccountRename(StoreClient *client, const char *old_username, goto fail; if (stat(new_store, &status) == 0 || errno != ENOENT) goto fail; - if (RenameDatabaseColumns(sieve, sieve_columns, - sizeof(sieve_columns) / sizeof(sieve_columns[0]), - old_username, new_username) != 0) - goto fail; - moved_sieve = 1; if (RenameDatabaseColumns(external, external_columns, sizeof(external_columns) / sizeof(external_columns[0]), @@ -445,10 +432,6 @@ rollback: sizeof(external_columns) / sizeof(external_columns[0]), new_username, old_username); - if (moved_sieve) - RenameDatabaseColumns(sieve, sieve_columns, - sizeof(sieve_columns) / sizeof(sieve_columns[0]), - new_username, old_username); fail: StoreEndUserMaintenance(); FreeAliasChanges(alias_changes, alias_change_count); diff --git a/src/agents/store/command.c b/src/agents/store/command.c index 0040a01..5bd3155 100644 --- a/src/agents/store/command.c +++ b/src/agents/store/command.c @@ -35,6 +35,7 @@ #include "calendar.h" #include "quota.h" #include "scheduler-command.h" +#include "sieve-command.h" #include "tlsrpt-command.h" #include "mail.h" @@ -184,6 +185,7 @@ StoreSetupCommands() BongoHashtablePutNoReplace(CommandTable, "NOOP", (void *) STORE_COMMAND_NOOP) || BongoHashtablePutNoReplace(CommandTable, "QUOTA", (void *) STORE_COMMAND_QUOTA) || BongoHashtablePutNoReplace(CommandTable, "SCHEDULER", (void *) STORE_COMMAND_SCHEDULER) || + BongoHashtablePutNoReplace(CommandTable, "SIEVE", (void *) STORE_COMMAND_SIEVE) || BongoHashtablePutNoReplace(CommandTable, "TIMEOUT", (void *) STORE_COMMAND_TIMEOUT) || BongoHashtablePutNoReplace(CommandTable, "TLSRPT", (void *) STORE_COMMAND_TLSRPT) || @@ -1272,6 +1274,10 @@ StoreCommandLoop(StoreClient *client) ccode = StoreCommandSCHEDULER(client, n, tokens); break; + case STORE_COMMAND_SIEVE: + ccode = StoreCommandSIEVE(client, n, tokens); + break; + case STORE_COMMAND_TLSRPT: ccode = StoreCommandTLSRPT(client, n, tokens); break; diff --git a/src/agents/store/command.h b/src/agents/store/command.h index 6a4bd23..0f79e85 100644 --- a/src/agents/store/command.h +++ b/src/agents/store/command.h @@ -98,6 +98,7 @@ typedef enum { STORE_COMMAND_NOOP, STORE_COMMAND_QUOTA, STORE_COMMAND_SCHEDULER, + STORE_COMMAND_SIEVE, STORE_COMMAND_TIMEOUT, STORE_COMMAND_TLSRPT, diff --git a/src/agents/store/db.c b/src/agents/store/db.c index 8393a27..08495b2 100644 --- a/src/agents/store/db.c +++ b/src/agents/store/db.c @@ -3,6 +3,7 @@ #include #include #include +#include #include #include "stored.h" @@ -130,6 +131,19 @@ StoreDBOpen(StoreClient *client, const char *user) } BongoSchedulerClose(&scheduler); BongoTlsRptStoreClose(&tlsrpt); + } else { + BongoSieveStore *sieve = NULL; + + if (!BongoSieveStoreAttach(&sieve, entry->handle->db)) { + BongoSieveStoreClose(&sieve); + MsgSQLClose(entry->handle); + entry->handle = NULL; + BongoHashtableRemove(StoreAgent.dbpool.entries, + (void *)user); + DBPoolEntryDelete(entry); + goto db_fail; + } + BongoSieveStoreClose(&sieve); } } entry->clients++; diff --git a/src/agents/store/sieve-command.c b/src/agents/store/sieve-command.c new file mode 100644 index 0000000..6cb7b68 --- /dev/null +++ b/src/agents/store/sieve-command.c @@ -0,0 +1,401 @@ +/**************************************************************************** + * + * 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. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, contact Novell, Inc. + * + ****************************************************************************/ +// Parts Copyright (C) 2026 Mario Fetka. See COPYING for details. + +#include + +#include "sieve-command.h" +#include "command-parsing.h" +#include "messages.h" + +#include +#include + +#include +#include +#include +#include +#include +#include + +#define SIEVE_WIRE_LIMIT (1024U * 1024U) + +static BongoSieveStore * +BorrowStore(StoreClient *client) +{ + BongoSieveStore *store = NULL; + + XplMutexLock(client->storedb->transactionLock); + if (!BongoSieveStoreBorrow(&store, client->storedb->db)) { + XplMutexUnlock(client->storedb->transactionLock); + return NULL; + } + return store; +} + +static void +ReturnStore(StoreClient *client, BongoSieveStore **store) +{ + BongoSieveStoreClose(store); + XplMutexUnlock(client->storedb->transactionLock); +} + +static CCode +BadSyntax(StoreClient *client) +{ + return ConnWriteStr(client->conn, MSG3022BADSYNTAX); +} + +static int +HexValue(unsigned char value) +{ + if (value >= '0' && value <= '9') return value - '0'; + value = (unsigned char)tolower(value); + if (value >= 'a' && value <= 'f') return value - 'a' + 10; + return -1; +} + +static char * +HexDecode(const char *input, size_t maximum) +{ + size_t length; + size_t index; + char *output; + + if (!input || !(length = strlen(input)) || (length & 1U) || + length / 2U > maximum) + return NULL; + output = malloc(length / 2U + 1U); + if (!output) return NULL; + for (index = 0; index < length; index += 2U) { + int high = HexValue((unsigned char)input[index]); + int low = HexValue((unsigned char)input[index + 1U]); + if (high < 0 || low < 0 || (high == 0 && low == 0)) { + free(output); + return NULL; + } + output[index / 2U] = (char)((high << 4) | low); + } + output[length / 2U] = '\0'; + return output; +} + +static char * +HexEncode(const char *input) +{ + static const char digits[] = "0123456789abcdef"; + size_t length; + size_t index; + char *output; + + if (!input || !(length = strlen(input)) || + length > (SIZE_MAX - 1U) / 2U) + return NULL; + output = malloc(length * 2U + 1U); + if (!output) return NULL; + for (index = 0; index < length; index++) { + unsigned char value = (unsigned char)input[index]; + output[index * 2U] = digits[value >> 4]; + output[index * 2U + 1U] = digits[value & 0x0f]; + } + output[length * 2U] = '\0'; + return output; +} + +static int +ParseSize(const char *text, size_t *value) +{ + char *end = NULL; + uintmax_t parsed; + + if (!text || *text < '0' || *text > '9') return 0; + errno = 0; + parsed = strtoumax(text, &end, 10); + if (errno == ERANGE || *end || parsed == 0 || + parsed > SIEVE_WIRE_LIMIT || parsed > SIZE_MAX) + return 0; + *value = (size_t)parsed; + return 1; +} + +static char * +ReceivePayload(StoreClient *client, size_t length) +{ + char *payload = malloc(length + 1U); + + if (!payload) { + ConnWriteStr(client->conn, MSG5005DBLIBERR); + return NULL; + } + if (ConnWriteStr(client->conn, "2002 Send Sieve script.\r\n") < 0 || + ConnFlush(client->conn) < 0 || + ConnReadCount(client->conn, payload, length) != (int)length) { + free(payload); + return NULL; + } + payload[length] = '\0'; + return payload; +} + +static CCode +Put(StoreClient *client, const char *encoded_name, const char *length_text) +{ + BongoSieveStore *store; + char *name = HexDecode(encoded_name, 128U); + char *script; + size_t length; + int result; + + if (!name || !ParseSize(length_text, &length)) { + free(name); + return BadSyntax(client); + } + script = ReceivePayload(client, length); + if (!script) { + free(name); + return -1; + } + store = BorrowStore(client); + if (!store) { + free(script); + free(name); + return ConnWriteStr(client->conn, MSG5005DBLIBERR); + } + result = BongoSieveStorePut(store, client->storeName, name, script, length); + ReturnStore(client, &store); + free(script); + free(name); + return ConnWriteF(client->conn, "1000 %d\r\n", result != 0); +} + +static CCode +Get(StoreClient *client, const char *encoded_name) +{ + BongoSieveStore *store; + char *name = NULL; + char *script = NULL; + size_t length = 0; + int active = 0; + int result; + CCode code; + + if (strcmp(encoded_name, "ACTIVE") != 0 && + !(name = HexDecode(encoded_name, 128U))) + return BadSyntax(client); + store = BorrowStore(client); + if (!store) { + free(name); + return ConnWriteStr(client->conn, MSG5005DBLIBERR); + } + result = BongoSieveStoreGet(store, client->storeName, name, + &script, &length, &active); + ReturnStore(client, &store); + free(name); + if (!result) return ConnWriteStr(client->conn, "1000 0\r\n"); + code = ConnWriteF(client->conn, "2002 %zu %d\r\n", length, active); + if (code >= 0) code = ConnWrite(client->conn, script, length); + if (code >= 0) code = ConnWriteStr(client->conn, "\r\n1000\r\n"); + free(script); + return code; +} + +static CCode +SetActive(StoreClient *client, const char *encoded_name) +{ + BongoSieveStore *store; + char *name = NULL; + int result; + + if (strcmp(encoded_name, "-") != 0 && + !(name = HexDecode(encoded_name, 128U))) + return BadSyntax(client); + store = BorrowStore(client); + if (!store) { + free(name); + return ConnWriteStr(client->conn, MSG5005DBLIBERR); + } + result = BongoSieveStoreSetActive(store, client->storeName, name); + ReturnStore(client, &store); + free(name); + return ConnWriteF(client->conn, "1000 %d\r\n", result != 0); +} + +static CCode +Delete(StoreClient *client, const char *encoded_name) +{ + BongoSieveStore *store; + char *name = HexDecode(encoded_name, 128U); + int result; + + if (!name) return BadSyntax(client); + store = BorrowStore(client); + if (!store) { + free(name); + return ConnWriteStr(client->conn, MSG5005DBLIBERR); + } + result = BongoSieveStoreDelete(store, client->storeName, name); + ReturnStore(client, &store); + free(name); + return ConnWriteF(client->conn, "1000 %d\r\n", result != 0); +} + +static CCode +Rename(StoreClient *client, const char *old_encoded, const char *new_encoded) +{ + BongoSieveStore *store; + char *old_name = HexDecode(old_encoded, 128U); + char *new_name = HexDecode(new_encoded, 128U); + int result; + + if (!old_name || !new_name) { + free(old_name); + free(new_name); + return BadSyntax(client); + } + store = BorrowStore(client); + if (!store) { + free(old_name); + free(new_name); + return ConnWriteStr(client->conn, MSG5005DBLIBERR); + } + result = BongoSieveStoreRename(store, client->storeName, + old_name, new_name); + ReturnStore(client, &store); + free(old_name); + free(new_name); + return ConnWriteF(client->conn, "1000 %d\r\n", result != 0); +} + +typedef struct { + StoreClient *client; + int ok; +} ListContext; + +static int +WriteListEntry(const char *name, int active, void *data) +{ + ListContext *context = data; + char *encoded = HexEncode(name); + + if (!encoded || + ConnWriteF(context->client->conn, "2001 %d %s\r\n", + active != 0, encoded) < 0) { + free(encoded); + context->ok = 0; + return 0; + } + free(encoded); + return 1; +} + +static CCode +List(StoreClient *client) +{ + BongoSieveStore *store; + ListContext context = { client, 1 }; + int result; + + store = BorrowStore(client); + if (!store) return ConnWriteStr(client->conn, MSG5005DBLIBERR); + result = BongoSieveStoreList(store, client->storeName, + WriteListEntry, &context); + ReturnStore(client, &store); + if (!result || !context.ok) return -1; + return ConnWriteStr(client->conn, MSG1000OK); +} + +static CCode +VacationClaim(StoreClient *client, const char *encoded_sender, + const char *encoded_handle, const char *interval_text) +{ + BongoSieveStore *store; + char *sender = HexDecode(encoded_sender, 320U); + char *handle = HexDecode(encoded_handle, 128U); + unsigned long interval; + int result; + + if (!sender || !handle || + ParseUnsignedLong(client, (char *)interval_text, &interval) != + TOKEN_OK) { + free(sender); + free(handle); + return BadSyntax(client); + } + store = BorrowStore(client); + if (!store) { + free(sender); + free(handle); + return ConnWriteStr(client->conn, MSG5005DBLIBERR); + } + result = BongoSieveVacationClaim(store, client->storeName, sender, + handle, interval); + ReturnStore(client, &store); + free(sender); + free(handle); + return ConnWriteF(client->conn, "1000 %d\r\n", result != 0); +} + +static CCode +VacationRelease(StoreClient *client, const char *encoded_sender, + const char *encoded_handle) +{ + BongoSieveStore *store; + char *sender = HexDecode(encoded_sender, 320U); + char *handle = HexDecode(encoded_handle, 128U); + + if (!sender || !handle) { + free(sender); + free(handle); + return BadSyntax(client); + } + store = BorrowStore(client); + if (!store) { + free(sender); + free(handle); + return ConnWriteStr(client->conn, MSG5005DBLIBERR); + } + BongoSieveVacationRelease(store, client->storeName, sender, handle); + ReturnStore(client, &store); + free(sender); + free(handle); + return ConnWriteStr(client->conn, MSG1000OK); +} + +CCode +StoreCommandSIEVE(StoreClient *client, int token_count, char **tokens) +{ + if (!IS_MANAGER(client) || !client->storedb || !client->storeName || + !strncmp(client->storeName, "_system", 7)) + return ConnWriteStr(client->conn, MSG3243MANAGERONLY); + if (token_count == 4 && !XplStrCaseCmp(tokens[1], "put")) + return Put(client, tokens[2], tokens[3]); + if (token_count == 3 && !XplStrCaseCmp(tokens[1], "get")) + return Get(client, tokens[2]); + if (token_count == 3 && !XplStrCaseCmp(tokens[1], "active")) + return SetActive(client, tokens[2]); + if (token_count == 3 && !XplStrCaseCmp(tokens[1], "delete")) + return Delete(client, tokens[2]); + if (token_count == 4 && !XplStrCaseCmp(tokens[1], "rename")) + return Rename(client, tokens[2], tokens[3]); + if (token_count == 2 && !XplStrCaseCmp(tokens[1], "list")) + return List(client); + if (token_count == 5 && !XplStrCaseCmp(tokens[1], "vacation-claim")) + return VacationClaim(client, tokens[2], tokens[3], tokens[4]); + if (token_count == 4 && !XplStrCaseCmp(tokens[1], "vacation-release")) + return VacationRelease(client, tokens[2], tokens[3]); + return BadSyntax(client); +} diff --git a/src/agents/store/sieve-command.h b/src/agents/store/sieve-command.h new file mode 100644 index 0000000..cba9856 --- /dev/null +++ b/src/agents/store/sieve-command.h @@ -0,0 +1,26 @@ +/**************************************************************************** + * + * 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. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, contact Novell, Inc. + * + ****************************************************************************/ +// Parts Copyright (C) 2026 Mario Fetka. See COPYING for details. + +#ifndef BONGO_STORE_SIEVE_COMMAND_H +#define BONGO_STORE_SIEVE_COMMAND_H + +#include "stored.h" + +CCode StoreCommandSIEVE(StoreClient *client, int token_count, char **tokens); + +#endif diff --git a/src/libs/sieve/CMakeLists.txt b/src/libs/sieve/CMakeLists.txt index c136871..3c93c5a 100644 --- a/src/libs/sieve/CMakeLists.txt +++ b/src/libs/sieve/CMakeLists.txt @@ -1,8 +1,14 @@ -add_library(libbongosieve sieve.c store.c managesieve.c) +StrictCompile() + +add_library(libbongosieve sieve.c store.c store-nmap.c managesieve.c) set_target_properties(libbongosieve PROPERTIES OUTPUT_NAME bongosieve) target_compile_definitions(libbongosieve PRIVATE BONGO_SIEVE_VALIDATE_PATH="${LIBEXEC_INSTALL_DIR}/bongo/bongo-sieve-validate") -target_link_libraries(libbongosieve PRIVATE Mailutils::Sieve SQLite3::SQLite3) +target_link_libraries(libbongosieve PRIVATE + Mailutils::Sieve + SQLite3::SQLite3 + bongomsgapi + bongoconnio) install(TARGETS libbongosieve DESTINATION ${LIB_INSTALL_DIR}) diff --git a/src/libs/sieve/sieve-store-private.h b/src/libs/sieve/sieve-store-private.h new file mode 100644 index 0000000..c7d5168 --- /dev/null +++ b/src/libs/sieve/sieve-store-private.h @@ -0,0 +1,50 @@ +/**************************************************************************** + * + * 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. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, contact Novell, Inc. + * + ****************************************************************************/ +// Parts Copyright (C) 2026 Mario Fetka. See COPYING for details. + +#ifndef BONGO_SIEVE_STORE_PRIVATE_H +#define BONGO_SIEVE_STORE_PRIVATE_H + +#include +#include +#include +#include + +struct BongoSieveStore { + sqlite3 *database; + Connection *connection; + char *user; + int borrowed_database; +}; + +int BongoSieveNmapPut(BongoSieveStore *store, const char *name, + const char *script, size_t length); +int BongoSieveNmapGet(BongoSieveStore *store, const char *name, + char **script, size_t *length, int *active); +int BongoSieveNmapSetActive(BongoSieveStore *store, const char *name); +int BongoSieveNmapDelete(BongoSieveStore *store, const char *name); +int BongoSieveNmapRename(BongoSieveStore *store, const char *old_name, + const char *new_name); +int BongoSieveNmapVacationClaim(BongoSieveStore *store, const char *sender, + const char *handle, + unsigned long interval_seconds); +void BongoSieveNmapVacationRelease(BongoSieveStore *store, + const char *sender, const char *handle); +int BongoSieveNmapList(BongoSieveStore *store, + BongoSieveListCallback callback, void *data); + +#endif diff --git a/src/libs/sieve/store-nmap.c b/src/libs/sieve/store-nmap.c new file mode 100644 index 0000000..c8e99e9 --- /dev/null +++ b/src/libs/sieve/store-nmap.c @@ -0,0 +1,340 @@ +/**************************************************************************** + * + * 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. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, contact Novell, Inc. + * + ****************************************************************************/ +// Parts Copyright (C) 2026 Mario Fetka. See COPYING for details. + +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "sieve-store-private.h" + +#define SIEVE_WIRE_LIMIT (1024U * 1024U) + +static int +ValidUser(const char *user) +{ + const unsigned char *cursor; + size_t length; + + if (!user || !(length = strlen(user)) || length > 320U) return 0; + for (cursor = (const unsigned char *)user; *cursor; cursor++) { + if (*cursor <= 0x20 || *cursor == 0x7f) return 0; + } + return 1; +} + +static int +HexValue(unsigned char value) +{ + if (value >= '0' && value <= '9') return value - '0'; + value = (unsigned char)tolower(value); + if (value >= 'a' && value <= 'f') return value - 'a' + 10; + return -1; +} + +static char * +HexEncode(const char *input) +{ + static const char digits[] = "0123456789abcdef"; + size_t length; + size_t index; + char *output; + + if (!input || !(length = strlen(input)) || + length > (SIZE_MAX - 1U) / 2U) + return NULL; + output = malloc(length * 2U + 1U); + if (!output) return NULL; + for (index = 0; index < length; index++) { + unsigned char value = (unsigned char)input[index]; + output[index * 2U] = digits[value >> 4]; + output[index * 2U + 1U] = digits[value & 0x0f]; + } + output[length * 2U] = '\0'; + return output; +} + +static char * +HexDecode(const char *input, size_t maximum) +{ + size_t length; + size_t index; + char *output; + + if (!input || !(length = strlen(input)) || (length & 1U) || + length / 2U > maximum) + return NULL; + output = malloc(length / 2U + 1U); + if (!output) return NULL; + for (index = 0; index < length; index += 2U) { + int high = HexValue((unsigned char)input[index]); + int low = HexValue((unsigned char)input[index + 1U]); + if (high < 0 || low < 0 || (high == 0 && low == 0)) { + free(output); + return NULL; + } + output[index / 2U] = (char)((high << 4) | low); + } + output[length / 2U] = '\0'; + return output; +} + +static int +ParseResult(const char *response, int *result) +{ + int count; + + return response && result && + sscanf(response, "%d%n", result, &count) == 1 && + response[count] == '\0' && (*result == 0 || *result == 1); +} + +int +BongoSieveStoreOpenNmap(BongoSieveStore **output, const char *user) +{ + BongoSieveStore *store; + char response[CONN_BUFSIZE + 1]; + + if (!output || !ValidUser(user)) return 0; + *output = NULL; + store = calloc(1, sizeof(*store)); + if (!store) return 0; + store->user = strdup(user); + store->connection = NMAPConnect("127.0.0.1", NULL); + if (!store->user || !store->connection || + !ConnSetTimeout(store->connection, 30) || + !NMAPAuthenticateToStore(store->connection, response, CONN_BUFSIZE) || + NMAPRunCommandF(store->connection, response, sizeof(response), + "STORE %s\r\n", user) != 1000) { + BongoSieveStoreClose(&store); + return 0; + } + *output = store; + return 1; +} + +int +BongoSieveNmapPut(BongoSieveStore *store, const char *name, + const char *script, size_t length) +{ + char response[CONN_BUFSIZE + 1]; + char *encoded = NULL; + int result = 0; + + if (!store || !store->connection || !script || !length || + length > SIEVE_WIRE_LIMIT || !(encoded = HexEncode(name))) + return 0; + if (NMAPSendCommandF(store->connection, "SIEVE PUT %s %zu\r\n", + encoded, length) >= 0 && + NMAPReadAnswer(store->connection, response, sizeof(response), TRUE) == + 2002 && + ConnWrite(store->connection, script, length) == (int)length && + ConnFlush(store->connection) >= 0 && + NMAPReadAnswer(store->connection, response, sizeof(response), TRUE) == + 1000 && + ParseResult(response, &result)) { + free(encoded); + return result; + } + free(encoded); + return 0; +} + +int +BongoSieveNmapGet(BongoSieveStore *store, const char *name, + char **script, size_t *length, int *active) +{ + char response[CONN_BUFSIZE + 1]; + char *encoded = NULL; + char *end = NULL; + uintmax_t parsed; + int is_active; + int count; + int code; + + if (!store || !store->connection || !script || !length || + (name && !(encoded = HexEncode(name)))) + return 0; + if (NMAPSendCommandF(store->connection, "SIEVE GET %s\r\n", + name ? encoded : "ACTIVE") < 0) { + free(encoded); + return 0; + } + free(encoded); + code = NMAPReadAnswer(store->connection, response, sizeof(response), TRUE); + if (code == 1000 && strcmp(response, "0") == 0) return 0; + if (code != 2002 || + sscanf(response, "%ju %d%n", &parsed, &is_active, &count) != 2 || + response[count] != '\0' || parsed == 0 || + parsed > SIEVE_WIRE_LIMIT || parsed > SIZE_MAX || + (is_active != 0 && is_active != 1)) + return 0; + errno = 0; + (void)strtoumax(response, &end, 10); + if (errno == ERANGE || end == response) return 0; + *script = malloc((size_t)parsed + 1U); + if (!*script) return 0; + if (NMAPReadCount(store->connection, *script, (size_t)parsed) != + (int)parsed || + NMAPReadCrLf(store->connection) != 2 || + NMAPReadAnswer(store->connection, response, sizeof(response), TRUE) != + 1000) { + free(*script); + *script = NULL; + return 0; + } + (*script)[parsed] = '\0'; + *length = (size_t)parsed; + if (active) *active = is_active; + return 1; +} + +int +BongoSieveNmapSetActive(BongoSieveStore *store, const char *name) +{ + char response[CONN_BUFSIZE + 1]; + char *encoded = NULL; + int result; + int code; + + if (!store || !store->connection || (name && *name && + !(encoded = HexEncode(name)))) + return 0; + code = NMAPRunCommandF(store->connection, response, sizeof(response), + "SIEVE ACTIVE %s\r\n", + name && *name ? encoded : "-"); + free(encoded); + return code == 1000 && ParseResult(response, &result) && result; +} + +int +BongoSieveNmapDelete(BongoSieveStore *store, const char *name) +{ + char response[CONN_BUFSIZE + 1]; + char *encoded = HexEncode(name); + int result; + int code; + + if (!store || !store->connection || !encoded) return 0; + code = NMAPRunCommandF(store->connection, response, sizeof(response), + "SIEVE DELETE %s\r\n", encoded); + free(encoded); + return code == 1000 && ParseResult(response, &result) && result; +} + +int +BongoSieveNmapRename(BongoSieveStore *store, const char *old_name, + const char *new_name) +{ + char response[CONN_BUFSIZE + 1]; + char *old_encoded = HexEncode(old_name); + char *new_encoded = HexEncode(new_name); + int result; + int code; + + if (!store || !store->connection || !old_encoded || !new_encoded) { + free(old_encoded); + free(new_encoded); + return 0; + } + code = NMAPRunCommandF(store->connection, response, sizeof(response), + "SIEVE RENAME %s %s\r\n", + old_encoded, new_encoded); + free(old_encoded); + free(new_encoded); + return code == 1000 && ParseResult(response, &result) && result; +} + +int +BongoSieveNmapVacationClaim(BongoSieveStore *store, const char *sender, + const char *handle, + unsigned long interval_seconds) +{ + char response[CONN_BUFSIZE + 1]; + char *sender_encoded = HexEncode(sender); + char *handle_encoded = HexEncode(handle); + int result; + int code; + + if (!store || !store->connection || !sender_encoded || !handle_encoded) { + free(sender_encoded); + free(handle_encoded); + return 0; + } + code = NMAPRunCommandF(store->connection, response, sizeof(response), + "SIEVE VACATION-CLAIM %s %s %lu\r\n", + sender_encoded, handle_encoded, interval_seconds); + free(sender_encoded); + free(handle_encoded); + return code == 1000 && ParseResult(response, &result) && result; +} + +void +BongoSieveNmapVacationRelease(BongoSieveStore *store, const char *sender, + const char *handle) +{ + char response[CONN_BUFSIZE + 1]; + char *sender_encoded = HexEncode(sender); + char *handle_encoded = HexEncode(handle); + + if (store && store->connection && sender_encoded && handle_encoded) { + (void)NMAPRunCommandF(store->connection, response, sizeof(response), + "SIEVE VACATION-RELEASE %s %s\r\n", + sender_encoded, handle_encoded); + } + free(sender_encoded); + free(handle_encoded); +} + +int +BongoSieveNmapList(BongoSieveStore *store, + BongoSieveListCallback callback, void *data) +{ + char response[CONN_BUFSIZE + 1]; + int code; + + if (!store || !store->connection || !callback || + NMAPSendCommandF(store->connection, "SIEVE LIST\r\n") < 0) + return 0; + while ((code = NMAPReadAnswer(store->connection, response, + sizeof(response), TRUE)) == 2001) { + char encoded[257]; + char *name; + int active; + int count; + + if (sscanf(response, "%d %256s%n", &active, encoded, &count) != 2 || + response[count] != '\0' || (active != 0 && active != 1) || + !(name = HexDecode(encoded, 128U))) + return 0; + if (!callback(name, active, data)) { + free(name); + return 0; + } + free(name); + } + return code == 1000; +} diff --git a/src/libs/sieve/store.c b/src/libs/sieve/store.c index b5ce81d..d8616aa 100644 --- a/src/libs/sieve/store.c +++ b/src/libs/sieve/store.c @@ -19,14 +19,26 @@ * ****************************************************************************/ +#include + #include #include #include #include -struct BongoSieveStore { - sqlite3 *database; -}; +#include "sieve-store-private.h" + +static const char SieveSchema[] = + "PRAGMA foreign_keys=ON;" + "CREATE TABLE IF NOT EXISTS sieve_scripts (" + " name TEXT PRIMARY KEY, script BLOB NOT NULL," + " active INTEGER NOT NULL DEFAULT 0 CHECK(active IN (0,1))," + " revision INTEGER NOT NULL DEFAULT 1, updated_at INTEGER NOT NULL);" + "CREATE UNIQUE INDEX IF NOT EXISTS sieve_one_active " + " ON sieve_scripts(active) WHERE active=1;" + "CREATE TABLE IF NOT EXISTS sieve_vacation_replies (" + " sender TEXT NOT NULL, handle TEXT NOT NULL," + " replied_at INTEGER NOT NULL, PRIMARY KEY(sender,handle));"; static int valid_identity(const char *value, size_t maximum, int script_name) @@ -42,31 +54,33 @@ valid_identity(const char *value, size_t maximum, int script_name) return 1; } +static int +valid_store_user(BongoSieveStore *store, const char *user) +{ + if (!store || !valid_identity(user, 320, 0)) return 0; + if (!store->user) { + store->user = strdup(user); + if (!store->user) return 0; + } + return strcmp(store->user, user) == 0; +} + int BongoSieveStoreOpen(BongoSieveStore **output, const char *path) { BongoSieveStore *store; - static const char schema[] = - "PRAGMA journal_mode=WAL;" - "PRAGMA foreign_keys=ON;" - "CREATE TABLE IF NOT EXISTS sieve_scripts (" - " user TEXT NOT NULL, name TEXT NOT NULL, script BLOB NOT NULL," - " active INTEGER NOT NULL DEFAULT 0 CHECK(active IN (0,1))," - " revision INTEGER NOT NULL DEFAULT 1, updated_at INTEGER NOT NULL," - " PRIMARY KEY(user,name));" - "CREATE UNIQUE INDEX IF NOT EXISTS sieve_one_active " - " ON sieve_scripts(user) WHERE active=1;" - "CREATE TABLE IF NOT EXISTS sieve_vacation_replies (" - " user TEXT NOT NULL, sender TEXT NOT NULL, handle TEXT NOT NULL," - " replied_at INTEGER NOT NULL, PRIMARY KEY(user,sender,handle));"; if (!output || !path || !*path) return 0; *output = NULL; store = calloc(1, sizeof(*store)); if (!store) return 0; if (sqlite3_open_v2(path, &store->database, - SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, NULL) != SQLITE_OK || + SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | + SQLITE_OPEN_FULLMUTEX, NULL) != SQLITE_OK || sqlite3_busy_timeout(store->database, 5000) != SQLITE_OK || - sqlite3_exec(store->database, schema, NULL, NULL, NULL) != SQLITE_OK) { + sqlite3_exec(store->database, "PRAGMA journal_mode=WAL;", + NULL, NULL, NULL) != SQLITE_OK || + sqlite3_exec(store->database, SieveSchema, NULL, NULL, NULL) != + SQLITE_OK) { BongoSieveStoreClose(&store); return 0; } @@ -74,11 +88,51 @@ BongoSieveStoreOpen(BongoSieveStore **output, const char *path) return 1; } +int +BongoSieveStoreAttach(BongoSieveStore **output, sqlite3 *database) +{ + BongoSieveStore *store; + + if (!output || !database) return 0; + *output = NULL; + store = calloc(1, sizeof(*store)); + if (!store) return 0; + store->database = database; + store->borrowed_database = 1; + if (sqlite3_exec(database, SieveSchema, NULL, NULL, NULL) != SQLITE_OK) { + BongoSieveStoreClose(&store); + return 0; + } + *output = store; + return 1; +} + +int +BongoSieveStoreBorrow(BongoSieveStore **output, sqlite3 *database) +{ + BongoSieveStore *store; + + if (!output || !database) return 0; + *output = NULL; + store = calloc(1, sizeof(*store)); + if (!store) return 0; + store->database = database; + store->borrowed_database = 1; + *output = store; + return 1; +} + void BongoSieveStoreClose(BongoSieveStore **store) { if (!store || !*store) return; - if ((*store)->database) sqlite3_close((*store)->database); + if ((*store)->connection) { + ConnClose((*store)->connection); + ConnFree((*store)->connection); + } + if ((*store)->database && !(*store)->borrowed_database) + sqlite3_close((*store)->database); + free((*store)->user); free(*store); *store = NULL; } @@ -90,15 +144,16 @@ BongoSieveStorePut(BongoSieveStore *store, const char *user, sqlite3_stmt *statement = NULL; int result = 0; static const char sql[] = - "INSERT INTO sieve_scripts(user,name,script,updated_at) " - "VALUES(?,?,?,unixepoch()) ON CONFLICT(user,name) DO UPDATE SET " + "INSERT INTO sieve_scripts(name,script,updated_at) " + "VALUES(?,?,unixepoch()) ON CONFLICT(name) DO UPDATE SET " "script=excluded.script, revision=revision+1, updated_at=unixepoch()"; - if (!store || !valid_identity(user, 320, 0) || + if (!valid_store_user(store, user) || !valid_identity(name, 128, 1) || !BongoSieveValidate(script, length)) return 0; + if (store->connection) + return BongoSieveNmapPut(store, name, script, length); if (sqlite3_prepare_v2(store->database, sql, -1, &statement, NULL) == SQLITE_OK && - sqlite3_bind_text(statement, 1, user, -1, SQLITE_TRANSIENT) == SQLITE_OK && - sqlite3_bind_text(statement, 2, name, -1, SQLITE_TRANSIENT) == SQLITE_OK && - sqlite3_bind_blob(statement, 3, script, (int)length, SQLITE_TRANSIENT) == SQLITE_OK && + sqlite3_bind_text(statement, 1, name, -1, SQLITE_TRANSIENT) == SQLITE_OK && + sqlite3_bind_blob(statement, 2, script, (int)length, SQLITE_TRANSIENT) == SQLITE_OK && sqlite3_step(statement) == SQLITE_DONE) result = 1; sqlite3_finalize(statement); return result; @@ -113,15 +168,17 @@ BongoSieveStoreGet(BongoSieveStore *store, const char *user, int bytes; int result = 0; const char *sql = name ? - "SELECT script,active FROM sieve_scripts WHERE user=? AND name=?" : - "SELECT script,active FROM sieve_scripts WHERE user=? AND active=1"; + "SELECT script,active FROM sieve_scripts WHERE name=?" : + "SELECT script,active FROM sieve_scripts WHERE active=1"; if (!store || !script || !length || !valid_identity(user, 320, 0) || (name && !valid_identity(name, 128, 1))) return 0; + if (!valid_store_user(store, user)) return 0; *script = NULL; *length = 0; + if (store->connection) + return BongoSieveNmapGet(store, name, script, length, active); if (sqlite3_prepare_v2(store->database, sql, -1, &statement, NULL) != SQLITE_OK || - sqlite3_bind_text(statement, 1, user, -1, SQLITE_TRANSIENT) != SQLITE_OK || - (name && sqlite3_bind_text(statement, 2, name, -1, SQLITE_TRANSIENT) != SQLITE_OK) || + (name && sqlite3_bind_text(statement, 1, name, -1, SQLITE_TRANSIENT) != SQLITE_OK) || sqlite3_step(statement) != SQLITE_ROW) goto finish; data = sqlite3_column_blob(statement, 0); bytes = sqlite3_column_bytes(statement, 0); @@ -142,23 +199,22 @@ BongoSieveStoreSetActive(BongoSieveStore *store, const char *user, const char *n { sqlite3_stmt *statement = NULL; int result = 0; - if (!store || !valid_identity(user, 320, 0) || + if (!valid_store_user(store, user) || (name && *name && !valid_identity(name, 128, 1))) return 0; + if (store->connection) return BongoSieveNmapSetActive(store, name); if (sqlite3_exec(store->database, "BEGIN IMMEDIATE", NULL, NULL, NULL) != SQLITE_OK) return 0; if (sqlite3_prepare_v2(store->database, - "UPDATE sieve_scripts SET active=0 WHERE user=?", -1, + "UPDATE sieve_scripts SET active=0", -1, &statement, NULL) == SQLITE_OK && - sqlite3_bind_text(statement, 1, user, -1, SQLITE_TRANSIENT) == SQLITE_OK && sqlite3_step(statement) == SQLITE_DONE) result = 1; sqlite3_finalize(statement); statement = NULL; if (result && name && *name) { result = 0; if (sqlite3_prepare_v2(store->database, - "UPDATE sieve_scripts SET active=1 WHERE user=? AND name=?", + "UPDATE sieve_scripts SET active=1 WHERE name=?", -1, &statement, NULL) == SQLITE_OK && - sqlite3_bind_text(statement, 1, user, -1, SQLITE_TRANSIENT) == SQLITE_OK && - sqlite3_bind_text(statement, 2, name, -1, SQLITE_TRANSIENT) == SQLITE_OK && + sqlite3_bind_text(statement, 1, name, -1, SQLITE_TRANSIENT) == SQLITE_OK && sqlite3_step(statement) == SQLITE_DONE && sqlite3_changes(store->database) == 1) result = 1; sqlite3_finalize(statement); @@ -172,12 +228,12 @@ BongoSieveStoreDelete(BongoSieveStore *store, const char *user, const char *name { sqlite3_stmt *statement = NULL; int result = 0; - if (!store || !valid_identity(user, 320, 0) || !valid_identity(name, 128, 1)) return 0; + if (!valid_store_user(store, user) || !valid_identity(name, 128, 1)) return 0; + if (store->connection) return BongoSieveNmapDelete(store, name); if (sqlite3_prepare_v2(store->database, - "DELETE FROM sieve_scripts WHERE user=? AND name=? AND active=0", + "DELETE FROM sieve_scripts WHERE name=? AND active=0", -1, &statement, NULL) == SQLITE_OK && - sqlite3_bind_text(statement, 1, user, -1, SQLITE_TRANSIENT) == SQLITE_OK && - sqlite3_bind_text(statement, 2, name, -1, SQLITE_TRANSIENT) == SQLITE_OK && + sqlite3_bind_text(statement, 1, name, -1, SQLITE_TRANSIENT) == SQLITE_OK && sqlite3_step(statement) == SQLITE_DONE && sqlite3_changes(store->database) == 1) result = 1; sqlite3_finalize(statement); return result; @@ -189,19 +245,19 @@ BongoSieveStoreRename(BongoSieveStore *store, const char *user, { sqlite3_stmt *statement = NULL; int result = 0; - if (!store || !valid_identity(user, 320, 0) || + if (!valid_store_user(store, user) || !valid_identity(old_name, 128, 1) || !valid_identity(new_name, 128, 1) || strcmp(old_name, new_name) == 0) return 0; + if (store->connection) + return BongoSieveNmapRename(store, old_name, new_name); if (sqlite3_prepare_v2(store->database, "UPDATE sieve_scripts SET name=?, revision=revision+1, " - "updated_at=unixepoch() WHERE user=? AND name=? AND NOT EXISTS " - "(SELECT 1 FROM sieve_scripts WHERE user=? AND name=?)", + "updated_at=unixepoch() WHERE name=? AND NOT EXISTS " + "(SELECT 1 FROM sieve_scripts WHERE name=?)", -1, &statement, NULL) == SQLITE_OK && sqlite3_bind_text(statement, 1, new_name, -1, SQLITE_TRANSIENT) == SQLITE_OK && - sqlite3_bind_text(statement, 2, user, -1, SQLITE_TRANSIENT) == SQLITE_OK && - sqlite3_bind_text(statement, 3, old_name, -1, SQLITE_TRANSIENT) == SQLITE_OK && - sqlite3_bind_text(statement, 4, user, -1, SQLITE_TRANSIENT) == SQLITE_OK && - sqlite3_bind_text(statement, 5, new_name, -1, SQLITE_TRANSIENT) == SQLITE_OK && + sqlite3_bind_text(statement, 2, old_name, -1, SQLITE_TRANSIENT) == SQLITE_OK && + sqlite3_bind_text(statement, 3, new_name, -1, SQLITE_TRANSIENT) == SQLITE_OK && sqlite3_step(statement) == SQLITE_DONE && sqlite3_changes(store->database) == 1) { result = 1; } @@ -217,17 +273,19 @@ BongoSieveVacationClaim(BongoSieveStore *store, const char *user, sqlite3_stmt *statement = NULL; int result = 0; static const char sql[] = - "INSERT INTO sieve_vacation_replies(user,sender,handle,replied_at) " - "VALUES(?,?,?,unixepoch()) ON CONFLICT(user,sender,handle) DO UPDATE SET " + "INSERT INTO sieve_vacation_replies(sender,handle,replied_at) " + "VALUES(?,?,unixepoch()) ON CONFLICT(sender,handle) DO UPDATE SET " "replied_at=unixepoch() WHERE replied_at <= unixepoch()-?"; - if (!store || !valid_identity(user, 320, 0) || + if (!valid_store_user(store, user) || !valid_identity(sender, 320, 0) || !valid_identity(handle, 128, 1) || interval_seconds > 366UL * 86400UL) return 0; + if (store->connection) + return BongoSieveNmapVacationClaim(store, sender, handle, + interval_seconds); if (sqlite3_prepare_v2(store->database, sql, -1, &statement, NULL) == SQLITE_OK && - sqlite3_bind_text(statement, 1, user, -1, SQLITE_TRANSIENT) == SQLITE_OK && - sqlite3_bind_text(statement, 2, sender, -1, SQLITE_TRANSIENT) == SQLITE_OK && - sqlite3_bind_text(statement, 3, handle, -1, SQLITE_TRANSIENT) == SQLITE_OK && - sqlite3_bind_int64(statement, 4, (sqlite3_int64)interval_seconds) == SQLITE_OK && + sqlite3_bind_text(statement, 1, sender, -1, SQLITE_TRANSIENT) == SQLITE_OK && + sqlite3_bind_text(statement, 2, handle, -1, SQLITE_TRANSIENT) == SQLITE_OK && + sqlite3_bind_int64(statement, 3, (sqlite3_int64)interval_seconds) == SQLITE_OK && sqlite3_step(statement) == SQLITE_DONE && sqlite3_changes(store->database) == 1) { result = 1; } @@ -240,13 +298,16 @@ BongoSieveVacationRelease(BongoSieveStore *store, const char *user, const char *sender, const char *handle) { sqlite3_stmt *statement = NULL; - if (!store || !user || !sender || !handle) return; + if (!valid_store_user(store, user) || !sender || !handle) return; + if (store->connection) { + BongoSieveNmapVacationRelease(store, sender, handle); + return; + } if (sqlite3_prepare_v2(store->database, - "DELETE FROM sieve_vacation_replies WHERE user=? AND sender=? AND handle=?", + "DELETE FROM sieve_vacation_replies WHERE sender=? AND handle=?", -1, &statement, NULL) == SQLITE_OK) { - sqlite3_bind_text(statement, 1, user, -1, SQLITE_TRANSIENT); - sqlite3_bind_text(statement, 2, sender, -1, SQLITE_TRANSIENT); - sqlite3_bind_text(statement, 3, handle, -1, SQLITE_TRANSIENT); + sqlite3_bind_text(statement, 1, sender, -1, SQLITE_TRANSIENT); + sqlite3_bind_text(statement, 2, handle, -1, SQLITE_TRANSIENT); sqlite3_step(statement); } sqlite3_finalize(statement); @@ -258,11 +319,12 @@ BongoSieveStoreList(BongoSieveStore *store, const char *user, { sqlite3_stmt *statement = NULL; int result = 0; - if (!store || !callback || !valid_identity(user, 320, 0)) return 0; + if (!callback || !valid_store_user(store, user)) return 0; + if (store->connection) + return BongoSieveNmapList(store, callback, data); if (sqlite3_prepare_v2(store->database, - "SELECT name,active FROM sieve_scripts WHERE user=? ORDER BY name", - -1, &statement, NULL) != SQLITE_OK || - sqlite3_bind_text(statement, 1, user, -1, SQLITE_TRANSIENT) != SQLITE_OK) { + "SELECT name,active FROM sieve_scripts ORDER BY name", + -1, &statement, NULL) != SQLITE_OK) { sqlite3_finalize(statement); return 0; } diff --git a/src/libs/sieve/tests/store-test.c b/src/libs/sieve/tests/store-test.c index 8ca8c98..6af6b39 100644 --- a/src/libs/sieve/tests/store-test.c +++ b/src/libs/sieve/tests/store-test.c @@ -40,6 +40,8 @@ static int store_test(void) !BongoSieveStoreSetActive(store, "alice@example.test", "main") || !BongoSieveStoreGet(store, "alice@example.test", NULL, &loaded, &length, &active) || length != strlen(script) || memcmp(loaded, script, length) || !active || + BongoSieveStoreGet(store, "bob@example.test", NULL, + &loaded, &length, &active) || BongoSieveStoreSetActive(store, "alice@example.test", "missing") || BongoSieveStoreDelete(store, "alice@example.test", "main")) { free(loaded);