Add vtestmail Collector integration tests
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user