Store Sieve state in user stores
Debian Trixie package bundle / packages (push) Successful in 22m11s

This commit is contained in:
Mario Fetka
2026-07-28 17:48:55 +02:00
parent f2dd078d62
commit b0cd6f35a2
19 changed files with 1133 additions and 116 deletions
+118
View File
@@ -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),
+3 -2
View File
@@ -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
+5 -5
View File
@@ -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`
+9
View File
@@ -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);
+16 -20
View File
@@ -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;
-1
View File
@@ -116,7 +116,6 @@ typedef struct _RulesAgentGlobals {
void *OutgoingPool;
BongoThreadPool *OutgoingThreadPool;
bongo_ssl_context *SSL_Context;
BongoSieveStore *SieveStore;
} RulesAgentGlobals;
extern RulesAgentGlobals RulesAgent;
+8 -7
View File
@@ -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;
+2
View File
@@ -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
-17
View File
@@ -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);
+6
View File
@@ -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;
+1
View File
@@ -98,6 +98,7 @@ typedef enum {
STORE_COMMAND_NOOP,
STORE_COMMAND_QUOTA,
STORE_COMMAND_SCHEDULER,
STORE_COMMAND_SIEVE,
STORE_COMMAND_TIMEOUT,
STORE_COMMAND_TLSRPT,
+14
View File
@@ -3,6 +3,7 @@
#include <memmgr.h>
#include <msgapi.h>
#include <bongoscheduler.h>
#include <bongosieve.h>
#include <bongotlsrpt.h>
#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++;
+401
View File
@@ -0,0 +1,401 @@
/****************************************************************************
* <Novell-copyright>
* Copyright (c) 2001 Novell, Inc. All Rights Reserved.
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of version 2 of the GNU General Public License
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, contact Novell, Inc.
* </Novell-copyright>
****************************************************************************/
// Parts Copyright (C) 2026 Mario Fetka. See COPYING for details.
#include <config.h>
#include "sieve-command.h"
#include "command-parsing.h"
#include "messages.h"
#include <bongosieve.h>
#include <nmap.h>
#include <ctype.h>
#include <errno.h>
#include <inttypes.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#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);
}
+26
View File
@@ -0,0 +1,26 @@
/****************************************************************************
* <Novell-copyright>
* Copyright (c) 2001 Novell, Inc. All Rights Reserved.
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of version 2 of the GNU General Public License
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, contact Novell, Inc.
* </Novell-copyright>
****************************************************************************/
// 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
+8 -2
View File
@@ -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})
+50
View File
@@ -0,0 +1,50 @@
/****************************************************************************
* <Novell-copyright>
* Copyright (c) 2001 Novell, Inc. All Rights Reserved.
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of version 2 of the GNU General Public License
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, contact Novell, Inc.
* </Novell-copyright>
****************************************************************************/
// Parts Copyright (C) 2026 Mario Fetka. See COPYING for details.
#ifndef BONGO_SIEVE_STORE_PRIVATE_H
#define BONGO_SIEVE_STORE_PRIVATE_H
#include <bongosieve.h>
#include <xpl.h>
#include <connio.h>
#include <sqlite3.h>
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
+340
View File
@@ -0,0 +1,340 @@
/****************************************************************************
* <Novell-copyright>
* Copyright (c) 2001 Novell, Inc. All Rights Reserved.
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of version 2 of the GNU General Public License
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, contact Novell, Inc.
* </Novell-copyright>
****************************************************************************/
// Parts Copyright (C) 2026 Mario Fetka. See COPYING for details.
#include <config.h>
#include <xpl.h>
#include <nmlib.h>
#include <ctype.h>
#include <errno.h>
#include <inttypes.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#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;
}
+124 -62
View File
@@ -19,14 +19,26 @@
* </Novell-copyright>
****************************************************************************/
#include <config.h>
#include <bongosieve.h>
#include <sqlite3.h>
#include <stdlib.h>
#include <string.h>
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;
}
+2
View File
@@ -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);