From 75229111d6fad5ffd1df07b791e0600166701bc9 Mon Sep 17 00:00:00 2001 From: Mario Fetka Date: Sat, 18 Jul 2026 14:59:06 +0200 Subject: [PATCH] Add vtestmail Collector integration tests --- docs/development.md | 27 ++ docs/external-accounts.md | 6 + src/agents/collector/CMakeLists.txt | 62 +++++ src/agents/collector/external_transport.c | 12 + .../tests/BongoVtestmailFixture.java | 246 ++++++++++++++++++ .../collector/tests/vtestmail-integration.py | 119 +++++++++ .../tests/vtestmail-transport-test.c | 158 +++++++++++ 7 files changed, 630 insertions(+) create mode 100644 src/agents/collector/tests/BongoVtestmailFixture.java create mode 100644 src/agents/collector/tests/vtestmail-integration.py create mode 100644 src/agents/collector/tests/vtestmail-transport-test.c diff --git a/docs/development.md b/docs/development.md index ec7a2cc..0ad67c7 100644 --- a/docs/development.md +++ b/docs/development.md @@ -89,3 +89,30 @@ New C and C header files use the same licence/copyright header style as the surrounding subsystem. Generated data and suffixless JSON configuration templates do not need a C source header. Add focused tests beside the subsystem's existing `tests` directory and register them with CTest. + +## Collector protocol integration tests + +The normal test suite has no Java dependency. An optional integration test +uses the archived Apache-2.0-licensed `vtestmail-core` 1.0.0 artifact as an +in-memory POP3 and IMAP peer. The artifact is test-only, its SHA-256 is +pinned by CMake, and it is never linked into or installed with Bongo. + +Download the immutable Maven Central artifact and enable the fixture explicitly: + +```sh +curl -fL \ + https://repo.maven.apache.org/maven2/net/markwalder/vtestmail-core/1.0.0/vtestmail-core-1.0.0.jar \ + -o /tmp/vtestmail-core-1.0.0.jar +cmake -S . -B build-vtestmail \ + -DBUILD_TESTING=ON \ + -DBONGO_VTESTMAIL_JAR=/tmp/vtestmail-core-1.0.0.jar +cmake --build build-vtestmail +ctest --test-dir build-vtestmail -R collector-vtestmail --output-on-failure +``` + +The test exercises authenticated POP3 and IMAP listing, fetching and deletion +over STLS/STARTTLS with certificate and hostname verification enabled. +vtestmail 1.0.0 does not implement the IMAP `UID SEARCH`, `UID FETCH` and +`UID STORE` subset needed by the Collector, so Bongo registers those three +minimal commands inside its isolated Java fixture. The upstream jar remains +unchanged. diff --git a/docs/external-accounts.md b/docs/external-accounts.md index c75538f..ff21a05 100644 --- a/docs/external-accounts.md +++ b/docs/external-accounts.md @@ -15,6 +15,12 @@ passwords are never included in URLs or log messages. Existing plaintext credential records from early 0.7 development versions are encrypted automatically the next time they are read. +For an internal provider signed by a private certificate authority, set +`BONGO_COLLECTOR_CAINFO` in the Collector service environment to the absolute +path of a PEM CA bundle. Certificate and host-name verification remain +enabled. Prefer installing a site-wide CA through the operating system trust +store when possible. + Remote deletion policies are `never`, `after_import`, and `after_days`. Messages are recorded as imported only after the Bongo store accepts them over NMAP. A failed remote deletion does not cause the message to be delivered diff --git a/src/agents/collector/CMakeLists.txt b/src/agents/collector/CMakeLists.txt index 6c91dad..fdcb884 100644 --- a/src/agents/collector/CMakeLists.txt +++ b/src/agents/collector/CMakeLists.txt @@ -29,4 +29,66 @@ if(BUILD_TESTING) ) target_link_libraries(collector-transport-test CURL::libcurl) add_test(NAME collector-external-transport COMMAND collector-transport-test) + + set(BONGO_VTESTMAIL_JAR "" CACHE FILEPATH + "Path to vtestmail-core 1.0.0 for the optional Collector integration test") + if(BONGO_VTESTMAIL_JAR) + if(NOT EXISTS "${BONGO_VTESTMAIL_JAR}") + message(FATAL_ERROR + "BONGO_VTESTMAIL_JAR does not exist: ${BONGO_VTESTMAIL_JAR}") + endif() + + file(SHA256 "${BONGO_VTESTMAIL_JAR}" BONGO_VTESTMAIL_SHA256) + set(BONGO_VTESTMAIL_EXPECTED_SHA256 + "9e330f028e646265e1ef81f9687ed21a2e0a2f3b69e04099b8e8a55f06b9d7f4") + if(NOT BONGO_VTESTMAIL_SHA256 STREQUAL + BONGO_VTESTMAIL_EXPECTED_SHA256) + message(FATAL_ERROR + "BONGO_VTESTMAIL_JAR is not the pinned vtestmail-core 1.0.0 artifact\n" + "Expected SHA-256: ${BONGO_VTESTMAIL_EXPECTED_SHA256}\n" + "Actual SHA-256: ${BONGO_VTESTMAIL_SHA256}") + endif() + + find_package(Java 11 COMPONENTS Runtime Development REQUIRED) + set(BONGO_VTESTMAIL_CLASSES + "${CMAKE_CURRENT_BINARY_DIR}/vtestmail-classes") + set(BONGO_VTESTMAIL_FIXTURE_CLASS + "${BONGO_VTESTMAIL_CLASSES}/net/markwalder/vtestmail/imap/BongoVtestmailFixture.class") + + add_custom_command( + OUTPUT "${BONGO_VTESTMAIL_FIXTURE_CLASS}" + COMMAND "${CMAKE_COMMAND}" -E make_directory + "${BONGO_VTESTMAIL_CLASSES}" + COMMAND "${Java_JAVAC_EXECUTABLE}" + -encoding UTF-8 + -classpath "${BONGO_VTESTMAIL_JAR}" + -d "${BONGO_VTESTMAIL_CLASSES}" + "${CMAKE_CURRENT_SOURCE_DIR}/tests/BongoVtestmailFixture.java" + DEPENDS + "${CMAKE_CURRENT_SOURCE_DIR}/tests/BongoVtestmailFixture.java" + "${BONGO_VTESTMAIL_JAR}" + VERBATIM) + add_custom_target(collector-vtestmail-fixture-classes + DEPENDS "${BONGO_VTESTMAIL_FIXTURE_CLASS}") + + add_executable(collector-vtestmail-transport-test + tests/vtestmail-transport-test.c + external_transport.c) + target_include_directories(collector-vtestmail-transport-test PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(collector-vtestmail-transport-test + CURL::libcurl) + add_dependencies(collector-vtestmail-transport-test + collector-vtestmail-fixture-classes) + + add_test(NAME collector-vtestmail-integration + COMMAND "${Python3_EXECUTABLE}" + "${CMAKE_CURRENT_SOURCE_DIR}/tests/vtestmail-integration.py" + --java "${Java_JAVA_EXECUTABLE}" + --classpath "${BONGO_VTESTMAIL_CLASSES}:${BONGO_VTESTMAIL_JAR}" + --probe "$") + set_tests_properties(collector-vtestmail-integration PROPERTIES + LABELS "collector;integration" + TIMEOUT 60) + endif() endif() diff --git a/src/agents/collector/external_transport.c b/src/agents/collector/external_transport.c index 67c80d7..34e30ed 100644 --- a/src/agents/collector/external_transport.c +++ b/src/agents/collector/external_transport.c @@ -55,6 +55,15 @@ SetError(char *error, size_t error_size, const char *message) } } +static void +ConfigureTlsTrust(CURL *curl) +{ + const char *ca_file = getenv("BONGO_COLLECTOR_CAINFO"); + if (ca_file != NULL && ca_file[0] != '\0') { + curl_easy_setopt(curl, CURLOPT_CAINFO, ca_file); + } +} + static size_t CollectResponse(char *data, size_t size, size_t count, void *context) { @@ -264,6 +273,7 @@ CollectorRemoteFetch(const CollectorExternalAccount *account, curl_easy_setopt(curl, CURLOPT_USE_SSL, CURLUSESSL_ALL); curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L); curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 2L); + ConfigureTlsTrust(curl); curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1L); curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 30L); curl_easy_setopt(curl, CURLOPT_TIMEOUT, 300L); @@ -353,6 +363,7 @@ CollectorRemoteDelete(const CollectorExternalAccount *account, curl_easy_setopt(curl, CURLOPT_USE_SSL, CURLUSESSL_ALL); curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L); curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 2L); + ConfigureTlsTrust(curl); curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1L); curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 30L); curl_easy_setopt(curl, CURLOPT_TIMEOUT, 120L); @@ -441,6 +452,7 @@ CollectorRemoteList(const CollectorExternalAccount *account, curl_easy_setopt(curl, CURLOPT_USE_SSL, CURLUSESSL_ALL); curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L); curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 2L); + ConfigureTlsTrust(curl); curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1L); curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 30L); curl_easy_setopt(curl, CURLOPT_TIMEOUT, 120L); diff --git a/src/agents/collector/tests/BongoVtestmailFixture.java b/src/agents/collector/tests/BongoVtestmailFixture.java new file mode 100644 index 0000000..8cc03c6 --- /dev/null +++ b/src/agents/collector/tests/BongoVtestmailFixture.java @@ -0,0 +1,246 @@ +/* + * Copyright 2026 The Bongo Project contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.markwalder.vtestmail.imap; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.AtomicMoveNotSupportedException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.security.KeyStore; +import java.security.cert.Certificate; +import java.util.Base64; +import java.util.List; +import java.util.Locale; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicBoolean; +import net.markwalder.vtestmail.pop3.Pop3Server; +import net.markwalder.vtestmail.store.Mailbox; +import net.markwalder.vtestmail.store.MailboxFolder; +import net.markwalder.vtestmail.store.MailboxMessage; +import net.markwalder.vtestmail.store.MailboxStore; + +/** Standalone vtestmail process used by Bongo's Collector integration test. */ +public final class BongoVtestmailFixture { + + private static final String PASSWORD = "collector-secret"; + + private BongoVtestmailFixture() { + } + + public static void main(String[] arguments) throws Exception { + if (arguments.length != 2) { + throw new IllegalArgumentException("expected READY_FILE and CA_FILE"); + } + + Path readyFile = Path.of(arguments[0]); + Path caFile = Path.of(arguments[1]); + MailboxStore store = new MailboxStore(); + addMessage(store, "pop-user", "pop-user@example.test", "pop3"); + addMessage(store, "imap-user", "imap-user@example.test", "imap"); + + Pop3Server pop3 = new Pop3Server(store); + ImapServer imap = new ImapServer(store); + imap.addCommand("UID", UidCommand::parse); + /* libcurl still enforces STARTTLS through CURLUSESSL_ALL. */ + imap.setLoginDisabled(false); + + pop3.setSSLProtocol("TLSv1.2"); + imap.setSSLProtocol("TLSv1.2"); + + AtomicBoolean stopped = new AtomicBoolean(false); + Runnable stop = () -> { + if (stopped.compareAndSet(false, true)) { + stopQuietly(imap); + stopQuietly(pop3); + } + }; + Runtime.getRuntime().addShutdownHook(new Thread(stop, + "bongo-vtestmail-shutdown")); + + try { + pop3.start(); + imap.start(); + exportCertificate(caFile); + writeReadyFile(readyFile, pop3.getPort(), imap.getPort()); + new CountDownLatch(1).await(); + } finally { + stop.run(); + } + } + + private static void addMessage(MailboxStore store, String username, + String address, String marker) { + Mailbox mailbox = store.createMailbox(username, PASSWORD, address); + String message = "From: fixture@example.test\r\n" + + "To: " + address + "\r\n" + + "Subject: Bongo Collector " + marker + " test\r\n" + + "Message-ID: \r\n" + + "X-Bongo-Vtestmail: " + marker + "\r\n" + + "\r\n" + + "Collector integration fixture.\r\n"; + mailbox.getInbox().addMessage(message); + } + + private static void exportCertificate(Path destination) throws Exception { + KeyStore store = KeyStore.getInstance("PKCS12"); + try (InputStream stream = BongoVtestmailFixture.class.getClassLoader() + .getResourceAsStream("vtestmail.pfx")) { + if (stream == null) { + throw new IOException("vtestmail.pfx is missing from the classpath"); + } + store.load(stream, "changeit".toCharArray()); + } + Certificate certificate = store.getCertificate("localhost"); + if (certificate == null) { + throw new IOException("localhost certificate is missing from vtestmail.pfx"); + } + String encoded = Base64.getMimeEncoder(64, new byte[] {'\n'}) + .encodeToString(certificate.getEncoded()); + String pem = "-----BEGIN CERTIFICATE-----\n" + encoded + + "\n-----END CERTIFICATE-----\n"; + Files.writeString(destination, pem, StandardCharsets.US_ASCII); + } + + private static void writeReadyFile(Path destination, int pop3Port, + int imapPort) throws IOException { + Path temporary = destination.resolveSibling( + destination.getFileName().toString() + ".tmp"); + String properties = "pop3=" + pop3Port + "\n" + + "imap=" + imapPort + "\n"; + Files.writeString(temporary, properties, StandardCharsets.US_ASCII); + try { + Files.move(temporary, destination, StandardCopyOption.ATOMIC_MOVE, + StandardCopyOption.REPLACE_EXISTING); + } catch (AtomicMoveNotSupportedException exception) { + Files.move(temporary, destination, + StandardCopyOption.REPLACE_EXISTING); + } + } + + private static void stopQuietly(AutoCloseable server) { + try { + server.close(); + } catch (Exception ignored) { + // The test process is already shutting down. + } + } + + /* + * vtestmail 1.0.0 advertises IMAP4rev2, but its release does not yet + * implement UID SEARCH/FETCH/STORE. The Collector needs exactly this + * narrow subset, so the fixture supplies it without patching the jar. + */ + private static final class UidCommand extends ImapCommand { + + private final String parameters; + + private UidCommand(String parameters) { + this.parameters = parameters; + } + + static UidCommand parse(String parameters) throws ImapException { + isNotEmpty(parameters); + return new UidCommand(parameters); + } + + @Override + public String toString() { + return "UID " + parameters; + } + + @Override + protected void execute(ImapServer server, ImapSession session, + ImapClient client, String tag) throws IOException, ImapException { + session.assertState(State.Selected); + String upper = parameters.toUpperCase(Locale.ROOT); + if (upper.equals("SEARCH ALL")) { + search(session, client, tag); + } else if (upper.startsWith("FETCH ")) { + fetch(session, client, tag); + } else if (upper.startsWith("STORE ")) { + store(session, client, tag); + } else { + client.writeLine(tag + " BAD unsupported UID command"); + } + } + + private void search(ImapSession session, ImapClient client, String tag) + throws IOException { + StringBuilder response = new StringBuilder("* SEARCH"); + for (MailboxMessage message : session.getFolder().getMessages()) { + response.append(' ').append(message.getUID()); + } + client.writeLine(response.toString()); + client.writeLine(tag + " OK UID SEARCH completed"); + } + + private void fetch(ImapSession session, ImapClient client, String tag) + throws IOException { + int uid = firstNumber(parameters.substring("FETCH ".length())); + List messages = session.getFolder().getMessages(); + for (int index = 0; index < messages.size(); index++) { + MailboxMessage message = messages.get(index); + if (message.getUID() == uid) { + String content = message.getContent(); + int length = content.getBytes(StandardCharsets.ISO_8859_1).length; + client.writeLine("* " + (index + 1) + " FETCH (UID " + uid + + " BODY[] {" + length + "}\r\n" + content + ")"); + client.writeLine(tag + " OK UID FETCH completed"); + return; + } + } + client.writeLine(tag + " OK UID FETCH completed"); + } + + private void store(ImapSession session, ImapClient client, String tag) + throws IOException, ImapException { + session.assertReadWrite(); + int uid = firstNumber(parameters.substring("STORE ".length())); + boolean delete = parameters.toUpperCase(Locale.ROOT) + .contains("+FLAGS.SILENT (\\DELETED)"); + if (!delete) { + client.writeLine(tag + " BAD unsupported UID STORE operation"); + return; + } + for (MailboxMessage message : session.getFolder().getMessages()) { + if (message.getUID() == uid) { + message.setDeleted(true); + break; + } + } + client.writeLine(tag + " OK UID STORE completed"); + } + + private static int firstNumber(String value) { + int end = 0; + while (end < value.length() && Character.isDigit(value.charAt(end))) { + end++; + } + if (end == 0) { + return -1; + } + try { + return Integer.parseInt(value.substring(0, end)); + } catch (NumberFormatException exception) { + return -1; + } + } + } +} diff --git a/src/agents/collector/tests/vtestmail-integration.py b/src/agents/collector/tests/vtestmail-integration.py new file mode 100644 index 0000000..66a1396 --- /dev/null +++ b/src/agents/collector/tests/vtestmail-integration.py @@ -0,0 +1,119 @@ +# /**************************************************************************** +# * +# * Copyright (c) 2001 Novell, Inc. All Rights Reserved. +# * +# * This program is free software; you can redistribute it and/or +# * modify it under the terms of version 2 of the GNU General Public License +# * as published by the Free Software Foundation. +# * +# * This program is distributed in the hope that it will be useful, +# * but WITHOUT ANY WARRANTY; without even the implied warranty of +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# * GNU General Public License for more details. +# * +# * You should have received a copy of the GNU General Public License +# * along with this program; if not, contact Novell, Inc. +# * +# * To contact Novell about this file by physical or electronic mail, you +# * may find current contact information at www.novell.com. +# * +# ****************************************************************************/ + +import argparse +import os +from pathlib import Path +import subprocess +import tempfile +import time + + +def read_properties(path): + properties = {} + for line in path.read_text(encoding="ascii").splitlines(): + if line and not line.startswith("#") and "=" in line: + name, value = line.split("=", 1) + properties[name] = value + return properties + + +def wait_until_ready(process, ready_file, timeout=15): + deadline = time.monotonic() + timeout + while time.monotonic() < deadline: + if ready_file.is_file(): + return read_properties(ready_file) + if process.poll() is not None: + raise RuntimeError("vtestmail fixture exited before becoming ready") + time.sleep(0.05) + raise TimeoutError("vtestmail fixture did not become ready") + + +def run_probe(executable, protocol, port, username, marker, environment): + completed = subprocess.run( + [executable, protocol, port, username, "collector-secret", marker], + env=environment, + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + timeout=30, + check=False, + ) + if completed.returncode != 0: + raise RuntimeError( + f"{protocol} Collector probe failed:\n{completed.stdout}" + ) + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("--java", required=True) + parser.add_argument("--classpath", required=True) + parser.add_argument("--probe", required=True) + arguments = parser.parse_args() + + with tempfile.TemporaryDirectory(prefix="bongo-vtestmail-") as directory: + root = Path(directory) + ready_file = root / "ready.properties" + ca_file = root / "vtestmail-ca.pem" + log_file = root / "fixture.log" + + with log_file.open("w+b") as log: + fixture = subprocess.Popen( + [ + arguments.java, + "-classpath", + arguments.classpath, + "net.markwalder.vtestmail.imap.BongoVtestmailFixture", + str(ready_file), + str(ca_file), + ], + stdin=subprocess.DEVNULL, + stdout=log, + stderr=subprocess.STDOUT, + ) + try: + properties = wait_until_ready(fixture, ready_file) + environment = os.environ.copy() + environment["BONGO_COLLECTOR_CAINFO"] = str(ca_file) + run_probe(arguments.probe, "pop3", properties["pop3"], + "pop-user", "pop3", environment) + run_probe(arguments.probe, "imap", properties["imap"], + "imap-user", "imap", environment) + except Exception: + log.flush() + log.seek(0) + fixture_log = log.read().decode("utf-8", errors="replace") + if fixture_log: + print("vtestmail fixture log:\n" + fixture_log) + raise + finally: + if fixture.poll() is None: + fixture.terminate() + try: + fixture.wait(timeout=10) + except subprocess.TimeoutExpired: + fixture.kill() + fixture.wait(timeout=5) + + +if __name__ == "__main__": + main() diff --git a/src/agents/collector/tests/vtestmail-transport-test.c b/src/agents/collector/tests/vtestmail-transport-test.c new file mode 100644 index 0000000..0cb91a5 --- /dev/null +++ b/src/agents/collector/tests/vtestmail-transport-test.c @@ -0,0 +1,158 @@ +/**************************************************************************** + * + * Copyright (c) 2001 Novell, Inc. All Rights Reserved. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of version 2 of the GNU General Public License + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, contact Novell, Inc. + * + * To contact Novell about this file by physical or electronic mail, you + * may find current contact information at www.novell.com. + * + ****************************************************************************/ + +#include +#include +#include +#include + +#include + +#include "external_transport.h" + +static int +CopyField(char *destination, size_t size, const char *source) +{ + int written = snprintf(destination, size, "%s", source); + return written < 0 || (size_t) written >= size ? -1 : 0; +} + +static int +ParsePort(const char *value) +{ + char *end = NULL; + long port; + errno = 0; + port = strtol(value, &end, 10); + if (errno != 0 || end == value || *end != '\0' || port < 1 || port > 65535) { + return -1; + } + return (int) port; +} + +static int +MessageContains(FILE *message, size_t message_size, const char *marker) +{ + char *data; + size_t read_size; + int found; + if (message_size > 1024U * 1024U) return 0; + data = malloc(message_size + 1); + if (data == NULL) return 0; + rewind(message); + read_size = fread(data, 1, message_size, message); + data[read_size] = '\0'; + found = read_size == message_size && strstr(data, marker) != NULL; + free(data); + return found; +} + +int +main(int argc, char **argv) +{ + CollectorExternalAccount account = {0}; + CollectorRemoteIds ids = {0}; + CollectorRemoteIds after_delete = {0}; + char expected[128]; + char error[512] = {0}; + FILE *message = NULL; + size_t message_size = 0; + int port; + int result = 1; + + if (argc != 6 || (strcmp(argv[1], "pop3") != 0 && + strcmp(argv[1], "imap") != 0)) { + fprintf(stderr, "usage: %s pop3|imap PORT USER PASSWORD MARKER\n", + argv[0]); + return 2; + } + port = ParsePort(argv[2]); + if (port < 0 || + CopyField(account.inbound_protocol, + sizeof(account.inbound_protocol), argv[1]) != 0 || + CopyField(account.inbound_host, + sizeof(account.inbound_host), "localhost") != 0 || + CopyField(account.inbound_tls, + sizeof(account.inbound_tls), "starttls") != 0 || + CopyField(account.inbound_username, + sizeof(account.inbound_username), argv[3]) != 0 || + CopyField(account.inbound_mailbox, + sizeof(account.inbound_mailbox), "INBOX") != 0 || + snprintf(expected, sizeof(expected), "X-Bongo-Vtestmail: %s", argv[5]) >= + (int) sizeof(expected)) { + fprintf(stderr, "invalid Collector probe arguments\n"); + return 2; + } + account.inbound_port = port; + + if (curl_global_init(CURL_GLOBAL_DEFAULT) != CURLE_OK) { + fprintf(stderr, "cannot initialise libcurl\n"); + return 1; + } + if (CollectorRemoteList(&account, argv[4], &ids, + error, sizeof(error)) != 0) { + fprintf(stderr, "%s list failed: %s\n", argv[1], error); + goto done; + } + if (ids.count != 1) { + fprintf(stderr, "%s list returned %zu messages instead of one\n", + argv[1], ids.count); + goto done; + } + message = tmpfile(); + if (message == NULL) { + perror("tmpfile"); + goto done; + } + if (CollectorRemoteFetch(&account, argv[4], &ids.items[0], message, + &message_size, error, sizeof(error)) != 0) { + fprintf(stderr, "%s fetch failed: %s\n", argv[1], error); + goto done; + } + if (!MessageContains(message, message_size, expected)) { + fprintf(stderr, "%s fetched message does not contain fixture marker\n", + argv[1]); + goto done; + } + if (CollectorRemoteDelete(&account, argv[4], &ids.items[0], + error, sizeof(error)) != 0) { + fprintf(stderr, "%s deletion failed: %s\n", argv[1], error); + goto done; + } + if (CollectorRemoteList(&account, argv[4], &after_delete, + error, sizeof(error)) != 0) { + fprintf(stderr, "%s post-deletion list failed: %s\n", argv[1], error); + goto done; + } + if (after_delete.count != 0) { + fprintf(stderr, "%s deletion left %zu messages behind\n", + argv[1], after_delete.count); + goto done; + } + result = 0; + +done: + if (message != NULL) fclose(message); + CollectorRemoteIdsFree(&after_delete); + CollectorRemoteIdsFree(&ids); + curl_global_cleanup(); + return result; +}