Skip to content

Import & Export

Export and import allows you to persist inbox credentials for later use. This is useful for debugging failed tests, manual testing scenarios, and cross-environment verification.

Export/Import enables you to:

  • Persist inbox credentials to disk
  • Resume inbox access in later test runs
  • Debug email issues by replaying scenarios
  • Share inboxes across environments (securely)

Exported inbox data is stored as JSON:

{
"version": 1,
"emailAddress": "[email protected]",
"expiresAt": "2024-01-15T10:30:00Z",
"inboxHash": "hash123...",
"serverSigPk": "base64url...",
"secretKey": "base64url...",
"exportedAt": "2024-01-14T10:30:00Z"
}

The public key is not included in exports as it can be derived from the secret key during import.

Inbox inbox = client.createInbox();
// Export to object
ExportedInbox exported = client.exportInbox(inbox);
// Or export by email address
ExportedInbox exported = client.exportInbox(inbox.getEmailAddress());
// Access properties
String email = exported.getEmailAddress();
String expiresAt = exported.getExpiresAt();
Inbox inbox = client.createInbox();
// Export directly to JSON file
client.exportInboxToFile(inbox, Path.of("inbox.json"));
// Restore inbox from exported data
Inbox restored = client.importInbox(exported);
// Use restored inbox normally
Email email = restored.waitForEmail();
// Import from JSON file
Inbox restored = client.importInboxFromFile(Path.of("inbox.json"));
// Use restored inbox
List<Email> emails = restored.listEmails();
PropertyTypeDescription
versionintExport format version (currently 1)
emailAddressStringInbox email address
expiresAtStringISO 8601 expiration timestamp
inboxHashStringUnique inbox identifier
serverSigPkStringServer signature public key (ML-DSA-65, base64url)
secretKeyStringML-KEM-768 secret key (base64url)
exportedAtStringISO 8601 export timestamp

The validate() method performs comprehensive integrity checks:

CheckDescription
VersionMust be 1 (current format version)
Required fieldsAll fields must be present and non-empty
Email formatMust contain exactly one @ character
Timestamp formatexpiresAt and exportedAt must be valid ISO 8601
Base64url encodingKeys must be valid base64url-encoded strings
Secret key sizeMust be exactly 2400 bytes (raw ML-KEM-768)
Server signature key sizeMust be exactly 1952 bytes (raw ML-DSA-65)
ExpirationInbox must not have expired
ExportedInbox exported = client.exportInbox(inbox);
// Validate data integrity
try {
exported.validate();
} catch (InvalidImportDataException e) {
System.err.println("Export data is invalid:");
for (String error : e.getErrors()) {
System.err.println(" - " + error);
}
}
// Check expiration manually (also done by validate())
Instant expires = Instant.parse(exported.getExpiresAt());
if (expires.isBefore(Instant.now())) {
throw new IllegalStateException("Inbox has expired");
}
Path tempFile = Files.createTempFile("inbox-", ".json");
try {
client.exportInboxToFile(inbox, tempFile);
// Use file...
} finally {
Files.deleteIfExists(tempFile);
}
.gitignore
*-inbox.json
*.inbox.json
/test-exports/
// If you must persist, encrypt the export
ExportedInbox exported = client.exportInbox(inbox);
String json = gson.toJson(exported);
String encrypted = encryptionService.encrypt(json);
Files.writeString(Path.of("inbox.enc"), encrypted);

Export inbox on test failure for later debugging:

class EmailTest {
private Inbox inbox;
private boolean failed = false;
@AfterEach
void cleanup(TestInfo info) throws IOException {
if (failed && inbox != null) {
Path path = Path.of(
"build/failed-inboxes",
info.getDisplayName().replaceAll("[^a-zA-Z0-9]", "_") + ".json"
);
Files.createDirectories(path.getParent());
client.exportInboxToFile(inbox, path);
System.out.println("Exported inbox to: " + path);
}
}
@Test
void testEmailFlow() {
try {
inbox = client.createInbox();
// Test code...
} catch (AssertionError | Exception e) {
failed = true;
throw e;
}
}
}

Reproduce the issue later:

Inbox inbox = client.importInboxFromFile(
Path.of("build/failed-inboxes/testEmailFlow.json")
);
for (Email email : inbox.listEmails()) {
System.out.println("Subject: " + email.getSubject());
System.out.println("From: " + email.getFrom());
System.out.println("Text: " + email.getText());
System.out.println("---");
}

Create a long-lived inbox for manual testing:

Inbox inbox = client.createInbox(
CreateInboxOptions.builder()
.ttl(Duration.ofHours(24))
.build()
);
System.out.println("Send test emails to: " + inbox.getEmailAddress());
System.out.println("Expires: " + inbox.getExpiresAt());
// Export for later use
client.exportInboxToFile(inbox, Path.of("manual-test-inbox.json"));

Check results later:

Inbox inbox = client.importInboxFromFile(Path.of("manual-test-inbox.json"));
List<Email> emails = inbox.listEmails();
System.out.println("Received " + emails.size() + " emails");

Get raw MIME content for detailed debugging:

Inbox inbox = client.importInboxFromFile(Path.of("debug-inbox.json"));
for (Email email : inbox.listEmails()) {
System.out.println("=== " + email.getSubject() + " ===");
// Get raw MIME content
String raw = inbox.getRawEmail(email.getId());
System.out.println(raw);
}
try {
Inbox inbox = client.importInbox(exported);
} catch (InvalidImportDataException e) {
System.err.println("Invalid data: " + e.getMessage());
// Data may be corrupted, incomplete, or tampered with
}
try {
Inbox inbox = client.importInbox(exported);
} catch (InboxAlreadyExistsException e) {
// Inbox was already imported in this client
Inbox existing = client.getInbox(exported.getEmailAddress());
}

Check expiration before importing:

ExportedInbox exported = loadFromFile(path);
Instant expires = Instant.parse(exported.getExpiresAt());
if (expires.isBefore(Instant.now())) {
throw new IllegalStateException(
"Inbox expired at " + exported.getExpiresAt()
);
}
Inbox inbox = client.importInbox(exported);
try {
Inbox inbox = client.importInboxFromFile(Path.of("inbox.json"));
} catch (IOException e) {
System.err.println("Could not read file: " + e.getMessage());
}
class SaveOnFailureExtension implements TestWatcher {
@Override
public void testFailed(ExtensionContext context, Throwable cause) {
// Get inbox from test instance
Object testInstance = context.getRequiredTestInstance();
if (testInstance instanceof EmailTestBase base) {
Inbox inbox = base.getInbox();
if (inbox != null) {
try {
Path path = Path.of(
"build/failed-inboxes",
context.getDisplayName() + ".json"
);
Files.createDirectories(path.getParent());
base.getClient().exportInboxToFile(inbox, path);
} catch (IOException e) {
// Ignore
}
}
}
}
}
@ExtendWith(SaveOnFailureExtension.class)
abstract class EmailTestBase {
protected abstract Inbox getInbox();
protected abstract VaultSandboxClient getClient();
}
class TestInboxes {
private static final Path EXPORTS_DIR = Path.of("src/test/resources/inboxes");
private final VaultSandboxClient client;
public TestInboxes(VaultSandboxClient client) {
this.client = client;
}
public void save(String name, Inbox inbox) throws IOException {
Files.createDirectories(EXPORTS_DIR);
client.exportInboxToFile(inbox, EXPORTS_DIR.resolve(name + ".json"));
}
public Inbox load(String name) throws IOException {
return client.importInboxFromFile(EXPORTS_DIR.resolve(name + ".json"));
}
}
LimitationDescription
Inboxes expireExport doesn’t extend TTL - inbox still expires at original time
Same API key requiredImport must use the same API key that created the inbox
Contains private keysSecurity risk if export files are leaked
Metadata onlyExport contains credentials, not the emails themselves
  1. Security First

    • Use temp files and delete after use
    • Never log or commit private keys
    • Encrypt exports if persistence is required
  2. Check Expiration

    • Validate expiration before import
    • Use appropriate TTL when creating inboxes
    • Handle expired imports gracefully
  3. Validate Data

    • Call validate() before import
    • Catch and handle import exceptions
    • Log failures for debugging
  4. Appropriate Use Cases

    • Debugging failed tests
    • Manual testing scenarios
    • Cross-environment verification
    • Not for long-term storage (inboxes expire)