Add vtestmail Collector integration tests

This commit is contained in:
Mario Fetka
2026-07-18 14:59:06 +02:00
parent 8c73eb0588
commit 75229111d6
7 changed files with 630 additions and 0 deletions
+27
View File
@@ -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.
+6
View File
@@ -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
+62
View File
@@ -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 "$<TARGET_FILE:collector-vtestmail-transport-test>")
set_tests_properties(collector-vtestmail-integration PROPERTIES
LABELS "collector;integration"
TIMEOUT 60)
endif()
endif()
+12
View File
@@ -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);
@@ -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: <collector-" + marker + "@example.test>\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<MailboxMessage> 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;
}
}
}
}
@@ -0,0 +1,119 @@
# /****************************************************************************
# * <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. 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.
# * </Novell-copyright>
# ****************************************************************************/
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()
@@ -0,0 +1,158 @@
/****************************************************************************
* <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. 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.
* </Novell-copyright>
****************************************************************************/
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <curl/curl.h>
#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;
}